• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2
3# Copyright 2020 The Pigweed Authors
4#
5# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6# use this file except in compliance with the License. You may obtain a copy of
7# the License at
8#
9#     https://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14# License for the specific language governing permissions and limitations under
15# the License.
16"""Environment setup script for Pigweed.
17
18This script installs everything and writes out a file for the user's shell
19to source.
20
21For now, this is valid Python 2 and Python 3. Once we switch to running this
22with PyOxidizer it can be upgraded to recent Python 3.
23"""
24
25from __future__ import print_function
26
27import argparse
28import copy
29import glob
30import inspect
31import json
32import os
33import shutil
34import subprocess
35import sys
36import time
37
38# If we're running oxidized, filesystem-centric import hacks won't work. In that
39# case, jump straight to the imports and assume oxidation brought in the deps.
40if not getattr(sys, 'oxidized', False):
41    old_sys_path = copy.deepcopy(sys.path)
42    filename = None
43    if hasattr(sys.modules[__name__], '__file__'):
44        filename = __file__
45    else:
46        # Try introspection in environments where __file__ is not populated.
47        frame = inspect.currentframe()
48        if frame is not None:
49            filename = inspect.getfile(frame)
50    # If none of our strategies worked, we're in a strange runtime environment.
51    # The imports are almost certainly going to fail.
52    if filename is None:
53        raise RuntimeError(
54            'Unable to locate pw_env_setup module; cannot continue.\n'
55            '\n'
56            'Try updating to one of the standard Python implemetations:\n'
57            '  https://www.python.org/downloads/'
58        )
59    sys.path = [
60        os.path.abspath(os.path.join(filename, os.path.pardir, os.path.pardir))
61    ]
62    import pw_env_setup  # pylint: disable=unused-import
63
64    sys.path = old_sys_path
65
66# pylint: disable=wrong-import-position
67from pw_env_setup.cipd_setup import update as cipd_update
68from pw_env_setup.cipd_setup import wrapper as cipd_wrapper
69from pw_env_setup.colors import Color, enable_colors
70from pw_env_setup import environment
71from pw_env_setup import spinner
72from pw_env_setup import virtualenv_setup
73from pw_env_setup import windows_env_start
74
75
76def _which(
77    executable, pathsep=os.pathsep, use_pathext=None, case_sensitive=None
78):
79    if use_pathext is None:
80        use_pathext = os.name == 'nt'
81    if case_sensitive is None:
82        case_sensitive = os.name != 'nt' and sys.platform != 'darwin'
83
84    if not case_sensitive:
85        executable = executable.lower()
86
87    exts = None
88    if use_pathext:
89        exts = frozenset(os.environ['PATHEXT'].split(pathsep))
90        if not case_sensitive:
91            exts = frozenset(x.lower() for x in exts)
92        if not exts:
93            raise ValueError('empty PATHEXT')
94
95    paths = os.environ['PATH'].split(pathsep)
96    for path in paths:
97        try:
98            entries = frozenset(os.listdir(path))
99            if not case_sensitive:
100                entries = frozenset(x.lower() for x in entries)
101        except OSError:
102            continue
103
104        if exts:
105            for ext in exts:
106                if executable + ext in entries:
107                    return os.path.join(path, executable + ext)
108        else:
109            if executable in entries:
110                return os.path.join(path, executable)
111
112    return None
113
114
115class _Result:
116    class Status:
117        DONE = 'done'
118        SKIPPED = 'skipped'
119        FAILED = 'failed'
120
121    def __init__(self, status, *messages):
122        self._status = status
123        self._messages = list(messages)
124
125    def ok(self):
126        return self._status in {_Result.Status.DONE, _Result.Status.SKIPPED}
127
128    def status_str(self, duration=None):
129        if not duration:
130            return self._status
131
132        duration_parts = []
133        if duration > 60:
134            minutes = int(duration // 60)
135            duration %= 60
136            duration_parts.append('{}m'.format(minutes))
137        duration_parts.append('{:.1f}s'.format(duration))
138        return '{} ({})'.format(self._status, ''.join(duration_parts))
139
140    def messages(self):
141        return self._messages
142
143
144class ConfigError(Exception):
145    pass
146
147
148def result_func(glob_warnings=()):
149    def result(status, *args):
150        return _Result(status, *([str(x) for x in glob_warnings] + list(args)))
151
152    return result
153
154
155class ConfigFileError(Exception):
156    pass
157
158
159class MissingSubmodulesError(Exception):
160    pass
161
162
163# TODO(mohrr) remove disable=useless-object-inheritance once in Python 3.
164# pylint: disable=useless-object-inheritance
165# pylint: disable=too-many-instance-attributes
166# pylint: disable=too-many-arguments
167class EnvSetup(object):
168    """Run environment setup for Pigweed."""
169
170    def __init__(
171        self,
172        pw_root,
173        cipd_cache_dir,
174        shell_file,
175        quiet,
176        install_dir,
177        strict,
178        virtualenv_gn_out_dir,
179        json_file,
180        project_root,
181        config_file,
182        use_existing_cipd,
183        check_submodules,
184        use_pinned_pip_packages,
185        cipd_only,
186        trust_cipd_hash,
187    ):
188        self._env = environment.Environment()
189        self._project_root = project_root
190        self._pw_root = pw_root
191        self._setup_root = os.path.join(
192            pw_root, 'pw_env_setup', 'py', 'pw_env_setup'
193        )
194        self._cipd_cache_dir = cipd_cache_dir
195        self._shell_file = shell_file
196        self._is_windows = os.name == 'nt'
197        self._quiet = quiet
198        self._install_dir = install_dir
199        self._virtualenv_root = os.path.join(self._install_dir, 'pigweed-venv')
200        self._strict = strict
201        self._cipd_only = cipd_only
202        self._trust_cipd_hash = trust_cipd_hash
203
204        if os.path.isfile(shell_file):
205            os.unlink(shell_file)
206
207        if isinstance(self._pw_root, bytes) and bytes != str:
208            self._pw_root = self._pw_root.decode()
209
210        self._cipd_package_file = []
211        self._virtualenv_requirements = []
212        self._virtualenv_constraints = []
213        self._virtualenv_gn_targets = []
214        self._virtualenv_gn_args = []
215        self._use_pinned_pip_packages = use_pinned_pip_packages
216        self._optional_submodules = []
217        self._required_submodules = []
218        self._virtualenv_system_packages = False
219        self._pw_packages = []
220        self._root_variable = None
221
222        self._check_submodules = check_submodules
223
224        self._json_file = json_file
225        self._gni_file = None
226
227        self._config_file_name = getattr(config_file, 'name', 'config file')
228        self._env.set('_PW_ENVIRONMENT_CONFIG_FILE', self._config_file_name)
229        if config_file:
230            self._parse_config_file(config_file)
231
232        self._check_submodule_presence()
233
234        self._use_existing_cipd = use_existing_cipd
235        self._virtualenv_gn_out_dir = virtualenv_gn_out_dir
236
237        if self._root_variable:
238            self._env.set(self._root_variable, project_root, deactivate=False)
239        self._env.set('PW_PROJECT_ROOT', project_root, deactivate=False)
240        self._env.set('PW_ROOT', pw_root, deactivate=False)
241        self._env.set('_PW_ACTUAL_ENVIRONMENT_ROOT', install_dir)
242        self._env.add_replacement('_PW_ACTUAL_ENVIRONMENT_ROOT', install_dir)
243        self._env.add_replacement('PW_ROOT', pw_root)
244
245    def _process_globs(self, globs):
246        unique_globs = []
247        for pat in globs:
248            if pat and pat not in unique_globs:
249                unique_globs.append(pat)
250
251        files = []
252        warnings = []
253        for pat in unique_globs:
254            if pat:
255                matches = glob.glob(pat)
256                if not matches:
257                    warning = 'pattern "{}" matched 0 files'.format(pat)
258                    warnings.append('warning: {}'.format(warning))
259                    if self._strict:
260                        raise ConfigError(warning)
261
262                files.extend(matches)
263
264        if globs and not files:
265            warnings.append('warning: matched 0 total files')
266            if self._strict:
267                raise ConfigError('matched 0 total files')
268
269        return files, warnings
270
271    def _parse_config_file(self, config_file):
272        config = json.load(config_file)
273
274        self._root_variable = config.pop('root_variable', None)
275
276        rosetta = config.pop('rosetta', 'allow')
277        if rosetta not in ('never', 'allow', 'force'):
278            raise ValueError(rosetta)
279        self._rosetta = rosetta in ('allow', 'force')
280        self._env.set('_PW_ROSETTA', str(int(self._rosetta)))
281
282        if 'json_file' in config:
283            self._json_file = config.pop('json_file')
284
285        self._gni_file = config.pop('gni_file', None)
286
287        self._optional_submodules.extend(config.pop('optional_submodules', ()))
288        self._required_submodules.extend(config.pop('required_submodules', ()))
289
290        if self._optional_submodules and self._required_submodules:
291            raise ValueError(
292                '{} contains both "optional_submodules" and '
293                '"required_submodules", but these options are mutually '
294                'exclusive'.format(self._config_file_name)
295            )
296
297        self._cipd_package_file.extend(
298            os.path.join(self._project_root, x)
299            for x in config.pop('cipd_package_files', ())
300        )
301
302        for pkg in config.pop('pw_packages', ()):
303            self._pw_packages.append(pkg)
304
305        virtualenv = config.pop('virtualenv', {})
306
307        if virtualenv.get('gn_root'):
308            root = os.path.join(self._project_root, virtualenv.pop('gn_root'))
309        else:
310            root = self._project_root
311
312        for target in virtualenv.pop('gn_targets', ()):
313            self._virtualenv_gn_targets.append(
314                virtualenv_setup.GnTarget('{}#{}'.format(root, target))
315            )
316
317        self._virtualenv_gn_args = virtualenv.pop('gn_args', ())
318
319        self._virtualenv_system_packages = virtualenv.pop(
320            'system_packages', False
321        )
322
323        for req_txt in virtualenv.pop('requirements', ()):
324            self._virtualenv_requirements.append(
325                os.path.join(self._project_root, req_txt)
326            )
327
328        for constraint_txt in virtualenv.pop('constraints', ()):
329            self._virtualenv_constraints.append(
330                os.path.join(self._project_root, constraint_txt)
331            )
332
333        if virtualenv:
334            raise ConfigFileError(
335                'unrecognized option in {}: "virtualenv.{}"'.format(
336                    self._config_file_name, next(iter(virtualenv))
337                )
338            )
339
340        if config:
341            raise ConfigFileError(
342                'unrecognized option in {}: "{}"'.format(
343                    self._config_file_name, next(iter(config))
344                )
345            )
346
347    def _check_submodule_presence(self):
348        uninitialized = set()
349
350        # Don't check submodule presence if using the Android Repo Tool.
351        if os.path.isdir(os.path.join(self._project_root, '.repo')):
352            return
353
354        if not self._check_submodules:
355            return
356
357        cmd = ['git', 'submodule', 'status', '--recursive']
358
359        for line in subprocess.check_output(
360            cmd, cwd=self._project_root
361        ).splitlines():
362            if isinstance(line, bytes):
363                line = line.decode()
364            # Anything but an initial '-' means the submodule is initialized.
365            if not line.startswith('-'):
366                continue
367            uninitialized.add(line.split()[1])
368
369        missing = uninitialized - set(self._optional_submodules)
370        if self._required_submodules:
371            missing = set(self._required_submodules) & uninitialized
372
373        if missing:
374            print(
375                'Not all submodules are initialized. Please run the '
376                'following commands.',
377                file=sys.stderr,
378            )
379            print('', file=sys.stderr)
380
381            for miss in sorted(missing):
382                print(
383                    '    git submodule update --init {}'.format(miss),
384                    file=sys.stderr,
385                )
386            print('', file=sys.stderr)
387
388            if self._required_submodules:
389                print(
390                    'If these submodules are not required, remove them from '
391                    'the "required_submodules"',
392                    file=sys.stderr,
393                )
394
395            else:
396                print(
397                    'If these submodules are not required, add them to the '
398                    '"optional_submodules"',
399                    file=sys.stderr,
400                )
401
402            print('list in the environment config JSON file:', file=sys.stderr)
403            print('    {}'.format(self._config_file_name), file=sys.stderr)
404            print('', file=sys.stderr)
405
406            raise MissingSubmodulesError(', '.join(sorted(missing)))
407
408    def _write_gni_file(self):
409        if self._cipd_only:
410            return
411
412        gni_file = os.path.join(
413            self._project_root, 'build_overrides', 'pigweed_environment.gni'
414        )
415        if self._gni_file:
416            gni_file = os.path.join(self._project_root, self._gni_file)
417
418        with open(gni_file, 'w') as outs:
419            self._env.gni(outs, self._project_root)
420
421    def _log(self, *args, **kwargs):
422        # Not using logging module because it's awkward to flush a log handler.
423        if self._quiet:
424            return
425        flush = kwargs.pop('flush', False)
426        print(*args, **kwargs)
427        if flush:
428            sys.stdout.flush()
429
430    def setup(self):
431        """Runs each of the env_setup steps."""
432
433        if os.name == 'nt':
434            windows_env_start.print_banner(bootstrap=True, no_shell_file=False)
435        else:
436            enable_colors()
437
438        steps = [
439            ('CIPD package manager', self.cipd),
440            ('Python environment', self.virtualenv),
441            ('pw packages', self.pw_package),
442            ('Host tools', self.host_tools),
443        ]
444
445        if self._is_windows:
446            steps.append(("Windows scripts", self.win_scripts))
447
448        if self._cipd_only:
449            steps = [('CIPD package manager', self.cipd)]
450
451        self._log(
452            Color.bold(
453                'Downloading and installing packages into local '
454                'source directory:\n'
455            )
456        )
457
458        max_name_len = max(len(name) for name, _ in steps)
459
460        self._env.comment(
461            '''
462This file is automatically generated. DO NOT EDIT!
463For details, see $PW_ROOT/pw_env_setup/py/pw_env_setup/env_setup.py and
464$PW_ROOT/pw_env_setup/py/pw_env_setup/environment.py.
465'''.strip()
466        )
467
468        if not self._is_windows:
469            self._env.comment(
470                '''
471For help debugging errors in this script, uncomment the next line.
472set -x
473Then use `set +x` to go back to normal.
474'''.strip()
475            )
476
477        self._env.echo(
478            Color.bold(
479                'Activating environment (setting environment variables):'
480            )
481        )
482        self._env.echo('')
483
484        for name, step in steps:
485            self._log(
486                '  Setting up {name:.<{width}}...'.format(
487                    name=name, width=max_name_len
488                ),
489                end='',
490                flush=True,
491            )
492            self._env.echo(
493                '  Setting environment variables for '
494                '{name:.<{width}}...'.format(name=name, width=max_name_len),
495                newline=False,
496            )
497
498            start = time.time()
499            spin = spinner.Spinner(self._quiet)
500            with spin():
501                result = step(spin)
502            stop = time.time()
503
504            self._log(result.status_str(stop - start))
505
506            self._env.echo(result.status_str())
507            for message in result.messages():
508                sys.stderr.write('{}\n'.format(message))
509                self._env.echo(message)
510
511            if not result.ok():
512                return -1
513
514            # Log the environment state at the end of each step for debugging.
515            log_dir = os.path.join(self._install_dir, 'logs')
516            if not os.path.isdir(log_dir):
517                os.makedirs(log_dir)
518            actions_json = os.path.join(
519                log_dir, 'post-{}.json'.format(name.replace(' ', '_'))
520            )
521            with open(actions_json, 'w') as outs:
522                self._env.json(outs)
523
524            # This file needs to be written after the CIPD step and before the
525            # Python virtualenv step. It also needs to be rewritten after the
526            # Python virtualenv step, so it's easiest to just write it after
527            # every step.
528            self._write_gni_file()
529
530        self._log('')
531        self._env.echo('')
532
533        self._env.finalize()
534
535        self._env.echo(Color.bold('Checking the environment:'))
536        self._env.echo()
537
538        self._env.doctor()
539        self._env.echo()
540
541        self._env.echo(
542            Color.bold('Environment looks good, you are ready to go!')
543        )
544        self._env.echo()
545
546        # Don't write new files if all we did was update CIPD packages.
547        if self._cipd_only:
548            return 0
549
550        with open(self._shell_file, 'w') as outs:
551            self._env.write(outs)
552
553        deactivate = os.path.join(
554            self._install_dir,
555            'deactivate{}'.format(os.path.splitext(self._shell_file)[1]),
556        )
557        with open(deactivate, 'w') as outs:
558            self._env.write_deactivate(outs)
559
560        config = {
561            # Skipping sysname and nodename in os.uname(). nodename could change
562            # based on the current network. sysname won't change, but is
563            # redundant because it's contained in release or version, and
564            # skipping it here simplifies logic.
565            'uname': ' '.join(getattr(os, 'uname', lambda: ())()[2:]),
566            'os': os.name,
567        }
568
569        with open(os.path.join(self._install_dir, 'config.json'), 'w') as outs:
570            outs.write(
571                json.dumps(config, indent=4, separators=(',', ': ')) + '\n'
572            )
573
574        json_file = self._json_file or os.path.join(
575            self._install_dir, 'actions.json'
576        )
577        with open(json_file, 'w') as outs:
578            self._env.json(outs)
579
580        return 0
581
582    def cipd(self, spin):
583        """Set up cipd and install cipd packages."""
584
585        install_dir = os.path.join(self._install_dir, 'cipd')
586
587        # There's no way to get to the UnsupportedPlatform exception if this
588        # flag is set, but this flag should only be set in LUCI builds which
589        # will always have CIPD.
590        if self._use_existing_cipd:
591            cipd_client = 'cipd'
592
593        else:
594            try:
595                cipd_client = cipd_wrapper.init(
596                    install_dir,
597                    silent=True,
598                    rosetta=self._rosetta,
599                )
600            except cipd_wrapper.UnsupportedPlatform as exc:
601                return result_func(('    {!r}'.format(exc),))(
602                    _Result.Status.SKIPPED,
603                    '    abandoning CIPD setup',
604                )
605
606        package_files, glob_warnings = self._process_globs(
607            self._cipd_package_file
608        )
609        result = result_func(glob_warnings)
610
611        if not package_files:
612            return result(_Result.Status.SKIPPED)
613
614        if not cipd_update.update(
615            cipd=cipd_client,
616            root_install_dir=install_dir,
617            package_files=package_files,
618            cache_dir=self._cipd_cache_dir,
619            env_vars=self._env,
620            rosetta=self._rosetta,
621            spin=spin,
622            trust_hash=self._trust_cipd_hash,
623        ):
624            return result(_Result.Status.FAILED)
625
626        return result(_Result.Status.DONE)
627
628    def virtualenv(self, unused_spin):
629        """Setup virtualenv."""
630
631        requirements, req_glob_warnings = self._process_globs(
632            self._virtualenv_requirements
633        )
634
635        constraints, constraint_glob_warnings = self._process_globs(
636            self._virtualenv_constraints
637        )
638
639        result = result_func(req_glob_warnings + constraint_glob_warnings)
640
641        orig_python3 = _which('python3')
642        with self._env():
643            new_python3 = _which('python3')
644
645        # There is an issue with the virtualenv module on Windows where it
646        # expects sys.executable to be called "python.exe" or it fails to
647        # properly execute. If we installed Python 3 in the CIPD step we need
648        # to address this. Detect if we did so and if so create a copy of
649        # python3.exe called python.exe so that virtualenv works.
650        if orig_python3 != new_python3 and self._is_windows:
651            python3_copy = os.path.join(
652                os.path.dirname(new_python3), 'python.exe'
653            )
654            if not os.path.exists(python3_copy):
655                shutil.copyfile(new_python3, python3_copy)
656            new_python3 = python3_copy
657
658        if not requirements and not self._virtualenv_gn_targets:
659            return result(_Result.Status.SKIPPED)
660
661        if not virtualenv_setup.install(
662            project_root=self._project_root,
663            venv_path=self._virtualenv_root,
664            requirements=requirements,
665            constraints=constraints,
666            gn_args=self._virtualenv_gn_args,
667            gn_targets=self._virtualenv_gn_targets,
668            gn_out_dir=self._virtualenv_gn_out_dir,
669            python=new_python3,
670            env=self._env,
671            system_packages=self._virtualenv_system_packages,
672            use_pinned_pip_packages=self._use_pinned_pip_packages,
673        ):
674            return result(_Result.Status.FAILED)
675
676        return result(_Result.Status.DONE)
677
678    def pw_package(self, unused_spin):
679        """Install "default" pw packages."""
680
681        result = result_func()
682
683        pkg_dir = os.path.join(self._install_dir, 'packages')
684        self._env.set('PW_PACKAGE_ROOT', pkg_dir)
685
686        if not os.path.isdir(pkg_dir):
687            os.makedirs(pkg_dir)
688
689        if not self._pw_packages:
690            return result(_Result.Status.SKIPPED)
691
692        for pkg in self._pw_packages:
693            print('installing {}'.format(pkg))
694            cmd = ['pw', 'package', 'install', '--force', pkg]
695
696            log = os.path.join(pkg_dir, '{}.log'.format(pkg))
697            try:
698                with open(log, 'w') as outs, self._env():
699                    print(*cmd, file=outs)
700                    subprocess.check_call(
701                        cmd,
702                        cwd=self._project_root,
703                        stdout=outs,
704                        stderr=subprocess.STDOUT,
705                    )
706            except subprocess.CalledProcessError:
707                with open(log, 'r') as ins:
708                    sys.stderr.write(ins.read())
709                    raise
710
711        return result(_Result.Status.DONE)
712
713    def host_tools(self, unused_spin):
714        # The host tools are grabbed from CIPD, at least initially. If the
715        # user has a current host build, that build will be used instead.
716        # TODO(mohrr) find a way to do stuff like this for all projects.
717        host_dir = os.path.join(self._pw_root, 'out', 'host')
718        self._env.prepend('PATH', os.path.join(host_dir, 'host_tools'))
719        return _Result(_Result.Status.DONE)
720
721    def win_scripts(self, unused_spin):
722        # These scripts act as a compatibility layer for windows.
723        env_setup_dir = os.path.join(self._pw_root, 'pw_env_setup')
724        self._env.prepend(
725            'PATH', os.path.join(env_setup_dir, 'windows_scripts')
726        )
727        return _Result(_Result.Status.DONE)
728
729
730def parse(argv=None):
731    """Parse command-line arguments."""
732    parser = argparse.ArgumentParser(prog="python -m pw_env_setup.env_setup")
733
734    pw_root = os.environ.get('PW_ROOT', None)
735    if not pw_root:
736        try:
737            with open(os.devnull, 'w') as outs:
738                pw_root = subprocess.check_output(
739                    ['git', 'rev-parse', '--show-toplevel'], stderr=outs
740                ).strip()
741        except subprocess.CalledProcessError:
742            pw_root = None
743
744    parser.add_argument(
745        '--pw-root',
746        default=pw_root,
747        required=not pw_root,
748    )
749
750    project_root = os.environ.get('PW_PROJECT_ROOT', None) or pw_root
751
752    parser.add_argument(
753        '--project-root',
754        default=project_root,
755        required=not project_root,
756    )
757
758    parser.add_argument(
759        '--cipd-cache-dir',
760        default=os.environ.get(
761            'CIPD_CACHE_DIR', os.path.expanduser('~/.cipd-cache-dir')
762        ),
763    )
764
765    parser.add_argument(
766        '--trust-cipd-hash',
767        action='store_true',
768        help='Only run the cipd executable if the ensure file or command-line '
769        'has changed. Defaults to false since files could have been deleted '
770        'from the installation directory and cipd would add them back.',
771    )
772
773    parser.add_argument(
774        '--shell-file',
775        help='Where to write the file for shells to source.',
776        required=True,
777    )
778
779    parser.add_argument(
780        '--quiet',
781        help='Reduce output.',
782        action='store_true',
783        default='PW_ENVSETUP_QUIET' in os.environ,
784    )
785
786    parser.add_argument(
787        '--install-dir',
788        help='Location to install environment.',
789        required=True,
790    )
791
792    parser.add_argument(
793        '--config-file',
794        help='JSON file describing CIPD and virtualenv requirements.',
795        type=argparse.FileType('r'),
796        required=True,
797    )
798
799    parser.add_argument(
800        '--virtualenv-gn-out-dir',
801        help=(
802            'Output directory to use when building and installing Python '
803            'packages with GN; defaults to a unique path in the environment '
804            'directory.'
805        ),
806    )
807
808    parser.add_argument('--json-file', help=argparse.SUPPRESS, default=None)
809
810    parser.add_argument(
811        '--use-existing-cipd',
812        help='Use cipd executable from the environment instead of fetching it.',
813        action='store_true',
814    )
815
816    parser.add_argument(
817        '--strict',
818        help='Fail if there are any warnings.',
819        action='store_true',
820    )
821
822    parser.add_argument(
823        '--unpin-pip-packages',
824        dest='use_pinned_pip_packages',
825        help='Do not use pins of pip packages.',
826        action='store_false',
827    )
828
829    parser.add_argument(
830        '--cipd-only',
831        help='Skip non-CIPD steps.',
832        action='store_true',
833    )
834
835    parser.add_argument(
836        '--skip-submodule-check',
837        help='Skip checking for submodule presence.',
838        dest='check_submodules',
839        action='store_false',
840    )
841
842    args = parser.parse_args(argv)
843
844    return args
845
846
847def main():
848    try:
849        return EnvSetup(**vars(parse())).setup()
850    except subprocess.CalledProcessError as err:
851        print()
852        print(err.output)
853        raise
854
855
856if __name__ == '__main__':
857    sys.exit(main())
858