1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5# This file contains code for populating the virtualenv environment for
6# Mozilla's build system. It is typically called as part of configure.
7
8from __future__ import absolute_import, print_function, unicode_literals
9
10import os
11import shutil
12import subprocess
13import sys
14
15
16IS_NATIVE_WIN = (sys.platform == 'win32' and os.sep == '\\')
17IS_CYGWIN = (sys.platform == 'cygwin')
18
19PY2 = sys.version_info[0] == 2
20PY3 = sys.version_info[0] == 3
21
22UPGRADE_WINDOWS = '''
23Please upgrade to the latest MozillaBuild development environment. See
24https://developer.mozilla.org/en-US/docs/Developer_Guide/Build_Instructions/Windows_Prerequisites
25'''.lstrip()
26
27UPGRADE_OTHER = '''
28Run |mach bootstrap| to ensure your system is up to date.
29
30If you still receive this error, your shell environment is likely detecting
31another Python version. Ensure a modern Python can be found in the paths
32defined by the $PATH environment variable and try again.
33'''.lstrip()
34
35here = os.path.abspath(os.path.dirname(__file__))
36
37
38class VirtualenvManager(object):
39    """Contains logic for managing virtualenvs for building the tree."""
40
41    def __init__(self, topsrcdir, topobjdir, virtualenv_path, log_handle,
42                 manifest_path):
43        """Create a new manager.
44
45        Each manager is associated with a source directory, a path where you
46        want the virtualenv to be created, and a handle to write output to.
47        """
48        # __PYVENV_LAUNCHER__ confuses pip, telling it to use the system
49        # python interpreter rather than the local virtual environment interpreter.
50        # See https://bugzilla.mozilla.org/show_bug.cgi?id=1607470
51        os.environ.pop('__PYVENV_LAUNCHER__', None)
52
53        assert os.path.isabs(
54            manifest_path), "manifest_path must be an absolute path: %s" % (manifest_path)
55        self.topsrcdir = topsrcdir
56        self.topobjdir = topobjdir
57        self.virtualenv_root = virtualenv_path
58
59        # Record the Python executable that was used to create the Virtualenv
60        # so we can check this against sys.executable when verifying the
61        # integrity of the virtualenv.
62        self.exe_info_path = os.path.join(self.virtualenv_root,
63                                          'python_exe.txt')
64
65        self.log_handle = log_handle
66        self.manifest_path = manifest_path
67
68    @property
69    def virtualenv_script_path(self):
70        """Path to virtualenv's own populator script."""
71        return os.path.join(self.topsrcdir, 'third_party', 'python',
72                            'virtualenv', 'virtualenv.py')
73
74    @property
75    def bin_path(self):
76        # virtualenv.py provides a similar API via path_locations(). However,
77        # we have a bit of a chicken-and-egg problem and can't reliably
78        # import virtualenv. The functionality is trivial, so just implement
79        # it here.
80        if IS_CYGWIN or IS_NATIVE_WIN:
81            return os.path.join(self.virtualenv_root, 'Scripts')
82
83        return os.path.join(self.virtualenv_root, 'bin')
84
85    @property
86    def python_path(self):
87        binary = 'python'
88        if sys.platform in ('win32', 'cygwin'):
89            binary += '.exe'
90
91        return os.path.join(self.bin_path, binary)
92
93    @property
94    def version_info(self):
95        return eval(subprocess.check_output([
96            self.python_path, '-c', 'import sys; print(sys.version_info[:])']))
97
98    @property
99    def activate_path(self):
100        return os.path.join(self.bin_path, 'activate_this.py')
101
102    def get_exe_info(self):
103        """Returns the version and file size of the python executable that was in
104        use when this virtualenv was created.
105        """
106        with open(self.exe_info_path, 'r') as fh:
107            version, size = fh.read().splitlines()
108        return int(version), int(size)
109
110    def write_exe_info(self, python):
111        """Records the the version of the python executable that was in use when
112        this virtualenv was created. We record this explicitly because
113        on OS X our python path may end up being a different or modified
114        executable.
115        """
116        ver = self.python_executable_hexversion(python)
117        with open(self.exe_info_path, 'w') as fh:
118            fh.write("%s\n" % ver)
119            fh.write("%s\n" % os.path.getsize(python))
120
121    def python_executable_hexversion(self, python):
122        """Run a Python executable and return its sys.hexversion value."""
123        program = 'import sys; print(sys.hexversion)'
124        out = subprocess.check_output([python, '-c', program]).rstrip()
125        return int(out)
126
127    def up_to_date(self, python):
128        """Returns whether the virtualenv is present and up to date.
129
130        Args:
131            python: Full path string to the Python executable that this virtualenv
132                should be running.  If the Python executable passed in to this
133                argument is not the same version as the Python the virtualenv was
134                built with then this method will return False.
135        """
136
137        deps = [self.manifest_path, __file__]
138
139        # check if virtualenv exists
140        if not os.path.exists(self.virtualenv_root) or \
141                not os.path.exists(self.activate_path):
142            return False
143
144        # Modifications to our package dependency list or to this file mean the
145        # virtualenv should be rebuilt.
146        activate_mtime = os.path.getmtime(self.activate_path)
147        dep_mtime = max(os.path.getmtime(p) for p in deps)
148        if dep_mtime > activate_mtime:
149            return False
150
151        # Verify that the Python we're checking here is either the virutalenv
152        # python, or we have the Python version that was used to create the
153        # virtualenv. If this fails, it is likely system Python has been
154        # upgraded, and our virtualenv would not be usable.
155        orig_version, orig_size = self.get_exe_info()
156        python_size = os.path.getsize(python)
157        hexversion = self.python_executable_hexversion(python)
158        if ((python, python_size) != (self.python_path, os.path.getsize(self.python_path)) and
159                (hexversion, python_size) != (orig_version, orig_size)):
160            return False
161
162        # recursively check sub packages.txt files
163        submanifests = [i[1] for i in self.packages()
164                        if i[0] == 'packages.txt']
165        for submanifest in submanifests:
166            submanifest = os.path.join(self.topsrcdir, submanifest)
167            submanager = VirtualenvManager(self.topsrcdir,
168                                           self.topobjdir,
169                                           self.virtualenv_root,
170                                           self.log_handle,
171                                           submanifest)
172            if not submanager.up_to_date(python):
173                return False
174
175        return True
176
177    def ensure(self, python=sys.executable):
178        """Ensure the virtualenv is present and up to date.
179
180        If the virtualenv is up to date, this does nothing. Otherwise, it
181        creates and populates the virtualenv as necessary.
182
183        This should be the main API used from this class as it is the
184        highest-level.
185        """
186        if self.up_to_date(python):
187            return self.virtualenv_root
188        return self.build(python)
189
190    def _log_process_output(self, *args, **kwargs):
191        env = kwargs.pop('env', None) or os.environ.copy()
192        # PYTHONEXECUTABLE can mess up the creation of virtualenvs when set.
193        env.pop('PYTHONEXECUTABLE', None)
194        kwargs['env'] = ensure_subprocess_env(env)
195
196        if hasattr(self.log_handle, 'fileno'):
197            return subprocess.call(*args, stdout=self.log_handle,
198                                   stderr=subprocess.STDOUT, **kwargs)
199
200        proc = subprocess.Popen(*args, stdout=subprocess.PIPE,
201                                stderr=subprocess.STDOUT, **kwargs)
202
203        for line in proc.stdout:
204            if PY2:
205                self.log_handle.write(line)
206            else:
207                self.log_handle.write(line.decode('UTF-8'))
208
209        return proc.wait()
210
211    def create(self, python):
212        """Create a new, empty virtualenv.
213
214        Receives the path to virtualenv's virtualenv.py script (which will be
215        called out to), the path to create the virtualenv in, and a handle to
216        write output to.
217        """
218
219        # Corner-case: in some cases, we call this function even though there's
220        # already a virtualenv in place in `self.virtualenv_root`. That is not a
221        # problem in itself, except when not using the same `python`. For example:
222        # - the old virtualenv was created with `/usr/bin/pythonx.y`
223        # - as such, in contains `pythonx.y` as a file, and `python` and `pythonx`
224        #   as symbolic links to `pythonx.y`
225        # - the new virtualenv is being created with `/usr/bin/pythonx`
226        # - the virtualenv script uses shutil.copyfile to copy `/usr/bin/pythonx`
227        #   to `pythonx` in the virtualenv. As that is an existing symbolic link,
228        #   the copy ends up writing the file into `pythonx.y`.
229        # - the virtualenv script then creates `python` and `pythonx.y` symbolic
230        #   links to `pythonx`. `pythonx` is still a symbolic link to `pythonx.y`,
231        #   and now `pythonx.y` is a symbolic link to `pythonx`, so we end with a
232        #   symbolic link loop, and no real python executable around.
233        # So if the file with the same name as the python executable used to create
234        # the new virtualenv is a symbolic link, remove it before invoking
235        # virtualenv.
236        venv_python = os.path.join(self.bin_path, os.path.basename(python))
237        if os.path.islink(venv_python):
238            os.remove(venv_python)
239
240        args = [python, self.virtualenv_script_path,
241                # Without this, virtualenv.py may attempt to contact the outside
242                # world and search for or download a newer version of pip,
243                # setuptools, or wheel. This is bad for security, reproducibility,
244                # and speed.
245                '--no-download',
246                self.virtualenv_root]
247
248        result = self._log_process_output(args)
249
250        if result:
251            raise Exception(
252                'Failed to create virtualenv: %s (virtualenv.py retcode: %s)' % (
253                    self.virtualenv_root, result))
254
255        self.write_exe_info(python)
256
257        return self.virtualenv_root
258
259    def packages(self):
260        mode = 'rU' if PY2 else 'r'
261        with open(self.manifest_path, mode) as fh:
262            packages = [line.rstrip().split(':')
263                        for line in fh]
264        return packages
265
266    def populate(self):
267        """Populate the virtualenv.
268
269        The manifest file consists of colon-delimited fields. The first field
270        specifies the action. The remaining fields are arguments to that
271        action. The following actions are supported:
272
273        setup.py -- Invoke setup.py for a package. Expects the arguments:
274            1. relative path directory containing setup.py.
275            2. argument(s) to setup.py. e.g. "develop". Each program argument
276               is delimited by a colon. Arguments with colons are not yet
277               supported.
278
279        filename.pth -- Adds the path given as argument to filename.pth under
280            the virtualenv site packages directory.
281
282        optional -- This denotes the action as optional. The requested action
283            is attempted. If it fails, we issue a warning and go on. The
284            initial "optional" field is stripped then the remaining line is
285            processed like normal. e.g.
286            "optional:setup.py:python/foo:built_ext:-i"
287
288        copy -- Copies the given file in the virtualenv site packages
289            directory.
290
291        packages.txt -- Denotes that the specified path is a child manifest. It
292            will be read and processed as if its contents were concatenated
293            into the manifest being read.
294
295        objdir -- Denotes a relative path in the object directory to add to the
296            search path. e.g. "objdir:build" will add $topobjdir/build to the
297            search path.
298
299        windows -- This denotes that the action should only be taken when run
300            on Windows.
301
302        !windows -- This denotes that the action should only be taken when run
303            on non-Windows systems.
304
305        python3 -- This denotes that the action should only be taken when run
306            on Python 3.
307
308        python2 -- This denotes that the action should only be taken when run
309            on python 2.
310
311        Note that the Python interpreter running this function should be the
312        one from the virtualenv. If it is the system Python or if the
313        environment is not configured properly, packages could be installed
314        into the wrong place. This is how virtualenv's work.
315        """
316        import distutils.sysconfig
317
318        packages = self.packages()
319        python_lib = distutils.sysconfig.get_python_lib()
320
321        def handle_package(package):
322            if package[0] == 'setup.py':
323                assert len(package) >= 2
324
325                self.call_setup(os.path.join(self.topsrcdir, package[1]),
326                                package[2:])
327
328                return True
329
330            if package[0] == 'copy':
331                assert len(package) == 2
332
333                src = os.path.join(self.topsrcdir, package[1])
334                dst = os.path.join(python_lib, os.path.basename(package[1]))
335
336                shutil.copy(src, dst)
337
338                return True
339
340            if package[0] == 'packages.txt':
341                assert len(package) == 2
342
343                src = os.path.join(self.topsrcdir, package[1])
344                assert os.path.isfile(src), "'%s' does not exist" % src
345                submanager = VirtualenvManager(self.topsrcdir,
346                                               self.topobjdir,
347                                               self.virtualenv_root,
348                                               self.log_handle,
349                                               src)
350                submanager.populate()
351
352                return True
353
354            if package[0].endswith('.pth'):
355                assert len(package) == 2
356
357                path = os.path.join(self.topsrcdir, package[1])
358
359                with open(os.path.join(python_lib, package[0]), 'a') as f:
360                    # This path is relative to the .pth file.  Using a
361                    # relative path allows the srcdir/objdir combination
362                    # to be moved around (as long as the paths relative to
363                    # each other remain the same).
364                    try:
365                        f.write("%s\n" % os.path.relpath(path, python_lib))
366                    except ValueError:
367                        # When objdir is on a separate drive, relpath throws
368                        f.write("%s\n" % os.path.join(python_lib, path))
369
370                return True
371
372            if package[0] == 'optional':
373                try:
374                    handle_package(package[1:])
375                    return True
376                except Exception:
377                    print('Error processing command. Ignoring',
378                          'because optional. (%s)' % ':'.join(package),
379                          file=self.log_handle)
380                    return False
381
382            if package[0] in ('windows', '!windows'):
383                for_win = not package[0].startswith('!')
384                is_win = sys.platform == 'win32'
385                if is_win == for_win:
386                    handle_package(package[1:])
387                return True
388
389            if package[0] in ('python2', 'python3'):
390                for_python3 = package[0].endswith('3')
391                if PY3 == for_python3:
392                    handle_package(package[1:])
393                return True
394
395            if package[0] == 'objdir':
396                assert len(package) == 2
397                path = os.path.join(self.topobjdir, package[1])
398
399                with open(os.path.join(python_lib, 'objdir.pth'), 'a') as f:
400                    f.write('%s\n' % path)
401
402                return True
403
404            raise Exception('Unknown action: %s' % package[0])
405
406        # We always target the OS X deployment target that Python itself was
407        # built with, regardless of what's in the current environment. If we
408        # don't do # this, we may run into a Python bug. See
409        # http://bugs.python.org/issue9516 and bug 659881.
410        #
411        # Note that this assumes that nothing compiled in the virtualenv is
412        # shipped as part of a distribution. If we do ship anything, the
413        # deployment target here may be different from what's targeted by the
414        # shipping binaries and # virtualenv-produced binaries may fail to
415        # work.
416        #
417        # We also ignore environment variables that may have been altered by
418        # configure or a mozconfig activated in the current shell. We trust
419        # Python is smart enough to find a proper compiler and to use the
420        # proper compiler flags. If it isn't your Python is likely broken.
421        IGNORE_ENV_VARIABLES = ('CC', 'CXX', 'CFLAGS', 'CXXFLAGS', 'LDFLAGS')
422
423        try:
424            old_target = os.environ.get('MACOSX_DEPLOYMENT_TARGET', None)
425            sysconfig_target = \
426                distutils.sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET')
427
428            if sysconfig_target is not None:
429                # MACOSX_DEPLOYMENT_TARGET is usually a string (e.g.: "10.14.6"), but
430                # in some cases it is an int (e.g.: 11). Since environment variables
431                # must all be str, explicitly convert it.
432                os.environ["MACOSX_DEPLOYMENT_TARGET"] = str(sysconfig_target)
433
434            old_env_variables = {}
435            for k in IGNORE_ENV_VARIABLES:
436                if k not in os.environ:
437                    continue
438
439                old_env_variables[k] = os.environ[k]
440                del os.environ[k]
441
442            for package in packages:
443                handle_package(package)
444
445            sitecustomize = os.path.join(
446                os.path.dirname(python_lib), 'sitecustomize.py')
447            with open(sitecustomize, 'w') as f:
448                f.write(
449                    '# Importing mach_bootstrap has the side effect of\n'
450                    '# installing an import hook\n'
451                    'import mach_bootstrap\n'
452                )
453
454        finally:
455            os.environ.pop('MACOSX_DEPLOYMENT_TARGET', None)
456
457            if old_target is not None:
458                os.environ['MACOSX_DEPLOYMENT_TARGET'] = old_target
459
460            os.environ.update(old_env_variables)
461
462    def call_setup(self, directory, arguments):
463        """Calls setup.py in a directory."""
464        setup = os.path.join(directory, 'setup.py')
465
466        program = [self.python_path, setup]
467        program.extend(arguments)
468
469        # We probably could call the contents of this file inside the context
470        # of this interpreter using execfile() or similar. However, if global
471        # variables like sys.path are adjusted, this could cause all kinds of
472        # havoc. While this may work, invoking a new process is safer.
473
474        try:
475            output = subprocess.check_output(program, cwd=directory, stderr=subprocess.STDOUT)
476            print(output)
477        except subprocess.CalledProcessError as e:
478            if 'Python.h: No such file or directory' in e.output:
479                print('WARNING: Python.h not found. Install Python development headers.')
480            else:
481                print(e.output)
482
483            raise Exception('Error installing package: %s' % directory)
484
485    def build(self, python):
486        """Build a virtualenv per tree conventions.
487
488        This returns the path of the created virtualenv.
489        """
490
491        self.create(python)
492
493        # We need to populate the virtualenv using the Python executable in
494        # the virtualenv for paths to be proper.
495
496        # If this module was run from Python 2 then the __file__ attribute may
497        # point to a Python 2 .pyc file. If we are generating a Python 3
498        # virtualenv from Python 2 make sure we call Python 3 with the path to
499        # the module and not the Python 2 .pyc file.
500        if os.path.splitext(__file__)[1] in ('.pyc', '.pyo'):
501            thismodule = __file__[:-1]
502        else:
503            thismodule = __file__
504
505        args = [self.python_path, thismodule, 'populate', self.topsrcdir,
506                self.topobjdir, self.virtualenv_root, self.manifest_path]
507
508        result = self._log_process_output(args, cwd=self.topsrcdir)
509
510        if result != 0:
511            raise Exception('Error populating virtualenv.')
512
513        os.utime(self.activate_path, None)
514
515        return self.virtualenv_root
516
517    def activate(self):
518        """Activate the virtualenv in this Python context.
519
520        If you run a random Python script and wish to "activate" the
521        virtualenv, you can simply instantiate an instance of this class
522        and call .ensure() and .activate() to make the virtualenv active.
523        """
524
525        exec(open(self.activate_path).read(), dict(__file__=self.activate_path))
526        if PY2 and isinstance(os.environ['PATH'], unicode):
527            os.environ['PATH'] = os.environ['PATH'].encode('utf-8')
528
529    def install_pip_package(self, package, vendored=False):
530        """Install a package via pip.
531
532        The supplied package is specified using a pip requirement specifier.
533        e.g. 'foo' or 'foo==1.0'.
534
535        If the package is already installed, this is a no-op.
536
537        If vendored is True, no package index will be used and no dependencies
538        will be installed.
539        """
540        from pip._internal.req.constructors import install_req_from_line
541
542        req = install_req_from_line(package)
543        req.check_if_exists(use_user_site=False)
544        if req.satisfied_by is not None:
545            return
546
547        args = [
548            'install',
549            package,
550        ]
551
552        if vendored:
553            args.extend([
554                '--no-deps',
555                '--no-index',
556                # The setup will by default be performed in an isolated build
557                # environment, and since we're running with --no-index, this
558                # means that pip will be unable to install in the isolated build
559                # environment any dependencies that might be specified in a
560                # setup_requires directive for the package. Since we're manually
561                # controlling our build environment, build isolation isn't a
562                # concern and we can disable that feature. Note that this is
563                # safe and doesn't risk trampling any other packages that may be
564                # installed due to passing `--no-deps --no-index` as well.
565                '--no-build-isolation',
566            ])
567
568        return self._run_pip(args)
569
570    def install_pip_requirements(self, path, require_hashes=True, quiet=False, vendored=False):
571        """Install a pip requirements.txt file.
572
573        The supplied path is a text file containing pip requirement
574        specifiers.
575
576        If require_hashes is True, each specifier must contain the
577        expected hash of the downloaded package. See:
578        https://pip.pypa.io/en/stable/reference/pip_install/#hash-checking-mode
579        """
580
581        if not os.path.isabs(path):
582            path = os.path.join(self.topsrcdir, path)
583
584        args = [
585            'install',
586            '--requirement',
587            path,
588        ]
589
590        if require_hashes:
591            args.append('--require-hashes')
592
593        if quiet:
594            args.append('--quiet')
595
596        if vendored:
597            args.extend([
598                '--no-deps',
599                '--no-index',
600            ])
601
602        return self._run_pip(args)
603
604    def _run_pip(self, args):
605        # It's tempting to call pip natively via pip.main(). However,
606        # the current Python interpreter may not be the virtualenv python.
607        # This will confuse pip and cause the package to attempt to install
608        # against the executing interpreter. By creating a new process, we
609        # force the virtualenv's interpreter to be used and all is well.
610        # It /might/ be possible to cheat and set sys.executable to
611        # self.python_path. However, this seems more risk than it's worth.
612        pip = os.path.join(self.bin_path, 'pip')
613        subprocess.check_call([pip] + args, stderr=subprocess.STDOUT, cwd=self.topsrcdir,
614                              universal_newlines=PY3)
615
616    def activate_pipenv(self, pipfile=None, populate=False, python=None):
617        """Activate a virtual environment managed by pipenv
618
619        If ``pipfile`` is not ``None`` then the Pipfile located at the path
620        provided will be used to create the virtual environment. If
621        ``populate`` is ``True`` then the virtual environment will be
622        populated from the manifest file. The optional ``python`` argument
623        indicates the version of Python for pipenv to use.
624        """
625        pipenv = os.path.join(self.bin_path, 'pipenv')
626        env = ensure_subprocess_env(os.environ.copy())
627        env.update(ensure_subprocess_env({
628            'PIPENV_IGNORE_VIRTUALENVS': '1',
629            'PIP_NO_INDEX': '1',
630            'WORKON_HOME': str(os.path.normpath(os.path.join(self.topobjdir, '_virtualenvs')))
631        }))
632        # On mac, running pipenv with LC_CTYPE set to "UTF-8" (which happens
633        # when wrapping with run-task on automation) fails.
634        # Unsetting it doesn't really matter for what pipenv does.
635        env.pop('LC_CTYPE', None)
636
637        # Avoid click RuntimeError under python 3 on linux: http://click.pocoo.org/python3/
638        if PY3 and sys.platform == 'linux':
639            env.update(ensure_subprocess_env({
640                'LC_ALL': 'C.UTF-8',
641                'LANG': 'C.UTF-8'
642            }))
643
644        if python is not None:
645            env.update(ensure_subprocess_env({
646                'PIPENV_DEFAULT_PYTHON_VERSION': str(python),
647                'PIPENV_PYTHON': str(python)
648            }))
649
650        def ensure_venv():
651            """Create virtual environment if needed and return path"""
652            venv = get_venv()
653            if venv is not None:
654                return venv
655            if python is not None:
656                subprocess.check_call(
657                    [pipenv, '--python', python],
658                    stderr=subprocess.STDOUT,
659                    env=env)
660            return get_venv()
661
662        def get_venv():
663            """Return path to virtual environment or None"""
664            try:
665                return subprocess.check_output(
666                        [pipenv, '--venv'],
667                        stderr=subprocess.STDOUT,
668                        env=env, universal_newlines=True).rstrip()
669
670            except subprocess.CalledProcessError:
671                # virtual environment does not exist
672                return None
673
674        if pipfile is not None:
675            # Install from Pipfile
676            env_ = env.copy()
677            del env_['PIP_NO_INDEX']
678            env_.update(ensure_subprocess_env({
679                'PIPENV_PIPFILE': str(pipfile)
680            }))
681            subprocess.check_call([pipenv, 'install'], stderr=subprocess.STDOUT, env=env_)
682
683        self.virtualenv_root = ensure_venv()
684
685        if populate:
686            # Populate from the manifest
687            subprocess.check_call([
688                pipenv, 'run', 'python', os.path.join(here, 'virtualenv.py'), 'populate',
689                self.topsrcdir, self.topobjdir, self.virtualenv_root, self.manifest_path],
690                stderr=subprocess.STDOUT, env=env)
691
692        self.activate()
693
694
695def verify_python_version(log_handle):
696    """Ensure the current version of Python is sufficient."""
697    from distutils.version import LooseVersion
698
699    major, minor, micro = sys.version_info[:3]
700    minimum_python_versions = {
701        2: LooseVersion('2.7.3'),
702        3: LooseVersion('3.5.0'),
703    }
704    our = LooseVersion('%d.%d.%d' % (major, minor, micro))
705
706    if (major not in minimum_python_versions or
707        our < minimum_python_versions[major]):
708        log_handle.write('One of the following Python versions are required:\n')
709        for minver in minimum_python_versions.values():
710            log_handle.write('* Python %s or greater\n' % minver)
711        log_handle.write('You are running Python %s.\n' % our)
712
713        if os.name in ('nt', 'ce'):
714            log_handle.write(UPGRADE_WINDOWS)
715        else:
716            log_handle.write(UPGRADE_OTHER)
717
718        sys.exit(1)
719
720
721def ensure_subprocess_env(env, encoding='utf-8'):
722    """Ensure the environment is in the correct format for the `subprocess`
723    module.
724
725    This method uses the method with same name from mozbuild.utils as
726    virtualenv.py must be a standalone module.
727
728    This will convert all keys and values to bytes on Python 2, and text on
729    Python 3.
730
731    Args:
732        env (dict): Environment to ensure.
733        encoding (str): Encoding to use when converting to/from bytes/text
734                        (default: utf-8).
735    """
736    # We can't import six.ensure_binary() or six.ensure_text() because this module
737    # has to run stand-alone.  Instead we'll implement an abbreviated version of the
738    # checks it does.
739
740    if PY3:
741        text_type = str
742        binary_type = bytes
743    else:
744        text_type = unicode
745        binary_type = str
746
747    def ensure_binary(s):
748        if isinstance(s, text_type):
749            return s.encode(encoding, errors='strict')
750        elif isinstance(s, binary_type):
751            return s
752        else:
753            raise TypeError("not expecting type '%s'" % type(s))
754
755    def ensure_text(s):
756        if isinstance(s, binary_type):
757            return s.decode(encoding, errors='strict')
758        elif isinstance(s, text_type):
759            return s
760        else:
761            raise TypeError("not expecting type '%s'" % type(s))
762
763    ensure = ensure_binary if PY2 else ensure_text
764
765    try:
766        return {ensure(k): ensure(v) for k, v in env.iteritems()}
767    except AttributeError:
768        return {ensure(k): ensure(v) for k, v in env.items()}
769
770
771if __name__ == '__main__':
772    if len(sys.argv) < 5:
773        print(
774            'Usage: populate_virtualenv.py /path/to/topsrcdir '
775            '/path/to/topobjdir /path/to/virtualenv /path/to/virtualenv_manifest')
776        sys.exit(1)
777
778    verify_python_version(sys.stdout)
779
780    topsrcdir, topobjdir, virtualenv_path, manifest_path = sys.argv[1:5]
781    populate = False
782
783    # This should only be called internally.
784    if sys.argv[1] == 'populate':
785        populate = True
786        topsrcdir, topobjdir, virtualenv_path, manifest_path = sys.argv[2:]
787
788    manager = VirtualenvManager(topsrcdir, topobjdir, virtualenv_path,
789                                sys.stdout, manifest_path)
790
791    if populate:
792        manager.populate()
793    else:
794        manager.ensure()
795