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.
6 # IMPORTANT NOTE: If you make local mods to this file, you must run:
7 # % pnacl/build.sh driver
8 # in order for them to take effect in the scons build. This command
9 # updates the copy in the toolchain/ tree.
25 # filetype needs to be imported here because pnacl-driver injects calls to
26 # filetype.ForceFileType into argument parse actions.
27 # TODO(dschuff): That's ugly. Find a better way.
31 from driver_env import env
32 # TODO: import driver_log and change these references from 'foo' to
33 # 'driver_log.foo', or split driver_log further
34 from driver_log import Log, DriverOpen, DriverClose, StringifyCommand, TempFiles, DriverExit, FixArch
35 from shelltools import shell
37 def ParseError(s, leftpos, rightpos, msg):
38 Log.Error("Parse Error: %s", msg)
40 Log.Error(' ' + (' '*leftpos) + ('^'*(rightpos - leftpos + 1)))
44 # Run a command with extra environment settings
45 def RunWithEnv(cmd, **kwargs):
53 def SetExecutableMode(path):
54 if os.name == "posix":
55 realpath = pathtools.tosys(path)
56 # os.umask gets and sets at the same time.
57 # There's no way to get it without setting it.
60 os.chmod(realpath, 0755 & ~umask)
63 def FilterOutArchArgs(args):
64 while '-arch' in args:
65 i = args.index('-arch')
66 args = args[:i] + args[i+2:]
69 # Parse and validate the target triple and return the architecture.
70 # We don't attempt to recognize all possible targets here, just the ones we
72 def ParseTriple(triple):
73 tokens = triple.split('-')
78 # The machine/vendor field could be present or not.
79 if os != 'nacl' and len(tokens) >= 3:
81 # Just check that the os is nacl.
85 Log.Fatal('machine/os ' + '-'.join(tokens[1:]) + ' not supported.')
88 def RunDriver(invocation, args, suppress_inherited_arch_args=False):
90 RunDriver() is used to invoke "driver" tools, e.g.
91 those prefixed with "pnacl-"
93 It automatically appends some additional flags to the invocation
94 which were inherited from the current invocation.
95 Those flags were preserved by ParseArgs
98 if isinstance(args, str):
99 args = shell.split(env.eval(args))
101 module_name = 'pnacl-%s' % invocation
102 script = env.eval('${DRIVER_BIN}/%s' % module_name)
103 script = shell.unescape(script)
105 inherited_driver_args = env.get('INHERITED_DRIVER_ARGS')
106 if suppress_inherited_arch_args:
107 inherited_driver_args = FilterOutArchArgs(inherited_driver_args)
109 script = pathtools.tosys(script)
110 cmd = [script] + args + inherited_driver_args
111 Log.Info('Driver invocation: %s', repr(cmd))
113 module = __import__(module_name)
114 # Save the environment, reset the environment, run
115 # the driver module, and then restore the environment.
118 DriverMain(module, cmd)
122 """ Memoize a function with no arguments """
128 newf.__name__ = f.__name__
135 name = platform.system().lower()
136 if name.startswith('cygwin_nt') or 'windows' in name:
138 if name not in ('linux', 'darwin', 'windows'):
139 Log.Fatal("Unsupported platform '%s'", name)
145 m = platform.machine()
151 if m not in ('i386', 'i686', 'x86_64'):
152 Log.Fatal("Unsupported architecture '%s'", m)
155 # Crawl backwards, starting from the directory containing this script,
156 # until we find a directory satisfying a filter function.
157 def FindBaseDir(function):
159 cur = env.getone('DRIVER_BIN')
160 while not function(cur) and Depth < 16:
161 cur = pathtools.dirname(cur)
170 """ Find native_client/ directory """
171 dir = FindBaseDir(lambda cur: pathtools.basename(cur) == 'native_client')
173 Log.Fatal("Unable to find 'native_client' directory")
174 return shell.escape(dir)
178 def FindBaseToolchain():
179 """ Find toolchain/ directory """
180 dir = FindBaseDir(lambda cur: pathtools.basename(cur) == 'toolchain')
182 Log.Fatal("Unable to find 'toolchain' directory")
183 return shell.escape(dir)
188 """ Find the base directory of the PNaCl toolchain """
189 # The <base> directory is one level up from the <base>/bin:
190 bindir = env.getone('DRIVER_BIN')
191 basedir = pathtools.dirname(bindir)
192 return shell.escape(basedir)
194 def AddHostBinarySearchPath(prefix):
195 """ Add a path to the list searched for host binaries. """
196 prefix = pathtools.normalize(prefix)
197 if pathtools.isdir(prefix) and not prefix.endswith('/'):
200 env.append('BPREFIXES', prefix)
203 def FindBaseHost(tool):
204 """ Find the base directory for host binaries (i.e. llvm/binutils) """
205 if env.has('BPREFIXES'):
206 for prefix in env.get('BPREFIXES'):
207 if os.path.exists(pathtools.join(prefix, 'bin',
208 tool + env.getone('EXEC_EXT'))):
211 base_pnacl = FindBasePNaCl()
212 base_host = pathtools.join(base_pnacl, 'host_' + env.getone('HOST_ARCH'))
213 if not pathtools.exists(pathtools.join(base_host, 'bin',
214 tool + env.getone('EXEC_EXT'))):
215 Log.Fatal('Could not find PNaCl host directory for ' + tool)
219 # Mock out ReadConfig if running unittests. Settings are applied directly
220 # by DriverTestEnv rather than reading this configuration file.
221 if env.has('PNACL_RUNNING_UNITTESTS'):
223 driver_bin = env.getone('DRIVER_BIN')
224 driver_conf = pathtools.join(driver_bin, 'driver.conf')
225 fp = DriverOpen(driver_conf, 'r')
230 if line == '' or line.startswith('#'):
234 Log.Fatal("%s: Parse error, missing '=' on line %d",
235 pathtools.touser(driver_conf), linecount)
236 keyname = line[:sep].strip()
237 value = line[sep+1:].strip()
238 env.setraw(keyname, value)
242 def AddPrefix(prefix, varname):
243 values = env.get(varname)
244 return ' '.join([prefix + shell.escape(v) for v in values ])
246 ######################################################################
250 ######################################################################
252 DriverArgPatterns = [
253 ( '--pnacl-driver-verbose', "env.set('LOG_VERBOSE', '1')"),
254 ( ('-arch', '(.+)'), "SetArch($0)"),
255 ( '--pnacl-sb', "env.set('SANDBOXED', '1')"),
256 ( '--pnacl-use-emulator', "env.set('USE_EMULATOR', '1')"),
257 ( '--dry-run', "env.set('DRY_RUN', '1')"),
258 ( '--pnacl-arm-bias', "env.set('BIAS', 'ARM')"),
259 ( '--pnacl-mips-bias', "env.set('BIAS', 'MIPS32')"),
260 ( '--pnacl-i686-bias', "env.set('BIAS', 'X8632')"),
261 ( '--pnacl-x86_64-bias', "env.set('BIAS', 'X8664')"),
262 ( '--pnacl-bias=(.+)', "env.set('BIAS', FixArch($0))"),
263 ( '--pnacl-default-command-line', "env.set('USE_DEFAULT_CMD_LINE', '1')"),
264 ( '-save-temps', "env.set('SAVE_TEMPS', '1')"),
265 ( '-no-save-temps', "env.set('SAVE_TEMPS', '0')"),
266 ( ('-B', '(.*)'), AddHostBinarySearchPath),
269 DriverArgPatternsNotInherited = [
270 ( '--pnacl-driver-set-([^=]+)=(.*)', "env.set($0, $1)"),
271 ( '--pnacl-driver-append-([^=]+)=(.*)', "env.append($0, $1)"),
275 def ShouldExpandCommandFile(arg):
276 """ We may be given files with commandline arguments.
277 Read in the arguments so that they can be handled as usual. """
278 if arg.startswith('@'):
279 possible_file = pathtools.normalize(arg[1:])
280 return pathtools.isfile(possible_file)
285 def DoExpandCommandFile(argv, i):
287 fd = DriverOpen(pathtools.normalize(arg[1:]), 'r')
290 # Use shlex here to process the response file contents.
291 # This ensures that single and double quoted args are
292 # handled correctly. Since this file is very likely
293 # to contain paths with windows path seperators we can't
294 # use the normal shlex.parse() since we need to disable
295 # disable '\' (the default escape char).
297 lex = shlex.shlex(line, posix=True)
299 lex.whitespace_split = True
300 more_args += list(lex)
303 argv = argv[:i] + more_args + argv[i+1:]
309 driver_patternlist=DriverArgPatterns,
310 driver_patternlist_not_inherited=DriverArgPatternsNotInherited):
311 """Parse argv using the patterns in patternlist
312 Also apply the built-in DriverArgPatterns unless instructed otherwise.
313 This function must be called by all (real) drivers.
315 if driver_patternlist:
316 driver_args, argv = ParseArgsBase(argv, driver_patternlist)
318 # TODO(robertm): think about a less obscure mechanism to
319 # replace the inherited args feature
320 assert not env.get('INHERITED_DRIVER_ARGS')
321 env.append('INHERITED_DRIVER_ARGS', *driver_args)
323 _, argv = ParseArgsBase(argv, driver_patternlist_not_inherited)
325 _, unmatched = ParseArgsBase(argv, patternlist)
328 Log.Error('Unrecognized argument: ' + u)
329 Log.Fatal('unknown arguments')
332 def ParseArgsBase(argv, patternlist):
333 """ Parse argv using the patterns in patternlist
334 Returns: (matched, unmatched)
340 if ShouldExpandCommandFile(argv[i]):
341 argv = DoExpandCommandFile(argv, i)
342 num_matched, action, groups = MatchOne(argv, i, patternlist)
344 unmatched.append(argv[i])
347 matched += argv[i:i+num_matched]
348 if isinstance(action, str):
349 # Perform $N substitution
350 for g in xrange(0, len(groups)):
351 action = action.replace('$%d' % g, 'groups[%d]' % g)
353 if isinstance(action, str):
354 # NOTE: this is essentially an eval for python expressions
355 # which does rely on the current environment for unbound vars
356 # Log.Info('about to exec [%s]', str(action))
360 except Exception, err:
361 Log.Fatal('ParseArgs action [%s] failed with: %s', action, err)
363 return (matched, unmatched)
366 def MatchOne(argv, i, patternlist):
367 """Find a pattern which matches argv starting at position i"""
368 for (regex, action) in patternlist:
369 if isinstance(regex, str):
375 match = re.compile('^'+r+'$').match(argv[i+j])
378 matches.append(match)
382 groups = [ list(m.groups()) for m in matches ]
383 groups = reduce(lambda x,y: x+y, groups, [])
384 return (len(regex), action, groups)
387 def UnrecognizedOption(*args):
388 Log.Fatal("Unrecognized option: " + ' '.join(args) + "\n" +
389 "Use '--help' for more information.")
392 ######################################################################
394 # File Naming System (Temp files & Output files)
396 ######################################################################
398 def DefaultOutputName(filename, outtype):
399 # For pre-processor mode, just print to stdout.
400 if outtype in ('pp'): return '-'
402 base = pathtools.basename(filename)
403 base = RemoveExtension(base)
404 if outtype in ('po'): return base + '.o'
406 assert(outtype in filetype.ExtensionMap.values())
407 assert(not filetype.IsSourceType(outtype))
409 return base + '.' + outtype
411 def RemoveExtension(filename):
412 if filename.endswith('.opt.bc'):
413 return filename[0:-len('.opt.bc')]
415 name, ext = pathtools.splitext(filename)
422 cur, piece = pathtools.split(cur)
429 # Generate a unique identifier for each input file.
430 # Start with the basename, and if that is not unique enough,
431 # add parent directories. Rinse, repeat.
432 class TempNameGen(object):
433 def __init__(self, inputs, output):
434 inputs = [ pathtools.abspath(i) for i in inputs ]
435 output = pathtools.abspath(output)
437 self.TempBase = output + '---linked'
439 # TODO(pdox): Figure out if there's a less confusing way
440 # to simplify the intermediate filename in this case.
441 #if len(inputs) == 1:
442 # # There's only one input file, don't bother adding the source name.
443 # TempMap[inputs[0]] = output + '---'
446 # Build the initial mapping
447 self.TempMap = dict()
449 if f.startswith('-'):
452 self.TempMap[f] = [1, path]
458 for (f, [n, path]) in self.TempMap.iteritems():
459 candidate = output + '---' + '_'.join(path[-n:]) + '---'
460 if candidate in ConflictMap:
461 Conflicts.add(ConflictMap[candidate])
464 ConflictMap[candidate] = f
466 if len(Conflicts) == 0:
471 n = self.TempMap[f][0]
472 if n+1 > len(self.TempMap[f][1]):
473 Log.Fatal('Unable to resolve naming conflicts')
474 self.TempMap[f][0] = n+1
478 for (f, [n, path]) in self.TempMap.iteritems():
479 candidate = output + '---' + '_'.join(path[-n:]) + '---'
480 NewMap[f] = candidate
481 self.TempMap = NewMap
484 def TempNameForOutput(self, imtype):
485 temp = self.TempBase + '.' + imtype
486 if not env.getbool('SAVE_TEMPS'):
490 def TempNameForInput(self, input, imtype):
491 fullpath = pathtools.abspath(input)
492 # If input is already a temporary name, just change the extension
493 if fullpath.startswith(self.TempBase):
494 temp = self.TempBase + '.' + imtype
497 temp = self.TempMap[fullpath] + '.' + imtype
499 if not env.getbool('SAVE_TEMPS'):
503 # (Invoked from loader.py)
504 # If the driver is waiting on a background process in RunWithLog()
505 # and the user Ctrl-C's or kill's the driver, it may leave
506 # the child process (such as llc) running. To prevent this,
507 # the code below sets up a signal handler which issues a kill to
508 # the currently running child processes.
509 CleanupProcesses = []
510 def SetupSignalHandlers():
511 global CleanupProcesses
512 def signal_handler(unused_signum, unused_frame):
513 for p in CleanupProcesses:
516 except BaseException:
518 os.kill(os.getpid(), signal.SIGKILL)
520 if os.name == "posix":
521 signal.signal(signal.SIGINT, signal_handler)
522 signal.signal(signal.SIGHUP, signal_handler)
523 signal.signal(signal.SIGTERM, signal_handler)
526 def ArgsTooLongForWindows(args):
527 """ Detect when a command line might be too long for Windows. """
528 if not IsWindowsPython():
531 return len(' '.join(args)) > 8191
534 def ConvertArgsToFile(args):
535 fd, outfile = tempfile.mkstemp()
536 # Remember to delete this file afterwards.
537 TempFiles.add(outfile)
539 other_args = args[1:]
540 os.write(fd, ' '.join(other_args))
542 return [cmd, '@' + outfile]
545 # The redirect_stdout and redirect_stderr is only used a handful of times
547 # The stdin_contents feature is currently only used by:
548 # sel_universal invocations in the translator
553 redirect_stdout=None,
554 redirect_stderr=None):
555 """ Run: Run a command.
556 Returns: return_code, stdout, stderr
558 Run() is used to invoke "other" tools, e.g.
559 those NOT prefixed with "pnacl-"
561 stdout and stderr only contain meaningful data if
562 redirect_{stdout,stderr} == subprocess.PIPE
564 Run will terminate the program upon failure unless errexit == False
565 TODO(robertm): errexit == True has not been tested and needs more work
567 redirect_stdout and redirect_stderr are passed straight
569 stdin_contents is an optional string used as stdin
574 if isinstance(args, str):
575 args = shell.split(env.eval(args))
577 args = [pathtools.tosys(args[0])] + args[1:]
579 Log.Info('Running: ' + StringifyCommand(args))
581 Log.Info('--------------stdin: begin')
582 Log.Info(stdin_contents)
583 Log.Info('--------------stdin: end')
585 if env.getbool('DRY_RUN'):
586 if redirect_stderr or redirect_stdout:
587 # TODO(pdox): Prevent this from happening, so that
588 # dry-run is more useful.
589 Log.Fatal("Unhandled dry-run case.")
593 # If we have too long of a cmdline on windows, running it would fail.
594 # Attempt to use a file with the command line options instead in that case.
595 if ArgsTooLongForWindows(args):
596 actual_args = ConvertArgsToFile(args)
597 Log.Info('Wrote long commandline to file for Windows: ' +
598 StringifyCommand(actual_args))
603 redirect_stdin = None
605 redirect_stdin = subprocess.PIPE
607 p = subprocess.Popen(actual_args,
608 stdin=redirect_stdin,
609 stdout=redirect_stdout,
610 stderr=redirect_stderr)
611 result_stdout, result_stderr = p.communicate(input=stdin_contents)
613 msg = '%s\nCommand was: %s' % (str(e),
614 StringifyCommand(args, stdin_contents))
618 Log.Info('Return Code: ' + str(p.returncode))
620 if errexit and p.returncode != 0:
621 if redirect_stdout == subprocess.PIPE:
622 Log.Error('--------------stdout: begin')
623 Log.Error(result_stdout)
624 Log.Error('--------------stdout: end')
626 if redirect_stderr == subprocess.PIPE:
627 Log.Error('--------------stderr: begin')
628 Log.Error(result_stderr)
629 Log.Error('--------------stderr: end')
630 DriverExit(p.returncode)
632 return p.returncode, result_stdout, result_stderr
635 def IsWindowsPython():
636 return 'windows' in platform.system().lower()
638 def SetupCygwinLibs():
639 bindir = env.getone('DRIVER_BIN')
640 # Prepend the directory containing cygwin1.dll etc. to the PATH to ensure we
642 os.environ['PATH'] = os.pathsep.join(
643 [pathtools.tosys(bindir)] + os.environ['PATH'].split(os.pathsep))
646 def HelpNotAvailable():
647 return 'Help text not available'
649 def DriverMain(module, argv):
650 # TODO(robertm): this is ugly - try to get rid of this
651 if '--pnacl-driver-verbose' in argv:
652 Log.IncreaseVerbosity()
653 env.set('LOG_VERBOSE', '1')
655 # driver_path has the form: /foo/bar/pnacl_root/newlib/bin/pnacl-clang
656 driver_path = pathtools.abspath(pathtools.normalize(argv[0]))
657 driver_bin = pathtools.dirname(driver_path)
658 script_name = pathtools.basename(driver_path)
659 env.set('SCRIPT_NAME', script_name)
660 env.set('DRIVER_PATH', driver_path)
661 env.set('DRIVER_BIN', driver_bin)
663 Log.SetScriptName(script_name)
667 if IsWindowsPython():
674 if ('--help' in argv or
677 '--help-full' in argv):
678 help_func = getattr(module, 'get_help', None)
680 Log.Fatal(HelpNotAvailable())
681 helpstr = help_func(argv)
685 return module.main(argv)
689 env.set('ARCH', FixArch(arch))
691 def GetArch(required = False):
692 arch = env.getone('ARCH')
696 if required and not arch:
697 Log.Fatal('Missing -arch!')
701 # Read an ELF file to determine the machine type. If ARCH is already set,
702 # make sure the file has the same architecture. If ARCH is not set,
703 # set the ARCH to the file's architecture.
705 # Returns True if the file matches ARCH.
707 # Returns False if the file doesn't match ARCH. This only happens when
708 # must_match is False. If must_match is True, then a fatal error is generated
710 def ArchMerge(filename, must_match):
711 file_type = filetype.FileType(filename)
712 if file_type in ('o','so'):
713 elfheader = elftools.GetELFHeader(filename)
715 Log.Fatal("%s: Cannot read ELF header", filename)
716 new_arch = elfheader.arch
717 elif filetype.IsNativeArchive(filename):
718 new_arch = file_type[len('archive-'):]
720 Log.Fatal('%s: Unexpected file type in ArchMerge', filename)
722 existing_arch = GetArch()
724 if not existing_arch:
727 elif new_arch != existing_arch:
729 msg = "%s: Incompatible object file (%s != %s)"
732 msg = "%s: Skipping incompatible object file (%s != %s)"
733 logfunc = Log.Warning
734 logfunc(msg, filename, new_arch, existing_arch)
736 else: # existing_arch and new_arch == existing_arch
739 def CheckTranslatorPrerequisites():
740 """ Assert that the scons artifacts for running the sandboxed translator
741 exist: sel_universal, and sel_ldr. """
742 if env.getbool('DRY_RUN'):
744 reqs = ['SEL_UNIVERSAL', 'SEL_LDR']
745 # Linux also requires the nacl bootstrap helper.
746 if GetBuildOS() == 'linux':
747 reqs.append('BOOTSTRAP_LDR')
749 needed_file = env.getone(var)
750 if not pathtools.exists(needed_file):
751 Log.Fatal('Could not find %s [%s]', var, needed_file)
753 class DriverChain(object):
754 """ The DriverChain class takes one or more input files,
755 an output file, and a sequence of steps. It executes
756 those steps, using intermediate files in between,
757 to generate the final outpu.
760 def __init__(self, input, output, namegen):
764 self.namegen = namegen
766 # "input" can be a list of files or a single file.
767 # If we're compiling for a single file, then we use
768 # TempNameForInput. If there are multiple files
769 # (e.g. linking), then we use TempNameForOutput.
770 self.use_names_for_input = isinstance(input, str)
772 def add(self, callback, output_type, **extra):
773 step = (callback, output_type, extra)
774 self.steps.append(step)
777 step_input = self.input
778 for (i, (callback, output_type, extra)) in enumerate(self.steps):
779 if i == len(self.steps)-1:
781 step_output = self.output
784 if self.use_names_for_input:
785 step_output = self.namegen.TempNameForInput(self.input, output_type)
787 step_output = self.namegen.TempNameForOutput(output_type)
788 callback(step_input, step_output, **extra)
789 step_input = step_output