Bug 1267454 - Add a parameter to find_program and check_prog to allow searching the given paths instead of $PATH. draft
authorChris Manchester <cmanchester@mozilla.com>
Thu, 05 May 2016 16:02:03 -0700
changeset 363980 1c55a98496aa816299cf8f56eb884f26cb948c92
parent 363790 29662e28a9c93ac67ee0b8ddfb65a9f29bbf73f5
child 363981 21af16108f74b7acd9b78ee42f7cf7ba2fb89a15
push id17355
push usercmanchester@mozilla.com
push dateThu, 05 May 2016 23:02:20 +0000
bugs1267454
milestone49.0a1
Bug 1267454 - Add a parameter to find_program and check_prog to allow searching the given paths instead of $PATH. MozReview-Commit-ID: F3lke9Q5rRR
build/moz.configure/checks.configure
build/moz.configure/util.configure
python/mozbuild/mozbuild/test/configure/common.py
python/mozbuild/mozbuild/test/configure/test_checks_configure.py
--- a/build/moz.configure/checks.configure
+++ b/build/moz.configure/checks.configure
@@ -76,26 +76,31 @@ def checking(what, callback=None):
 # - `what` is a human readable description of what is being looked for. It
 #   defaults to the lowercase version of `var`.
 # - `input` is a string reference to an existing option or a reference to a
 #   @depends function resolving to explicit input for the program check.
 #   The default is to create an option for the environment variable `var`.
 #   This argument allows to use a different kind of option (possibly using a
 #   configure flag), or doing some pre-processing with a @depends function.
 # - `allow_missing` indicates whether not finding the program is an error.
+# - `paths` is a list of paths or @depends function returning a list of paths
+#   that will cause the given path(s) to be searched rather than $PATH. Input
+#   paths may either be individual paths or delimited by os.pathsep, to allow
+#   passing $PATH (for example) as an element.
 #
 # The simplest form is:
 #   check_prog('PROG', ('a', 'b'))
 # This will look for 'a' or 'b' in $PATH, and set_config PROG to the one
 # it can find. If PROG is already set from the environment or command line,
 # use that value instead.
 @template
 @imports(_from='mozbuild.shellutil', _import='quote')
 @imports(_from='mozbuild.configure', _import='DependsFunction')
-def check_prog(var, progs, what=None, input=None, allow_missing=False):
+def check_prog(var, progs, what=None, input=None, allow_missing=False,
+               paths=None):
     if input:
         # Wrap input with type checking and normalization.
         @depends(input)
         def input(value):
             if not value:
                 return
             if isinstance(value, str):
                 return (value,)
