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