• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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