@@ -104,32 +109,34 @@ def check_prog(var, progs, what=None, in
             configure_error('input must resolve to a tuple or a list with a '
                             'single element, or a string')
     else:
         option(env=var, nargs=1,
                help='Path to %s' % (what or 'the %s program' % var.lower()))
         input = var
     what = what or var.lower()
 
+    # Trick to make a @depends function out of an immediate value.
     if not isinstance(progs, DependsFunction):
-        # Trick to make a @depends function out of an immediate value.
         progs = depends('--help')(lambda h: progs)
+    if not isinstance(paths, DependsFunction):
+        paths = depends('--help')(lambda h: paths)
 
-    @depends_if(input, progs)
+    @depends_if(input, progs, paths)
     @checking('for %s' % what, lambda x: quote(x) if x else 'not found')
-    def check(value, progs):
+    def check(value, progs, paths):
         if progs is None:
             progs = ()
 
         if not isinstance(progs, (tuple, list)):
             configure_error('progs must resolve to a list or tuple!')
 
         for prog in value or progs:
             log.debug('%s: Trying %s', var.lower(), quote(prog))
-            result = find_program(prog)
+            result = find_program(prog, paths)
             if result:
                 return result
 
         if not allow_missing or value:
             raise FatalCheckError('Cannot find %s' % what)
 
     @depends_if(check, progs)
     def normalized_for_config(value, progs):
--- a/build/moz.configure/util.configure
+++ b/build/moz.configure/util.configure
@@ -25,24 +25,35 @@ def is_absolute_or_relative(path):
         return True
     return os.sep in path
 
 
 @imports(_import='mozpack.path', _as='mozpath')
 def normsep(path):
     return mozpath.normsep(path)
 
-
+# Locates the given program using which, or returns the given path if it
+# exists.
+# The `paths` parameter may be passed to search the given paths instead of
+# $PATH.
 @imports(_from='which', _import='which')
 @imports(_from='which', _import='WhichError')
-def find_program(file):
+@imports('itertools')
+@imports(_from='os', _import='pathsep')
+def find_program(file, paths=None):
     if is_absolute_or_relative(file):
         return os.path.abspath(file) if os.path.isfile(file) else None
     try:
-        return normsep(which(file))
+        if paths:
+            if not isinstance(paths, list):
+                die("Paths provided to find_program must be a list of strings, "
+                    "not %r" % paths)
+            paths = list(itertools.chain(
+                *(p.split(pathsep) for p in paths if p)))
+        return normsep(which(file, path=paths))
     except WhichError:
         return None
 
 
 def unique_list(l):
     result = []
     for i in l:
         if l not in result:
--- a/python/mozbuild/mozbuild/test/configure/common.py
+++ b/python/mozbuild/mozbuild/test/configure/common.py
@@ -104,21 +104,21 @@ class ConfigureTestSandbox(ConfigureSand
                 CalledProcessError=subprocess.CalledProcessError,
                 check_output=self.check_output,
                 PIPE=subprocess.PIPE,
                 Popen=self.Popen,
             )
 
         return super(ConfigureTestSandbox, self)._get_one_import(what)
 
-    def which(self, command):
-        for parent in self._search_path:
-            path = mozpath.join(parent, command)
-            if self.OS.path.exists(path):
-                return path
+    def which(self, command, path=None):
+        for parent in (path or self._search_path):
+            candidate = mozpath.join(parent, command)
+            if self.OS.path.exists(candidate):
+                return candidate
         raise WhichError()
 
     def Popen(self, args, stdin=None, stdout=None, stderr=None, **kargs):
         try:
             program = self.which(args[0])
         except WhichError:
             raise OSError(errno.ENOENT, 'File not found')
 
--- a/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
@@ -121,28 +121,30 @@ class TestChecksConfigure(unittest.TestC
 
         out.truncate(0)
         foo(['foo', 'bar'])
         self.assertEqual(out.getvalue(), 'checking for a thing... foo bar\n')
 
     KNOWN_A = mozpath.abspath('/usr/bin/known-a')
     KNOWN_B = mozpath.abspath('/usr/local/bin/known-b')
     KNOWN_C = mozpath.abspath('/home/user/bin/known c')
+    OTHER_A = mozpath.abspath('/lib/other/known-a')
 
     def get_result(self, command='', args=[], environ={},
                    prog='/bin/configure'):
         config = {}
         out = StringIO()
         paths = {
             self.KNOWN_A: None,
             self.KNOWN_B: None,
             self.KNOWN_C: None,
         }
         environ = dict(environ)
         environ['PATH'] = os.pathsep.join(os.path.dirname(p) for p in paths)
+        paths[self.OTHER_A] = None
         sandbox = ConfigureTestSandbox(paths, config, environ, [prog] + args,
                                        out, out)
         base_dir = os.path.join(topsrcdir, 'build', 'moz.configure')
         sandbox.include_file(os.path.join(base_dir, 'util.configure'))
         sandbox.include_file(os.path.join(base_dir, 'checks.configure'))
 
         status = 0
         try:
@@ -391,11 +393,59 @@ class TestChecksConfigure(unittest.TestC
                 'foo = depends("--help")(lambda h: {"a": "b"})\n'
                 'check_prog("FOO", ("known-a",), input=foo)'
             )
 
         self.assertEqual(e.exception.message,
                          'input must resolve to a tuple or a list with a '
                          'single element, or a string')
 
+    def test_check_prog_with_path(self):
+        config, out, status = self.get_result('check_prog("A", ("known-a",), paths=["/some/path"])')
+        self.assertEqual(status, 1)
+        self.assertEqual(config, {})
+        self.assertEqual(out, textwrap.dedent('''\
+            checking for a... not found
+            DEBUG: a: Trying known-a
+            ERROR: Cannot find a
+        '''))
+
+        config, out, status = self.get_result('check_prog("A", ("known-a",), paths=["%s"])' %
+                                              os.path.dirname(self.OTHER_A))
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {'A': self.OTHER_A})
+        self.assertEqual(out, textwrap.dedent('''\
+            checking for a... %s
+        ''' % self.OTHER_A))
+
+        dirs = map(mozpath.dirname, (self.OTHER_A, self.KNOWN_A))
+        config, out, status = self.get_result(textwrap.dedent('''\
+            check_prog("A", ("known-a",), paths=["%s"])
+        ''' % os.pathsep.join(dirs)))
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {'A': self.OTHER_A})
+        self.assertEqual(out, textwrap.dedent('''\
+            checking for a... %s
+        ''' % self.OTHER_A))
+
+        dirs = map(mozpath.dirname, (self.KNOWN_A, self.KNOWN_B))
+        config, out, status = self.get_result(textwrap.dedent('''\
+            check_prog("A", ("known-a",), paths=["%s", "%s"])
+        ''' % (os.pathsep.join(dirs), self.OTHER_A)))
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {'A': self.KNOWN_A})
+        self.assertEqual(out, textwrap.dedent('''\
+            checking for a... %s
+        ''' % self.KNOWN_A))
+
+        config, out, status = self.get_result('check_prog("A", ("known-a",), paths="%s")' %
+                                              os.path.dirname(self.OTHER_A))
+
+        self.assertEqual(status, 1)
+        self.assertEqual(config, {})
+        self.assertEqual(out, textwrap.dedent('''\
+            checking for a... 
+            DEBUG: a: Trying known-a
+            ERROR: Paths provided to find_program must be a list of strings, not %r
+        ''' % mozpath.dirname(self.OTHER_A)))
 
 if __name__ == '__main__':
     main()