1#!/usr/bin/env python 2 3# Copyright 2017 The Glslang Authors. All rights reserved. 4# Copyright (c) 2018 Valve Corporation 5# Copyright (c) 2018-2021 LunarG, Inc. 6# 7# Licensed under the Apache License, Version 2.0 (the "License"); 8# you may not use this file except in compliance with the License. 9# You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, software 14# distributed under the License is distributed on an "AS IS" BASIS, 15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16# See the License for the specific language governing permissions and 17# limitations under the License. 18 19# This script was heavily leveraged from KhronosGroup/glslang 20# update_glslang_sources.py. 21"""update_deps.py 22 23Get and build dependent repositories using known-good commits. 24 25Purpose 26------- 27 28This program is intended to assist a developer of this repository 29(the "home" repository) by gathering and building the repositories that 30this home repository depend on. It also checks out each dependent 31repository at a "known-good" commit in order to provide stability in 32the dependent repositories. 33 34Python Compatibility 35-------------------- 36 37This program can be used with Python 2.7 and Python 3. 38 39Known-Good JSON Database 40------------------------ 41 42This program expects to find a file named "known-good.json" in the 43same directory as the program file. This JSON file is tailored for 44the needs of the home repository by including its dependent repositories. 45 46Program Options 47--------------- 48 49See the help text (update_deps.py --help) for a complete list of options. 50 51Program Operation 52----------------- 53 54The program uses the user's current directory at the time of program 55invocation as the location for fetching and building the dependent 56repositories. The user can override this by using the "--dir" option. 57 58For example, a directory named "build" in the repository's root directory 59is a good place to put the dependent repositories because that directory 60is not tracked by Git. (See the .gitignore file.) The "external" directory 61may also be a suitable location. 62A user can issue: 63 64$ cd My-Repo 65$ mkdir build 66$ cd build 67$ ../scripts/update_deps.py 68 69or, to do the same thing, but using the --dir option: 70 71$ cd My-Repo 72$ mkdir build 73$ scripts/update_deps.py --dir=build 74 75With these commands, the "build" directory is considered the "top" 76directory where the program clones the dependent repositories. The 77JSON file configures the build and install working directories to be 78within this "top" directory. 79 80Note that the "dir" option can also specify an absolute path: 81 82$ cd My-Repo 83$ scripts/update_deps.py --dir=/tmp/deps 84 85The "top" dir is then /tmp/deps (Linux filesystem example) and is 86where this program will clone and build the dependent repositories. 87 88Helper CMake Config File 89------------------------ 90 91When the program finishes building the dependencies, it writes a file 92named "helper.cmake" to the "top" directory that contains CMake commands 93for setting CMake variables for locating the dependent repositories. 94This helper file can be used to set up the CMake build files for this 95"home" repository. 96 97A complete sequence might look like: 98 99$ git clone git@github.com:My-Group/My-Repo.git 100$ cd My-Repo 101$ mkdir build 102$ cd build 103$ ../scripts/update_deps.py 104$ cmake -C helper.cmake .. 105$ cmake --build . 106 107JSON File Schema 108---------------- 109 110There's no formal schema for the "known-good" JSON file, but here is 111a description of its elements. All elements are required except those 112marked as optional. Please see the "known_good.json" file for 113examples of all of these elements. 114 115- name 116 117The name of the dependent repository. This field can be referenced 118by the "deps.repo_name" structure to record a dependency. 119 120- url 121 122Specifies the URL of the repository. 123Example: https://github.com/KhronosGroup/Vulkan-Loader.git 124 125- sub_dir 126 127The directory where the program clones the repository, relative to 128the "top" directory. 129 130- build_dir 131 132The directory used to build the repository, relative to the "top" 133directory. 134 135- install_dir 136 137The directory used to store the installed build artifacts, relative 138to the "top" directory. 139 140- commit 141 142The commit used to checkout the repository. This can be a SHA-1 143object name or a refname used with the remote name "origin". 144For example, this field can be set to "origin/sdk-1.1.77" to 145select the end of the sdk-1.1.77 branch. 146 147- deps (optional) 148 149An array of pairs consisting of a CMake variable name and a 150repository name to specify a dependent repo and a "link" to 151that repo's install artifacts. For example: 152 153"deps" : [ 154 { 155 "var_name" : "VULKAN_HEADERS_INSTALL_DIR", 156 "repo_name" : "Vulkan-Headers" 157 } 158] 159 160which represents that this repository depends on the Vulkan-Headers 161repository and uses the VULKAN_HEADERS_INSTALL_DIR CMake variable to 162specify the location where it expects to find the Vulkan-Headers install 163directory. 164Note that the "repo_name" element must match the "name" element of some 165other repository in the JSON file. 166 167- prebuild (optional) 168- prebuild_linux (optional) (For Linux and MacOS) 169- prebuild_windows (optional) 170 171A list of commands to execute before building a dependent repository. 172This is useful for repositories that require the execution of some 173sort of "update" script or need to clone an auxillary repository like 174googletest. 175 176The commands listed in "prebuild" are executed first, and then the 177commands for the specific platform are executed. 178 179- custom_build (optional) 180 181A list of commands to execute as a custom build instead of using 182the built in CMake way of building. Requires "build_step" to be 183set to "custom" 184 185You can insert the following keywords into the commands listed in 186"custom_build" if they require runtime information (like whether the 187build config is "Debug" or "Release"). 188 189Keywords: 190{0} reference to a dictionary of repos and their attributes 191{1} reference to the command line arguments set before start 192{2} reference to the CONFIG_MAP value of config. 193 194Example: 195{2} returns the CONFIG_MAP value of config e.g. debug -> Debug 196{1}.config returns the config variable set when you ran update_dep.py 197{0}[Vulkan-Headers][repo_root] returns the repo_root variable from 198 the Vulkan-Headers GoodRepo object. 199 200- cmake_options (optional) 201 202A list of options to pass to CMake during the generation phase. 203 204- ci_only (optional) 205 206A list of environment variables where one must be set to "true" 207(case-insensitive) in order for this repo to be fetched and built. 208This list can be used to specify repos that should be built only in CI. 209Typically, this list might contain "TRAVIS" and/or "APPVEYOR" because 210each of these CI systems sets an environment variable with its own 211name to "true". Note that this could also be (ab)used to control 212the processing of the repo with any environment variable. The default 213is an empty list, which means that the repo is always processed. 214 215- build_step (optional) 216 217Specifies if the dependent repository should be built or not. This can 218have a value of 'build', 'custom', or 'skip'. The dependent repositories are 219built by default. 220 221- build_platforms (optional) 222 223A list of platforms the repository will be built on. 224Legal options include: 225"windows" 226"linux" 227"darwin" 228 229Builds on all platforms by default. 230 231Note 232---- 233 234The "sub_dir", "build_dir", and "install_dir" elements are all relative 235to the effective "top" directory. Specifying absolute paths is not 236supported. However, the "top" directory specified with the "--dir" 237option can be a relative or absolute path. 238 239""" 240 241from __future__ import print_function 242 243import argparse 244import json 245import os.path 246import subprocess 247import sys 248import platform 249import multiprocessing 250import shlex 251import shutil 252import stat 253import time 254 255KNOWN_GOOD_FILE_NAME = 'known_good.json' 256 257CONFIG_MAP = { 258 'debug': 'Debug', 259 'release': 'Release', 260 'relwithdebinfo': 'RelWithDebInfo', 261 'minsizerel': 'MinSizeRel' 262} 263 264VERBOSE = False 265 266DEVNULL = open(os.devnull, 'wb') 267 268 269def on_rm_error( func, path, exc_info): 270 """Error handler for recursively removing a directory. The 271 shutil.rmtree function can fail on Windows due to read-only files. 272 This handler will change the permissions for tha file and continue. 273 """ 274 os.chmod( path, stat.S_IWRITE ) 275 os.unlink( path ) 276 277def make_or_exist_dirs(path): 278 "Wrapper for os.makedirs that tolerates the directory already existing" 279 # Could use os.makedirs(path, exist_ok=True) if we drop python2 280 if not os.path.isdir(path): 281 os.makedirs(path) 282 283def command_output(cmd, directory, fail_ok=False): 284 """Runs a command in a directory and returns its standard output stream. 285 286 Captures the standard error stream and prints it if error. 287 288 Raises a RuntimeError if the command fails to launch or otherwise fails. 289 """ 290 if VERBOSE: 291 print('In {d}: {cmd}'.format(d=directory, cmd=cmd)) 292 p = subprocess.Popen( 293 cmd, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 294 (stdout, stderr) = p.communicate() 295 if p.returncode != 0: 296 print('*** Error ***\nstderr contents:\n{}'.format(stderr)) 297 if not fail_ok: 298 raise RuntimeError('Failed to run {} in {}'.format(cmd, directory)) 299 if VERBOSE: 300 print(stdout) 301 return stdout 302 303def escape(path): 304 return path.replace('\\', '/') 305 306class GoodRepo(object): 307 """Represents a repository at a known-good commit.""" 308 309 def __init__(self, json, args): 310 """Initializes this good repo object. 311 312 Args: 313 'json': A fully populated JSON object describing the repo. 314 'args': Results from ArgumentParser 315 """ 316 self._json = json 317 self._args = args 318 # Required JSON elements 319 self.name = json['name'] 320 self.url = json['url'] 321 self.sub_dir = json['sub_dir'] 322 self.commit = json['commit'] 323 # Optional JSON elements 324 self.build_dir = None 325 self.install_dir = None 326 if json.get('build_dir'): 327 self.build_dir = os.path.normpath(json['build_dir']) 328 if json.get('install_dir'): 329 self.install_dir = os.path.normpath(json['install_dir']) 330 self.deps = json['deps'] if ('deps' in json) else [] 331 self.prebuild = json['prebuild'] if ('prebuild' in json) else [] 332 self.prebuild_linux = json['prebuild_linux'] if ( 333 'prebuild_linux' in json) else [] 334 self.prebuild_windows = json['prebuild_windows'] if ( 335 'prebuild_windows' in json) else [] 336 self.custom_build = json['custom_build'] if ('custom_build' in json) else [] 337 self.cmake_options = json['cmake_options'] if ( 338 'cmake_options' in json) else [] 339 self.ci_only = json['ci_only'] if ('ci_only' in json) else [] 340 self.build_step = json['build_step'] if ('build_step' in json) else 'build' 341 self.build_platforms = json['build_platforms'] if ('build_platforms' in json) else [] 342 self.optional = set(json.get('optional', [])) 343 # Absolute paths for a repo's directories 344 dir_top = os.path.abspath(args.dir) 345 self.repo_dir = os.path.join(dir_top, self.sub_dir) 346 if self.build_dir: 347 self.build_dir = os.path.join(dir_top, self.build_dir) 348 if self.install_dir: 349 self.install_dir = os.path.join(dir_top, self.install_dir) 350 # Check if platform is one to build on 351 self.on_build_platform = False 352 if self.build_platforms == [] or platform.system().lower() in self.build_platforms: 353 self.on_build_platform = True 354 355 def Clone(self, retries=10, retry_seconds=60): 356 print('Cloning {n} into {d}'.format(n=self.name, d=self.repo_dir)) 357 for retry in range(retries): 358 make_or_exist_dirs(self.repo_dir) 359 try: 360 command_output(['git', 'clone', self.url, '.'], self.repo_dir) 361 # If we get here, we didn't raise an error 362 return 363 except RuntimeError as e: 364 print("Error cloning on iteration {}/{}: {}".format(retry + 1, retries, e)) 365 if retry + 1 < retries: 366 if retry_seconds > 0: 367 print("Waiting {} seconds before trying again".format(retry_seconds)) 368 time.sleep(retry_seconds) 369 if os.path.isdir(self.repo_dir): 370 print("Removing old tree {}".format(self.repo_dir)) 371 shutil.rmtree(self.repo_dir, onerror=on_rm_error) 372 continue 373 374 # If we get here, we've exhausted our retries. 375 print("Failed to clone {} on all retries.".format(self.url)) 376 raise e 377 378 def Fetch(self, retries=10, retry_seconds=60): 379 for retry in range(retries): 380 try: 381 command_output(['git', 'fetch', 'origin'], self.repo_dir) 382 # if we get here, we didn't raise an error, and we're done 383 return 384 except RuntimeError as e: 385 print("Error fetching on iteration {}/{}: {}".format(retry + 1, retries, e)) 386 if retry + 1 < retries: 387 if retry_seconds > 0: 388 print("Waiting {} seconds before trying again".format(retry_seconds)) 389 time.sleep(retry_seconds) 390 continue 391 392 # If we get here, we've exhausted our retries. 393 print("Failed to fetch {} on all retries.".format(self.url)) 394 raise e 395 396 def Checkout(self): 397 print('Checking out {n} in {d}'.format(n=self.name, d=self.repo_dir)) 398 if self._args.do_clean_repo: 399 if os.path.isdir(self.repo_dir): 400 shutil.rmtree(self.repo_dir, onerror = on_rm_error) 401 if not os.path.exists(os.path.join(self.repo_dir, '.git')): 402 self.Clone() 403 self.Fetch() 404 if len(self._args.ref): 405 command_output(['git', 'checkout', self._args.ref], self.repo_dir) 406 else: 407 command_output(['git', 'checkout', self.commit], self.repo_dir) 408 print(command_output(['git', 'status'], self.repo_dir)) 409 410 def CustomPreProcess(self, cmd_str, repo_dict): 411 return cmd_str.format(repo_dict, self._args, CONFIG_MAP[self._args.config]) 412 413 def PreBuild(self): 414 """Execute any prebuild steps from the repo root""" 415 for p in self.prebuild: 416 command_output(shlex.split(p), self.repo_dir) 417 if platform.system() == 'Linux' or platform.system() == 'Darwin': 418 for p in self.prebuild_linux: 419 command_output(shlex.split(p), self.repo_dir) 420 if platform.system() == 'Windows': 421 for p in self.prebuild_windows: 422 command_output(shlex.split(p), self.repo_dir) 423 424 def CustomBuild(self, repo_dict): 425 """Execute any custom_build steps from the repo root""" 426 for p in self.custom_build: 427 cmd = self.CustomPreProcess(p, repo_dict) 428 command_output(shlex.split(cmd), self.repo_dir) 429 430 def CMakeConfig(self, repos): 431 """Build CMake command for the configuration phase and execute it""" 432 if self._args.do_clean_build: 433 shutil.rmtree(self.build_dir) 434 if self._args.do_clean_install: 435 shutil.rmtree(self.install_dir) 436 437 # Create and change to build directory 438 make_or_exist_dirs(self.build_dir) 439 os.chdir(self.build_dir) 440 441 cmake_cmd = [ 442 'cmake', self.repo_dir, 443 '-DCMAKE_INSTALL_PREFIX=' + self.install_dir 444 ] 445 446 # For each repo this repo depends on, generate a CMake variable 447 # definitions for "...INSTALL_DIR" that points to that dependent 448 # repo's install dir. 449 for d in self.deps: 450 dep_commit = [r for r in repos if r.name == d['repo_name']] 451 if len(dep_commit) and dep_commit[0].on_build_platform: 452 cmake_cmd.append('-D{var_name}={install_dir}'.format( 453 var_name=d['var_name'], 454 install_dir=dep_commit[0].install_dir)) 455 456 # Add any CMake options 457 for option in self.cmake_options: 458 cmake_cmd.append(escape(option.format(**self.__dict__))) 459 460 # Set build config for single-configuration generators 461 if platform.system() == 'Linux' or platform.system() == 'Darwin': 462 cmake_cmd.append('-DCMAKE_BUILD_TYPE={config}'.format( 463 config=CONFIG_MAP[self._args.config])) 464 465 # Use the CMake -A option to select the platform architecture 466 # without needing a Visual Studio generator. 467 if platform.system() == 'Windows' and self._args.generator != "Ninja": 468 if self._args.arch.lower() == '64' or self._args.arch == 'x64' or self._args.arch == 'win64': 469 cmake_cmd.append('-A') 470 cmake_cmd.append('x64') 471 else: 472 cmake_cmd.append('-A') 473 cmake_cmd.append('Win32') 474 475 # Apply a generator, if one is specified. This can be used to supply 476 # a specific generator for the dependent repositories to match 477 # that of the main repository. 478 if self._args.generator is not None: 479 cmake_cmd.extend(['-G', self._args.generator]) 480 481 if VERBOSE: 482 print("CMake command: " + " ".join(cmake_cmd)) 483 484 ret_code = subprocess.call(cmake_cmd) 485 if ret_code != 0: 486 sys.exit(ret_code) 487 488 def CMakeBuild(self): 489 """Build CMake command for the build phase and execute it""" 490 cmake_cmd = ['cmake', '--build', self.build_dir, '--target', 'install'] 491 if self._args.do_clean: 492 cmake_cmd.append('--clean-first') 493 494 if platform.system() == 'Windows': 495 cmake_cmd.append('--config') 496 cmake_cmd.append(CONFIG_MAP[self._args.config]) 497 498 # Speed up the build. 499 if platform.system() == 'Linux' or platform.system() == 'Darwin': 500 cmake_cmd.append('--') 501 num_make_jobs = multiprocessing.cpu_count() 502 env_make_jobs = os.environ.get('MAKE_JOBS', None) 503 if env_make_jobs is not None: 504 try: 505 num_make_jobs = min(num_make_jobs, int(env_make_jobs)) 506 except ValueError: 507 print('warning: environment variable MAKE_JOBS has non-numeric value "{}". ' 508 'Using {} (CPU count) instead.'.format(env_make_jobs, num_make_jobs)) 509 cmake_cmd.append('-j{}'.format(num_make_jobs)) 510 if platform.system() == 'Windows' and self._args.generator != "Ninja": 511 cmake_cmd.append('--') 512 cmake_cmd.append('/maxcpucount') 513 514 if VERBOSE: 515 print("CMake command: " + " ".join(cmake_cmd)) 516 517 ret_code = subprocess.call(cmake_cmd) 518 if ret_code != 0: 519 sys.exit(ret_code) 520 521 def Build(self, repos, repo_dict): 522 """Build the dependent repo""" 523 print('Building {n} in {d}'.format(n=self.name, d=self.repo_dir)) 524 print('Build dir = {b}'.format(b=self.build_dir)) 525 print('Install dir = {i}\n'.format(i=self.install_dir)) 526 527 # Run any prebuild commands 528 self.PreBuild() 529 530 if self.build_step == 'custom': 531 self.CustomBuild(repo_dict) 532 return 533 534 # Build and execute CMake command for creating build files 535 self.CMakeConfig(repos) 536 537 # Build and execute CMake command for the build 538 self.CMakeBuild() 539 540 def IsOptional(self, opts): 541 if len(self.optional.intersection(opts)) > 0: return True 542 else: return False 543 544def GetGoodRepos(args): 545 """Returns the latest list of GoodRepo objects. 546 547 The known-good file is expected to be in the same 548 directory as this script unless overridden by the 'known_good_dir' 549 parameter. 550 """ 551 if args.known_good_dir: 552 known_good_file = os.path.join( os.path.abspath(args.known_good_dir), 553 KNOWN_GOOD_FILE_NAME) 554 else: 555 known_good_file = os.path.join( 556 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME) 557 with open(known_good_file) as known_good: 558 return [ 559 GoodRepo(repo, args) 560 for repo in json.loads(known_good.read())['repos'] 561 ] 562 563 564def GetInstallNames(args): 565 """Returns the install names list. 566 567 The known-good file is expected to be in the same 568 directory as this script unless overridden by the 'known_good_dir' 569 parameter. 570 """ 571 if args.known_good_dir: 572 known_good_file = os.path.join(os.path.abspath(args.known_good_dir), 573 KNOWN_GOOD_FILE_NAME) 574 else: 575 known_good_file = os.path.join( 576 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME) 577 with open(known_good_file) as known_good: 578 install_info = json.loads(known_good.read()) 579 if install_info.get('install_names'): 580 return install_info['install_names'] 581 else: 582 return None 583 584 585def CreateHelper(args, repos, filename): 586 """Create a CMake config helper file. 587 588 The helper file is intended to be used with 'cmake -C <file>' 589 to build this home repo using the dependencies built by this script. 590 591 The install_names dictionary represents the CMake variables used by the 592 home repo to locate the install dirs of the dependent repos. 593 This information is baked into the CMake files of the home repo and so 594 this dictionary is kept with the repo via the json file. 595 """ 596 install_names = GetInstallNames(args) 597 with open(filename, 'w') as helper_file: 598 for repo in repos: 599 if install_names and repo.name in install_names and repo.on_build_platform: 600 helper_file.write('set({var} "{dir}" CACHE STRING "" FORCE)\n' 601 .format( 602 var=install_names[repo.name], 603 dir=escape(repo.install_dir))) 604 605 606def main(): 607 parser = argparse.ArgumentParser( 608 description='Get and build dependent repos at known-good commits') 609 parser.add_argument( 610 '--known_good_dir', 611 dest='known_good_dir', 612 help="Specify directory for known_good.json file.") 613 parser.add_argument( 614 '--dir', 615 dest='dir', 616 default='.', 617 help="Set target directory for repository roots. Default is \'.\'.") 618 parser.add_argument( 619 '--ref', 620 dest='ref', 621 default='', 622 help="Override 'commit' with git reference. E.g., 'origin/master'") 623 parser.add_argument( 624 '--no-build', 625 dest='do_build', 626 action='store_false', 627 help= 628 "Clone/update repositories and generate build files without performing compilation", 629 default=True) 630 parser.add_argument( 631 '--clean', 632 dest='do_clean', 633 action='store_true', 634 help="Clean files generated by compiler and linker before building", 635 default=False) 636 parser.add_argument( 637 '--clean-repo', 638 dest='do_clean_repo', 639 action='store_true', 640 help="Delete repository directory before building", 641 default=False) 642 parser.add_argument( 643 '--clean-build', 644 dest='do_clean_build', 645 action='store_true', 646 help="Delete build directory before building", 647 default=False) 648 parser.add_argument( 649 '--clean-install', 650 dest='do_clean_install', 651 action='store_true', 652 help="Delete install directory before building", 653 default=False) 654 parser.add_argument( 655 '--arch', 656 dest='arch', 657 choices=['32', '64', 'x86', 'x64', 'win32', 'win64'], 658 type=str.lower, 659 help="Set build files architecture (Windows)", 660 default='64') 661 parser.add_argument( 662 '--config', 663 dest='config', 664 choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'], 665 type=str.lower, 666 help="Set build files configuration", 667 default='debug') 668 parser.add_argument( 669 '--generator', 670 dest='generator', 671 help="Set the CMake generator", 672 default=None) 673 parser.add_argument( 674 '--optional', 675 dest='optional', 676 type=lambda a: set(a.lower().split(',')), 677 help="Comma-separated list of 'optional' resources that may be skipped. Only 'tests' is currently supported as 'optional'", 678 default=set()) 679 680 args = parser.parse_args() 681 save_cwd = os.getcwd() 682 683 # Create working "top" directory if needed 684 make_or_exist_dirs(args.dir) 685 abs_top_dir = os.path.abspath(args.dir) 686 687 repos = GetGoodRepos(args) 688 repo_dict = {} 689 690 print('Starting builds in {d}'.format(d=abs_top_dir)) 691 for repo in repos: 692 # If the repo has a platform whitelist, skip the repo 693 # unless we are building on a whitelisted platform. 694 if not repo.on_build_platform: 695 continue 696 697 # Skip test-only repos if the --tests option was not passed in 698 if repo.IsOptional(args.optional): 699 continue 700 701 field_list = ('url', 702 'sub_dir', 703 'commit', 704 'build_dir', 705 'install_dir', 706 'deps', 707 'prebuild', 708 'prebuild_linux', 709 'prebuild_windows', 710 'custom_build', 711 'cmake_options', 712 'ci_only', 713 'build_step', 714 'build_platforms', 715 'repo_dir', 716 'on_build_platform') 717 repo_dict[repo.name] = {field: getattr(repo, field) for field in field_list} 718 719 # If the repo has a CI whitelist, skip the repo unless 720 # one of the CI's environment variable is set to true. 721 if len(repo.ci_only): 722 do_build = False 723 for env in repo.ci_only: 724 if not env in os.environ: 725 continue 726 if os.environ[env].lower() == 'true': 727 do_build = True 728 break 729 if not do_build: 730 continue 731 732 # Clone/update the repository 733 repo.Checkout() 734 735 # Build the repository 736 if args.do_build and repo.build_step != 'skip': 737 repo.Build(repos, repo_dict) 738 739 # Need to restore original cwd in order for CreateHelper to find json file 740 os.chdir(save_cwd) 741 CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake')) 742 743 sys.exit(0) 744 745 746if __name__ == '__main__': 747 main() 748