2 # Copyright (c) 2012 The Native Client 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.
17 # filetype needs to be imported here because pnacl-driver injects calls to
18 # filetype.ForceFileType into argument parse actions.
19 # TODO(dschuff): That's ugly. Find a better way.
23 from driver_env import env
24 # TODO: import driver_log and change these references from 'foo' to
25 # 'driver_log.foo', or split driver_log further
26 from driver_log import Log, DriverOpen, DriverClose, StringifyCommand, DriverExit, FixArch
27 from driver_temps import TempFiles
28 from shelltools import shell
30 def ParseError(s, leftpos, rightpos, msg):
31 Log.Error("Parse Error: %s", msg)
33 Log.Error(' ' + (' '*leftpos) + ('^'*(rightpos - leftpos + 1)))
37 # Run a command with extra environment settings
38 def RunWithEnv(cmd, **kwargs):
46 def SetExecutableMode(path):
47 if os.name == "posix":
48 realpath = pathtools.tosys(path)
49 # os.umask gets and sets at the same time.
50 # There's no way to get it without setting it.
53 os.chmod(realpath, 0755 & ~umask)
56 def FilterOutArchArgs(args):
57 while '-arch' in args:
58 i = args.index('-arch')
59 args = args[:i] + args[i+2:]
62 # Parse and validate the target triple and return the architecture.
63 # We don't attempt to recognize all possible targets here, just the ones we
65 def ParseTriple(triple):
66 tokens = triple.split('-')
71 # The machine/vendor field could be present or not.
72 if os != 'nacl' and len(tokens) >= 3:
74 # Just check that the os is nacl.
78 Log.Fatal('machine/os ' + '-'.join(tokens[1:]) + ' not supported.')
82 if sys.platform == 'darwin':
84 elif sys.platform.startswith('linux'):
86 elif sys.platform in ('cygwin', 'win32'):
89 Log.Fatal('Machine: %s not supported.' % sys.platform)
93 def GetArchNameShort():
94 machine = platform.machine().lower()
95 if machine.startswith('arm'):
97 elif machine.startswith('mips'):
99 elif (machine.startswith('x86')
100 or machine in ('amd32', 'i386', 'i686', 'ia32', '32', 'amd64', '64')):
103 Log.Fatal('Architecture: %s not supported.' % machine)
106 def RunDriver(module_name, args, suppress_inherited_arch_args=False):
108 RunDriver() is used to invoke "driver" tools, e.g.
109 those prefixed with "pnacl-"
111 It automatically appends some additional flags to the invocation
112 which were inherited from the current invocation.
113 Those flags were preserved by ParseArgs
116 if isinstance(args, str):
117 args = shell.split(env.eval(args))
119 script = env.eval('${DRIVER_BIN}/%s' % module_name)
120 script = shell.unescape(script)
122 inherited_driver_args = env.get('INHERITED_DRIVER_ARGS')
123 if suppress_inherited_arch_args:
124 inherited_driver_args = FilterOutArchArgs(inherited_driver_args)
126 script = pathtools.tosys(script)
127 cmd = [script] + args + inherited_driver_args
128 Log.Info('Driver invocation: %s', repr(cmd))
130 module = __import__(module_name)
131 # Save the environment, reset the environment, run
132 # the driver module, and then restore the environment.
135 DriverMain(module, cmd)
139 """ Memoize a function with no arguments """
145 newf.__name__ = f.__name__
152 name = platform.system().lower()
153 if name.startswith('cygwin_nt') or 'windows' in name:
155 if name not in ('linux', 'darwin', 'windows'):
156 Log.Fatal("Unsupported platform '%s'", name)
162 m = platform.machine()
168 if m not in ('i386', 'i686', 'x86_64'):
169 Log.Fatal("Unsupported architecture '%s'", m)
172 # Crawl backwards, starting from the directory containing this script,
173 # until we find a directory satisfying a filter function.
174 def FindBaseDir(function):
176 cur = env.getone('DRIVER_BIN')
177 while not function(cur) and Depth < 16:
178 cur = pathtools.dirname(cur)
187 """ Find native_client/ directory """
188 dir = FindBaseDir(lambda cur: pathtools.basename(cur) == 'native_client')
190 Log.Fatal("Unable to find 'native_client' directory")
191 return shell.escape(dir)
195 def FindBaseToolchain():
196 """ Find toolchain/OS_ARCH directory """
197 base_dir = FindBaseDir(lambda cur: pathtools.basename(cur) == 'toolchain')
199 Log.Fatal("Unable to find 'toolchain' directory")
200 toolchain_dir = os.path.join(
202 '%s_%s' % (GetOSName(), GetArchNameShort())
204 return shell.escape(toolchain_dir)
209 """ Find the base directory of the PNaCl toolchain """
210 # The <base> directory is one level up from the <base>/bin:
211 bindir = env.getone('DRIVER_BIN')
212 basedir = pathtools.dirname(bindir)
213 return shell.escape(basedir)
215 def AddHostBinarySearchPath(prefix):
216 """ Add a path to the list searched for host binaries. """
217 prefix = pathtools.normalize(prefix)
218 if pathtools.isdir(prefix) and not prefix.endswith('/'):
221 env.append('BPREFIXES', prefix)
224 def FindBaseHost(tool):
225 """ Find the base directory for host binaries (i.e. llvm/binutils) """
226 if env.has('BPREFIXES'):
227 for prefix in env.get('BPREFIXES'):
228 if os.path.exists(pathtools.join(prefix, 'bin',
229 tool + env.getone('EXEC_EXT'))):
232 base_pnacl = FindBasePNaCl()
233 if not pathtools.exists(pathtools.join(base_pnacl, 'bin',
234 tool + env.getone('EXEC_EXT'))):
235 Log.Fatal('Could not find PNaCl host directory for ' + tool)
239 # Mock out ReadConfig if running unittests. Settings are applied directly
240 # by DriverTestEnv rather than reading this configuration file.
241 if env.has('PNACL_RUNNING_UNITTESTS'):
243 driver_bin = env.getone('DRIVER_BIN')
244 driver_conf = pathtools.join(driver_bin, 'driver.conf')
245 fp = DriverOpen(driver_conf, 'r')
250 if line == '' or line.startswith('#'):
254 Log.Fatal("%s: Parse error, missing '=' on line %d",
255 pathtools.touser(driver_conf), linecount)
256 keyname = line[:sep].strip()
257 value = line[sep+1:].strip()
258 env.setraw(keyname, value)
262 def AddPrefix(prefix, varname):
263 values = env.get(varname)
264 return ' '.join([prefix + shell.escape(v) for v in values ])
266 ######################################################################
270 ######################################################################
272 DriverArgPatterns = [
273 ( '--pnacl-driver-verbose', "env.set('LOG_VERBOSE', '1')"),
274 ( ('-arch', '(.+)'), "SetArch($0)"),
275 ( '--pnacl-sb', "env.set('SANDBOXED', '1')"),
276 ( '--pnacl-use-emulator', "env.set('USE_EMULATOR', '1')"),
277 ( '--dry-run', "env.set('DRY_RUN', '1')"),
278 ( '--pnacl-arm-bias', "env.set('BIAS', 'ARM')"),
279 ( '--pnacl-mips-bias', "env.set('BIAS', 'MIPS32')"),
280 ( '--pnacl-i686-bias', "env.set('BIAS', 'X8632')"),
281 ( '--pnacl-x86_64-bias', "env.set('BIAS', 'X8664')"),
282 ( '--pnacl-bias=(.+)', "env.set('BIAS', FixArch($0))"),
283 ( '-save-temps', "env.set('SAVE_TEMPS', '1')"),
284 ( '-no-save-temps', "env.set('SAVE_TEMPS', '0')"),
285 ( ('-B', '(.*)'), AddHostBinarySearchPath),
288 DriverArgPatternsNotInherited = [
289 ( '--pnacl-driver-set-([^=]+)=(.*)', "env.set($0, $1)"),
290 ( '--pnacl-driver-append-([^=]+)=(.*)', "env.append($0, $1)"),
294 def ShouldExpandCommandFile(arg):
295 """ We may be given files with commandline arguments.
296 Read in the arguments so that they can be handled as usual. """
297 if arg.startswith('@'):
298 possible_file = pathtools.normalize(arg[1:])
299 return pathtools.isfile(possible_file)
304 def DoExpandCommandFile(argv, i):
306 fd = DriverOpen(pathtools.normalize(arg[1:]), 'r')
309 # Use shlex here to process the response file contents.
310 # This ensures that single and double quoted args are
311 # handled correctly. Since this file is very likely
312 # to contain paths with windows path seperators we can't
313 # use the normal shlex.parse() since we need to disable
314 # disable '\' (the default escape char).
316 lex = shlex.shlex(line, posix=True)
318 lex.whitespace_split = True
319 more_args += list(lex)
322 argv = argv[:i] + more_args + argv[i+1:]
328 driver_patternlist=DriverArgPatterns,
329 driver_patternlist_not_inherited=DriverArgPatternsNotInherited):
330 """Parse argv using the patterns in patternlist
331 Also apply the built-in DriverArgPatterns unless instructed otherwise.
332 This function must be called by all (real) drivers.
334 if driver_patternlist:
335 driver_args, argv = ParseArgsBase(argv, driver_patternlist)
337 # TODO(robertm): think about a less obscure mechanism to
338 # replace the inherited args feature
339 assert not env.get('INHERITED_DRIVER_ARGS')
340 env.append('INHERITED_DRIVER_ARGS', *driver_args)
342 _, argv = ParseArgsBase(argv, driver_patternlist_not_inherited)
344 _, unmatched = ParseArgsBase(argv, patternlist)
347 Log.Error('Unrecognized argument: ' + u)
348 Log.Fatal('unknown arguments')
351 def ParseArgsBase(argv, patternlist):
352 """ Parse argv using the patterns in patternlist
353 Returns: (matched, unmatched)
359 if ShouldExpandCommandFile(argv[i]):
360 argv = DoExpandCommandFile(argv, i)
361 num_matched, action, groups = MatchOne(argv, i, patternlist)
363 unmatched.append(argv[i])
366 matched += argv[i:i+num_matched]
367 if isinstance(action, str):
368 # Perform $N substitution
369 for g in xrange(0, len(groups)):
370 action = action.replace('$%d' % g, 'groups[%d]' % g)
372 if isinstance(action, str):
373 # NOTE: this is essentially an eval for python expressions
374 # which does rely on the current environment for unbound vars
375 # Log.Info('about to exec [%s]', str(action))
379 except Exception, err:
380 Log.Fatal('ParseArgs action [%s] failed with: %s', action, err)
382 return (matched, unmatched)
385 def MatchOne(argv, i, patternlist):
386 """Find a pattern which matches argv starting at position i"""
387 for (regex, action) in patternlist:
388 if isinstance(regex, str):
394 match = re.compile('^'+r+'$').match(argv[i+j])
397 matches.append(match)
401 groups = [ list(m.groups()) for m in matches ]
402 groups = reduce(lambda x,y: x+y, groups, [])
403 return (len(regex), action, groups)
406 def UnrecognizedOption(*args):
407 Log.Fatal("Unrecognized option: " + ' '.join(args) + "\n" +
408 "Use '--help' for more information.")
411 ######################################################################
413 # File Naming System (Temp files & Output files)
415 ######################################################################
417 def DefaultOutputName(filename, outtype):
418 # For pre-processor mode, just print to stdout.
419 if outtype in ('pp'): return '-'
421 base = pathtools.basename(filename)
422 base = RemoveExtension(base)
423 if outtype in ('po'): return base + '.o'
425 assert(outtype in filetype.ExtensionMap.values())
426 assert(not filetype.IsSourceType(outtype))
428 return base + '.' + outtype
430 def RemoveExtension(filename):
431 if filename.endswith('.opt.bc'):
432 return filename[0:-len('.opt.bc')]
434 name, ext = pathtools.splitext(filename)
441 cur, piece = pathtools.split(cur)
449 def CheckPathLength(filename, exit_on_failure=True):
450 '''Check that the length of the path is short enough for Windows.
452 On Windows, MAX_PATH is ~260 and applies to absolute paths, and to relative
453 paths and the absolute paths they expand to (except for specific uses of
454 some APIs; see link below). Most applications don't bother to support long
455 paths properly (including LLVM, GNU binutils, and ninja). If a path is too
456 long, ERROR_PATH_NOT_FOUND is returned, which isn't very useful or clear for
457 users. In addition the Chrome build has deep directory hierarchies with long
459 This function checks that the path is valid, so we can throw meaningful
462 http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
464 if not IsWindowsPython() and not env.has('PNACL_RUNNING_UNITTESTS'):
467 # First check the name as-is (it's usually a relative path)
468 if len(filename) > 255:
470 Log.Fatal('Path name %s is too long (%d characters)' %
471 (filename, len(filename)))
473 if os.path.isabs(filename):
476 # Don't assume that the underlying tools or windows APIs will normalize
477 # the path before using it. Conservatively count the length of CWD + filename
478 appended_name = os.path.join(os.getcwd(), filename)
479 if len(appended_name) > 255:
481 Log.Fatal('Path name %s (expanded from %s) is too long (%d characters)' %
482 (appended_name, filename, len(appended_name)))
486 # Generate a unique identifier for each input file.
487 # Start with the basename, and if that is not unique enough,
488 # add parent directories. Rinse, repeat.
489 class TempNameGen(object):
490 def __init__(self, inputs, output):
491 inputs = [ pathtools.abspath(i) for i in inputs ]
492 output = pathtools.abspath(output)
494 self.TempBase = output + '---linked'
495 self.OutputDir = pathtools.dirname(output)
497 # TODO(pdox): Figure out if there's a less confusing way
498 # to simplify the intermediate filename in this case.
499 #if len(inputs) == 1:
500 # # There's only one input file, don't bother adding the source name.
501 # TempMap[inputs[0]] = output + '---'
504 # Build the initial mapping
505 self.TempMap = dict()
507 if f.startswith('-'):
510 self.TempMap[f] = [1, path]
516 for (f, [n, path]) in self.TempMap.iteritems():
517 candidate = output + '---' + '_'.join(path[-n:]) + '---'
518 if candidate in ConflictMap:
519 Conflicts.add(ConflictMap[candidate])
522 ConflictMap[candidate] = f
524 if len(Conflicts) == 0:
529 n = self.TempMap[f][0]
530 if n+1 > len(self.TempMap[f][1]):
531 Log.Fatal('Unable to resolve naming conflicts')
532 self.TempMap[f][0] = n+1
536 for (f, [n, path]) in self.TempMap.iteritems():
537 candidate = output + '---' + '_'.join(path[-n:]) + '---'
538 NewMap[f] = candidate
539 self.TempMap = NewMap
542 def ValidatePathLength(self, temp, imtype):
543 # If the temp name is too long, just pick a random one instead.
544 if not CheckPathLength(temp, exit_on_failure=False):
545 # imtype is sometimes just an extension, and sometimes a compound
546 # extension (e.g. pre_opt.pexe). To keep name length shorter,
547 # only take the last extension
549 imtype = imtype[imtype.rfind('.') + 1:]
550 temp = pathtools.join(
552 str(random.randrange(100000, 1000000)) + '.' + imtype)
553 CheckPathLength(temp)
556 def TempNameForOutput(self, imtype):
557 temp = self.ValidatePathLength(self.TempBase + '.' + imtype, imtype)
561 def TempNameForInput(self, input, imtype):
562 fullpath = pathtools.abspath(input)
563 # If input is already a temporary name, just change the extension
564 if fullpath.startswith(self.TempBase):
565 temp = self.TempBase + '.' + imtype
568 temp = self.TempMap[fullpath] + '.' + imtype
570 temp = self.ValidatePathLength(temp, imtype)
574 # (Invoked from loader.py)
575 # If the driver is waiting on a background process in RunWithLog()
576 # and the user Ctrl-C's or kill's the driver, it may leave
577 # the child process (such as llc) running. To prevent this,
578 # the code below sets up a signal handler which issues a kill to
579 # the currently running child processes.
580 CleanupProcesses = []
581 def SetupSignalHandlers():
582 global CleanupProcesses
583 def signal_handler(unused_signum, unused_frame):
584 for p in CleanupProcesses:
587 except BaseException:
589 os.kill(os.getpid(), signal.SIGKILL)
591 if os.name == "posix":
592 signal.signal(signal.SIGINT, signal_handler)
593 signal.signal(signal.SIGHUP, signal_handler)
594 signal.signal(signal.SIGTERM, signal_handler)
597 def ArgsTooLongForWindows(args):
598 """ Detect when a command line might be too long for Windows. """
599 if not IsWindowsPython():
602 return len(' '.join(args)) > 8191
605 def ConvertArgsToFile(args):
606 fd, outfile = tempfile.mkstemp()
607 # Remember to delete this file afterwards.
608 TempFiles.add(outfile)
610 other_args = args[1:]
611 os.write(fd, ' '.join(other_args))
613 return [cmd, '@' + outfile]
616 # The redirect_stdout and redirect_stderr is only used a handful of times
618 # The stdin_contents feature is currently only used by:
619 # sel_universal invocations in the translator
624 redirect_stdout=None,
625 redirect_stderr=None):
626 """ Run: Run a command.
627 Returns: return_code, stdout, stderr
629 Run() is used to invoke "other" tools, e.g.
630 those NOT prefixed with "pnacl-"
632 stdout and stderr only contain meaningful data if
633 redirect_{stdout,stderr} == subprocess.PIPE
635 Run will terminate the program upon failure unless errexit == False
636 TODO(robertm): errexit == True has not been tested and needs more work
638 redirect_stdout and redirect_stderr are passed straight
640 stdin_contents is an optional string used as stdin
645 if isinstance(args, str):
646 args = shell.split(env.eval(args))
648 args = [pathtools.tosys(args[0])] + args[1:]
650 Log.Info('Running: ' + StringifyCommand(args))
652 Log.Info('--------------stdin: begin')
653 Log.Info(stdin_contents)
654 Log.Info('--------------stdin: end')
656 if env.getbool('DRY_RUN'):
657 if redirect_stderr or redirect_stdout:
658 # TODO(pdox): Prevent this from happening, so that
659 # dry-run is more useful.
660 Log.Fatal("Unhandled dry-run case.")
664 # If we have too long of a cmdline on windows, running it would fail.
665 # Attempt to use a file with the command line options instead in that case.
666 if ArgsTooLongForWindows(args):
667 actual_args = ConvertArgsToFile(args)
668 Log.Info('Wrote long commandline to file for Windows: ' +
669 StringifyCommand(actual_args))
674 redirect_stdin = None
676 redirect_stdin = subprocess.PIPE
678 p = subprocess.Popen(actual_args,
679 stdin=redirect_stdin,
680 stdout=redirect_stdout,
681 stderr=redirect_stderr)
682 result_stdout, result_stderr = p.communicate(input=stdin_contents)
684 msg = '%s\nCommand was: %s' % (str(e),
685 StringifyCommand(args, stdin_contents))
689 Log.Info('Return Code: ' + str(p.returncode))
691 if errexit and p.returncode != 0:
692 if redirect_stdout == subprocess.PIPE:
693 Log.Error('--------------stdout: begin')
694 Log.Error(result_stdout)
695 Log.Error('--------------stdout: end')
697 if redirect_stderr == subprocess.PIPE:
698 Log.Error('--------------stderr: begin')
699 Log.Error(result_stderr)
700 Log.Error('--------------stderr: end')
701 DriverExit(p.returncode)
703 return p.returncode, result_stdout, result_stderr
706 def IsWindowsPython():
707 return 'windows' in platform.system().lower()
709 def SetupCygwinLibs():
710 bindir = env.getone('DRIVER_BIN')
711 # Prepend the directory containing cygwin1.dll etc. to the PATH to ensure we
713 os.environ['PATH'] = os.pathsep.join(
714 [pathtools.tosys(bindir)] + os.environ['PATH'].split(os.pathsep))
717 def HelpNotAvailable():
718 return 'Help text not available'
720 def DriverMain(module, argv):
721 # TODO(robertm): this is ugly - try to get rid of this
722 if '--pnacl-driver-verbose' in argv:
723 Log.IncreaseVerbosity()
724 env.set('LOG_VERBOSE', '1')
726 # driver_path has the form: /foo/bar/pnacl_root/newlib/bin/pnacl-clang
727 driver_path = pathtools.abspath(pathtools.normalize(argv[0]))
728 driver_bin = pathtools.dirname(driver_path)
729 script_name = pathtools.basename(driver_path)
730 env.set('SCRIPT_NAME', script_name)
731 env.set('DRIVER_PATH', driver_path)
732 env.set('DRIVER_BIN', driver_bin)
734 Log.SetScriptName(script_name)
738 if IsWindowsPython():
745 if ('--help' in argv or
748 '--help-full' in argv):
749 help_func = getattr(module, 'get_help', None)
751 Log.Fatal(HelpNotAvailable())
752 helpstr = help_func(argv)
756 return module.main(argv)
759 def MaybeStripNonSFISuffix(s):
760 """Removes _NONSFI suffix if possible, otherwise |s| as is."""
761 return s[:-len('_NONSFI')] if s.endswith('_NONSFI') else s
766 env.set('ARCH', arch)
767 base_arch = MaybeStripNonSFISuffix(arch)
768 env.set('BASE_ARCH', base_arch)
769 env.setbool('NONSFI_NACL', arch != base_arch)
772 def GetArch(required = False):
773 arch = env.getone('ARCH')
777 if required and not arch:
778 Log.Fatal('Missing -arch!')
782 # Read an ELF file or an archive file to determine the machine type. If ARCH is
783 # already set, make sure the file has the same architecture. If ARCH is not
784 # set, set the ARCH to the file's architecture.
785 # Note that the SFI and NONSFI shares the same file format, so they will be
788 # Returns True if the file matches ARCH.
790 # Returns False if the file doesn't match ARCH. This only happens when
791 # must_match is False. If must_match is True, then a fatal error is generated
793 def ArchMerge(filename, must_match):
794 file_type = filetype.FileType(filename)
795 if file_type in ('o','so'):
796 elfheader = elftools.GetELFHeader(filename)
798 Log.Fatal("%s: Cannot read ELF header", filename)
799 new_arch = elfheader.arch
800 elif filetype.IsNativeArchive(filename):
801 new_arch = file_type[len('archive-'):]
803 Log.Fatal('%s: Unexpected file type in ArchMerge', filename)
805 existing_arch = GetArch()
806 if not existing_arch:
810 # The _NONSFI binary format is as same as the SFI's.
811 existing_arch = MaybeStripNonSFISuffix(existing_arch)
813 if new_arch != existing_arch:
815 msg = "%s: Incompatible object file (%s != %s)"
818 msg = "%s: Skipping incompatible object file (%s != %s)"
819 logfunc = Log.Warning
820 logfunc(msg, filename, new_arch, existing_arch)
823 # existing_arch and new_arch == existing_arch
826 def CheckTranslatorPrerequisites():
827 """ Assert that the scons artifacts for running the sandboxed translator
828 exist: sel_universal, and sel_ldr. """
829 if env.getbool('DRY_RUN'):
831 reqs = ['SEL_UNIVERSAL', 'SEL_LDR']
832 # Linux also requires the nacl bootstrap helper.
833 if GetBuildOS() == 'linux':
834 reqs.append('BOOTSTRAP_LDR')
836 needed_file = env.getone(var)
837 if not pathtools.exists(needed_file):
838 Log.Fatal('Could not find %s [%s]', var, needed_file)
840 class DriverChain(object):
841 """ The DriverChain class takes one or more input files,
842 an output file, and a sequence of steps. It executes
843 those steps, using intermediate files in between,
844 to generate the final outpu.
847 def __init__(self, input, output, namegen):
851 self.namegen = namegen
853 # "input" can be a list of files or a single file.
854 # If we're compiling for a single file, then we use
855 # TempNameForInput. If there are multiple files
856 # (e.g. linking), then we use TempNameForOutput.
857 if isinstance(input, str):
858 self.use_names_for_input = True
859 CheckPathLength(input)
861 self.use_names_for_input = False
863 CheckPathLength(path)
864 CheckPathLength(output)
866 def add(self, callback, output_type, **extra):
867 step = (callback, output_type, extra)
868 self.steps.append(step)
871 step_input = self.input
872 for (i, (callback, output_type, extra)) in enumerate(self.steps):
873 if i == len(self.steps)-1:
875 step_output = self.output
878 if self.use_names_for_input:
879 step_output = self.namegen.TempNameForInput(self.input, output_type)
881 step_output = self.namegen.TempNameForOutput(output_type)
882 callback(step_input, step_output, **extra)
883 step_input = step_output