2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
19 if sys
.version_info
< (2, 6, 0):
20 sys
.stderr
.write("python 2.6 or later is required run this script\n")
23 NeededMatcher
= re
.compile('^ *NEEDED *([^ ]+)\n$')
24 FormatMatcher
= re
.compile('^(.+):\\s*file format (.+)\n$')
27 # Names returned by Linux's objdump:
28 'elf64-x86-64': 'x86-64',
29 'elf32-i386': 'x86-32',
30 'elf32-little': 'arm',
31 'elf32-littlearm': 'arm',
32 # Names returned by x86_64-nacl-objdump:
33 'elf64-nacl': 'x86-64',
34 'elf32-nacl': 'x86-32',
44 # These constants are used within nmf files.
45 RUNNABLE_LD
= 'runnable-ld.so' # Name of the dynamic loader
46 MAIN_NEXE
= 'main.nexe' # Name of entry point for execution
47 PROGRAM_KEY
= 'program' # Key of the program section in an nmf file
48 URL_KEY
= 'url' # Key of the url field for a particular file in an nmf file
49 FILES_KEY
= 'files' # Key of the files section in an nmf file
50 PORTABLE_KEY
= 'portable' # key for portable section of manifest
51 TRANSLATE_KEY
= 'pnacl-translate' # key for translatable objects
54 # The proper name of the dynamic linker, as kept in the IRT. This is
55 # excluded from the nmf file by convention.
57 'x86-32': 'ld-nacl-x86-32.so.1',
58 'x86-64': 'ld-nacl-x86-64.so.1',
63 def DebugPrint(message
):
64 if DebugPrint
.debug_mode
:
65 sys
.stderr
.write('%s\n' % message
)
68 DebugPrint
.debug_mode
= False # Set to True to enable extra debug prints
72 """Just like os.makedirs but doesn't generate errors when dirname
75 if os
.path
.isdir(dirname
):
78 Trace("mkdir: %s" % dirname
)
81 except OSError as exception_info
:
82 if exception_info
.errno
!= errno
.EEXIST
:
86 class Error(Exception):
87 '''Local Error class for this file.'''
91 def ParseElfHeader(path
):
92 """Determine properties of a nexe by parsing elf header.
93 Return tuple of architecture and boolean signalling whether
94 the executable is dynamic (has INTERP header) or static.
99 # unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
100 # Elf64_Half e_type; /* Object file type */
101 # Elf64_Half e_machine; /* Architecture */
104 elf_header_format
= '16s2H'
105 elf_header_size
= struct
.calcsize(elf_header_format
)
107 with
open(path
, 'rb') as f
:
108 header
= f
.read(elf_header_size
)
110 header
= struct
.unpack(elf_header_format
, header
)
111 e_ident
, _
, e_machine
= header
[:3]
113 elf_magic
= '\x7fELF'
114 if e_ident
[:4] != elf_magic
:
115 raise Error('Not a valid NaCL executable: %s' % path
)
117 e_machine_mapping
= {
122 if e_machine
not in e_machine_mapping
:
123 raise Error('Unknown machine type: %s' % e_machine
)
125 # Set arch based on the machine type in the elf header
126 arch
= e_machine_mapping
[e_machine
]
128 # Now read the full header in either 64bit or 32bit mode
129 dynamic
= IsDynamicElf(path
, arch
== 'x86-64')
133 def IsDynamicElf(path
, is64bit
):
134 """Examine an elf file to determine if it is dynamically
136 This is determined by searching the program headers for
137 a header of type PT_INTERP.
140 elf_header_format
= '16s2HI3QI3H'
142 elf_header_format
= '16s2HI3II3H'
144 elf_header_size
= struct
.calcsize(elf_header_format
)
146 with
open(path
, 'rb') as f
:
147 header
= f
.read(elf_header_size
)
148 header
= struct
.unpack(elf_header_format
, header
)
149 p_header_offset
= header
[5]
150 p_header_entry_size
= header
[9]
151 num_p_header
= header
[10]
152 f
.seek(p_header_offset
)
153 p_headers
= f
.read(p_header_entry_size
*num_p_header
)
155 # Read the first word of each Phdr to find out its type.
159 # Elf32_Word p_type; /* Segment type */
162 elf_phdr_format
= 'I'
166 p_header
= p_headers
[:p_header_entry_size
]
167 p_headers
= p_headers
[p_header_entry_size
:]
168 phdr_type
= struct
.unpack(elf_phdr_format
, p_header
[:4])[0]
169 if phdr_type
== PT_INTERP
:
175 class ArchFile(object):
176 '''Simple structure containing information about
179 name: Name of this file
180 path: Full path to this file on the build system
181 arch: Architecture of this file (e.g., x86-32)
182 url: Relative path to file in the staged web directory.
183 Used for specifying the "url" attribute in the nmf file.'''
185 def __init__(self
, name
, path
, url
, arch
=None):
191 self
.arch
= ParseElfHeader(path
)[0]
194 return '<ArchFile %s>' % self
.path
197 '''Return the file path when invoked with the str() function'''
201 class NmfUtils(object):
202 '''Helper class for creating and managing nmf files
205 manifest: A JSON-structured dict containing the nmf structure
206 needed: A dict with key=filename and value=ArchFile (see GetNeeded)
209 def __init__(self
, main_files
=None, objdump
=None,
210 lib_path
=None, extra_files
=None, lib_prefix
=None,
215 main_files: List of main entry program files. These will be named
216 files->main.nexe for dynamic nexes, and program for static nexes
217 objdump: path to x86_64-nacl-objdump tool (or Linux equivalent)
218 lib_path: List of paths to library directories
219 extra_files: List of extra files to include in the nmf
220 lib_prefix: A list of path components to prepend to the library paths,
221 both for staging the libraries and for inclusion into the nmf file.
222 Examples: ['..'], ['lib_dir']
223 remap: Remaps the library name in the manifest.
225 self
.objdump
= objdump
226 self
.main_files
= main_files
or []
227 self
.extra_files
= extra_files
or []
228 self
.lib_path
= lib_path
or []
231 self
.lib_prefix
= lib_prefix
or []
232 self
.remap
= remap
or {}
233 self
.pnacl
= main_files
and main_files
[0].endswith('pexe')
235 def GleanFromObjdump(self
, files
):
236 '''Get architecture and dependency information for given files
239 files: A dict with key=filename and value=list or set of archs. E.g.:
240 { '/path/to/my.nexe': ['x86-32']
241 '/path/to/lib64/libmy.so': ['x86-64'],
242 '/path/to/mydata.so': ['x86-32', 'x86-64'],
243 '/path/to/my.data': None } # Indicates all architectures
245 Returns: A tuple with the following members:
246 input_info: A dict with key=filename and value=ArchFile of input files.
247 Includes the input files as well, with arch filled in if absent.
248 Example: { '/path/to/my.nexe': ArchFile(my.nexe),
249 '/path/to/libfoo.so': ArchFile(libfoo.so) }
250 needed: A set of strings formatted as "arch/name". Example:
251 set(['x86-32/libc.so', 'x86-64/libgcc.so'])
254 raise Error('No objdump executable specified (see --help for more info)')
255 DebugPrint('GleanFromObjdump(%s)' % ([self
.objdump
, '-p'] + files
.keys()))
256 proc
= subprocess
.Popen([self
.objdump
, '-p'] + files
.keys(),
257 stdout
=subprocess
.PIPE
,
258 stderr
=subprocess
.PIPE
, bufsize
=-1)
261 output
, err_output
= proc
.communicate()
263 raise Error('%s\nStdError=%s\nobjdump failed with error code: %d' %
264 (output
, err_output
, proc
.returncode
))
266 for line
in output
.splitlines(True):
267 # Objdump should display the architecture first and then the dependencies
268 # second for each file in the list.
269 matched
= FormatMatcher
.match(line
)
271 filename
= matched
.group(1)
272 arch
= OBJDUMP_ARCH_MAP
[matched
.group(2)]
273 if files
[filename
] is None or arch
in files
[filename
]:
274 name
= os
.path
.basename(filename
)
275 input_info
[filename
] = ArchFile(
279 url
='/'.join(self
.lib_prefix
+ [ARCH_LOCATION
[arch
], name
]))
280 matched
= NeededMatcher
.match(line
)
282 if files
[filename
] is None or arch
in files
[filename
]:
283 needed
.add('/'.join([arch
, matched
.group(1)]))
284 return input_info
, needed
286 def FindLibsInPath(self
, name
):
287 '''Finds the set of libraries matching |name| within lib_path
290 name: name of library to find
293 A list of system paths that match the given name within the lib_path'''
295 for dirname
in self
.lib_path
:
296 filename
= os
.path
.join(dirname
, name
)
297 if os
.path
.exists(filename
):
298 files
.append(filename
)
300 raise Error('cannot find library %s' % name
)
304 '''Collect the list of dependencies for the main_files
307 A dict with key=filename and value=ArchFile of input files.
308 Includes the input files as well, with arch filled in if absent.
309 Example: { '/path/to/my.nexe': ArchFile(my.nexe),
310 '/path/to/libfoo.so': ArchFile(libfoo.so) }'''
315 DebugPrint('GetNeeded(%s)' % self
.main_files
)
317 dynamic
= any(ParseElfHeader(f
)[1] for f
in self
.main_files
)
321 all_files
, unexamined
= self
.GleanFromObjdump(
322 dict([(f
, None) for f
in self
.main_files
]))
323 for name
, arch_file
in all_files
.items():
326 unexamined
.add('/'.join([arch_file
.arch
, RUNNABLE_LD
]))
328 files_to_examine
= {}
329 for arch_name
in unexamined
:
330 arch
, name
= arch_name
.split('/')
331 for path
in self
.FindLibsInPath(name
):
332 files_to_examine
.setdefault(path
, set()).add(arch
)
333 new_files
, needed
= self
.GleanFromObjdump(files_to_examine
)
334 all_files
.update(new_files
)
335 examined |
= unexamined
336 unexamined
= needed
- examined
338 # With the runnable-ld.so scheme we have today, the proper name of
339 # the dynamic linker should be excluded from the list of files.
340 ldso
= [LD_NACL_MAP
[arch
] for arch
in set(OBJDUMP_ARCH_MAP
.values())]
341 for name
, arch_map
in all_files
.items():
342 if arch_map
.name
in ldso
:
345 self
.needed
= all_files
347 for filename
in self
.main_files
:
348 url
= os
.path
.split(filename
)[1]
349 archfile
= ArchFile(name
=os
.path
.basename(filename
),
350 path
=filename
, url
=url
)
351 self
.needed
[filename
] = archfile
355 def StageDependencies(self
, destination_dir
):
356 '''Copies over the dependencies into a given destination directory
358 Each library will be put into a subdirectory that corresponds to the arch.
361 destination_dir: The destination directory for staging the dependencies
363 nexe_root
= os
.path
.dirname(os
.path
.abspath(self
.main_files
[0]))
364 nexe_root
= os
.path
.normcase(nexe_root
)
366 needed
= self
.GetNeeded()
367 for source
, arch_file
in needed
.items():
368 urldest
= arch_file
.url
370 # for .nexe and .so files specified on the command line stage
371 # them in paths relative to the .nexe (with the .nexe always
372 # being staged at the root).
373 if source
in self
.main_files
:
374 absdest
= os
.path
.normcase(os
.path
.abspath(urldest
))
375 if absdest
.startswith(nexe_root
):
376 urldest
= os
.path
.relpath(urldest
, nexe_root
)
378 destination
= os
.path
.join(destination_dir
, urldest
)
380 if (os
.path
.normcase(os
.path
.abspath(source
)) ==
381 os
.path
.normcase(os
.path
.abspath(destination
))):
384 # make sure target dir exists
385 MakeDir(os
.path
.dirname(destination
))
387 Trace('copy: %s -> %s' % (source
, destination
))
388 shutil
.copy2(source
, destination
)
390 def _GeneratePNaClManifest(self
):
392 manifest
[PROGRAM_KEY
] = {}
393 manifest
[PROGRAM_KEY
][PORTABLE_KEY
] = {}
394 sha
= hashlib
.sha256()
395 with
open(self
.main_files
[0], 'rb') as f
:
397 manifest
[PROGRAM_KEY
][PORTABLE_KEY
][TRANSLATE_KEY
] = {
398 "url": os
.path
.basename(self
.main_files
[0]),
399 "sha256": sha
.hexdigest()
401 self
.manifest
= manifest
403 def _GenerateManifest(self
):
404 '''Create a JSON formatted dict containing the files
406 NaCl will map url requests based on architecture. The startup NEXE
407 can always be found under the top key PROGRAM. Additional files are under
408 the FILES key further mapped by file name. In the case of 'runnable' the
409 PROGRAM key is populated with urls pointing the runnable-ld.so which acts
410 as the startup nexe. The application itself is then placed under the
411 FILES key mapped as 'main.exe' instead of the original name so that the
412 loader can find it. '''
413 manifest
= { FILES_KEY
: {}, PROGRAM_KEY
: {} }
415 needed
= self
.GetNeeded()
417 runnable
= any(n
.endswith(RUNNABLE_LD
) for n
in needed
)
419 extra_files_kv
= [(key
, ArchFile(name
=key
,
423 for key
, arch
, url
in self
.extra_files
]
425 nexe_root
= os
.path
.dirname(os
.path
.abspath(self
.main_files
[0]))
427 for need
, archinfo
in needed
.items() + extra_files_kv
:
428 urlinfo
= { URL_KEY
: archinfo
.url
}
431 # If starting with runnable-ld.so, make that the main executable.
433 if need
.endswith(RUNNABLE_LD
):
434 manifest
[PROGRAM_KEY
][archinfo
.arch
] = urlinfo
437 if need
in self
.main_files
:
438 # Ensure that the .nexe and .so names are relative to the root
439 # of where the .nexe lives.
440 if os
.path
.abspath(urlinfo
[URL_KEY
]).startswith(nexe_root
):
441 urlinfo
[URL_KEY
] = os
.path
.relpath(urlinfo
[URL_KEY
], nexe_root
)
443 if need
.endswith(".nexe"):
444 # Place it under program if we aren't using the runnable-ld.so.
446 manifest
[PROGRAM_KEY
][archinfo
.arch
] = urlinfo
448 # Otherwise, treat it like another another file named main.nexe.
451 name
= self
.remap
.get(name
, name
)
452 fileinfo
= manifest
[FILES_KEY
].get(name
, {})
453 fileinfo
[archinfo
.arch
] = urlinfo
454 manifest
[FILES_KEY
][name
] = fileinfo
455 self
.manifest
= manifest
457 def GetManifest(self
):
458 '''Returns a JSON-formatted dict containing the NaCl dependencies'''
459 if not self
.manifest
:
461 self
._GeneratePNaClManifest
()
463 self
._GenerateManifest
()
467 '''Returns the Manifest as a JSON-formatted string'''
468 pretty_string
= json
.dumps(self
.GetManifest(), indent
=2)
469 # json.dumps sometimes returns trailing whitespace and does not put
470 # a newline at the end. This code fixes these problems.
471 pretty_lines
= pretty_string
.split('\n')
472 return '\n'.join([line
.rstrip() for line
in pretty_lines
]) + '\n'
477 sys
.stderr
.write(str(msg
) + '\n')
479 Trace
.verbose
= False
482 def ParseExtraFiles(encoded_list
, err
):
483 """Parse the extra-files list and return a canonicalized list of
484 [key, arch, url] triples. The |encoded_list| should be a list of
485 strings of the form 'key:url' or 'key:arch:url', where an omitted
486 'arch' is taken to mean 'portable'.
488 All entries in |encoded_list| are checked for syntax errors before
489 returning. Error messages are written to |err| (typically
490 sys.stderr) so that the user has actionable feedback for fixing all
491 errors, rather than one at a time. If there are any errors, None is
492 returned instead of a list, since an empty list is a valid return
497 for ix
in range(len(encoded_list
)):
498 kv
= encoded_list
[ix
]
499 unquoted
= quote
.unquote(kv
, ':')
500 if len(unquoted
) == 3:
501 if unquoted
[1] != ':':
502 err
.write('Syntax error for key:value tuple ' +
503 'for --extra-files argument: ' + kv
+ '\n')
506 canonicalized
.append([unquoted
[0], 'portable', unquoted
[2]])
507 elif len(unquoted
) == 5:
508 if unquoted
[1] != ':' or unquoted
[3] != ':':
509 err
.write('Syntax error for key:arch:url tuple ' +
510 'for --extra-files argument: ' +
514 canonicalized
.append([unquoted
[0], unquoted
[2], unquoted
[4]])
516 err
.write('Bad key:arch:url tuple for --extra-files: ' + kv
+ '\n')
523 parser
= optparse
.OptionParser(
524 usage
='Usage: %prog [options] nexe [extra_libs...]')
525 parser
.add_option('-o', '--output', dest
='output',
526 help='Write manifest file to FILE (default is stdout)',
528 parser
.add_option('-D', '--objdump', dest
='objdump',
529 help='Use TOOL as the "objdump" tool to run',
531 parser
.add_option('-L', '--library-path', dest
='lib_path',
532 action
='append', default
=[],
533 help='Add DIRECTORY to library search path',
535 parser
.add_option('-P', '--path-prefix', dest
='path_prefix', default
='',
536 help='A path to prepend to shared libraries in the .nmf',
538 parser
.add_option('-s', '--stage-dependencies', dest
='stage_dependencies',
539 help='Destination directory for staging libraries',
541 parser
.add_option('-t', '--toolchain', help='Legacy option, do not use')
542 parser
.add_option('-n', '--name', dest
='name',
543 help='Rename FOO as BAR',
544 action
='append', default
=[], metavar
='FOO,BAR')
545 parser
.add_option('-x', '--extra-files',
546 help=('Add extra key:file tuple to the "files"' +
547 ' section of the .nmf'),
548 action
='append', default
=[], metavar
='FILE')
549 parser
.add_option('-v', '--verbose',
550 help='Verbose output', action
='store_true')
551 parser
.add_option('-d', '--debug-mode',
552 help='Debug mode', action
='store_true')
553 options
, args
= parser
.parse_args(argv
)
556 if options
.debug_mode
:
557 DebugPrint
.debug_mode
= True
559 if options
.toolchain
is not None:
560 print 'warning: option -t/--toolchain is deprecated.'
563 raise Error('No nexe files specified. See --help for more info')
565 canonicalized
= ParseExtraFiles(options
.extra_files
, sys
.stderr
)
566 if canonicalized
is None:
567 parser
.error('Bad --extra-files (-x) argument syntax')
570 for ren
in options
.name
:
571 parts
= ren
.split(',')
573 raise Error('Expecting --name=<orig_arch.so>,<new_name.so>')
574 remap
[parts
[0]] = parts
[1]
576 if options
.path_prefix
:
577 path_prefix
= options
.path_prefix
.split('/')
581 nmf
= NmfUtils(objdump
=options
.objdump
,
583 lib_path
=options
.lib_path
,
584 extra_files
=canonicalized
,
585 lib_prefix
=path_prefix
,
589 if options
.output
is None:
590 sys
.stdout
.write(nmf
.GetJson())
592 with
open(options
.output
, 'w') as output
:
593 output
.write(nmf
.GetJson())
595 if options
.stage_dependencies
and not nmf
.pnacl
:
596 Trace('Staging dependencies...')
597 nmf
.StageDependencies(options
.stage_dependencies
)
602 # Invoke this file directly for simple testing.
603 if __name__
== '__main__':
605 rtn
= main(sys
.argv
[1:])
607 sys
.stderr
.write('%s: %s\n' % (os
.path
.basename(__file__
), e
))