ff09871b225ebb305492e546a1ded78ab16ffd30
[platform/upstream/Vulkan-Tools.git] / scripts / update_deps.py
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
23 Get and build dependent repositories using known-good commits.
24
25 Purpose
26 -------
27
28 This program is intended to assist a developer of this repository
29 (the "home" repository) by gathering and building the repositories that
30 this home repository depend on.  It also checks out each dependent
31 repository at a "known-good" commit in order to provide stability in
32 the dependent repositories.
33
34 Python Compatibility
35 --------------------
36
37 This program can be used with Python 2.7 and Python 3.
38
39 Known-Good JSON Database
40 ------------------------
41
42 This program expects to find a file named "known-good.json" in the
43 same directory as the program file.  This JSON file is tailored for
44 the needs of the home repository by including its dependent repositories.
45
46 Program Options
47 ---------------
48
49 See the help text (update_deps.py --help) for a complete list of options.
50
51 Program Operation
52 -----------------
53
54 The program uses the user's current directory at the time of program
55 invocation as the location for fetching and building the dependent
56 repositories.  The user can override this by using the "--dir" option.
57
58 For example, a directory named "build" in the repository's root directory
59 is a good place to put the dependent repositories because that directory
60 is not tracked by Git. (See the .gitignore file.)  The "external" directory
61 may also be a suitable location.
62 A user can issue:
63
64 $ cd My-Repo
65 $ mkdir build
66 $ cd build
67 $ ../scripts/update_deps.py
68
69 or, 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
75 With these commands, the "build" directory is considered the "top"
76 directory where the program clones the dependent repositories.  The
77 JSON file configures the build and install working directories to be
78 within this "top" directory.
79
80 Note that the "dir" option can also specify an absolute path:
81
82 $ cd My-Repo
83 $ scripts/update_deps.py --dir=/tmp/deps
84
85 The "top" dir is then /tmp/deps (Linux filesystem example) and is
86 where this program will clone and build the dependent repositories.
87
88 Helper CMake Config File
89 ------------------------
90
91 When the program finishes building the dependencies, it writes a file
92 named "helper.cmake" to the "top" directory that contains CMake commands
93 for setting CMake variables for locating the dependent repositories.
94 This helper file can be used to set up the CMake build files for this
95 "home" repository.
96
97 A 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
107 JSON File Schema
108 ----------------
109
110 There's no formal schema for the "known-good" JSON file, but here is
111 a description of its elements.  All elements are required except those
112 marked as optional.  Please see the "known_good.json" file for
113 examples of all of these elements.
114
115 - name
116
117 The name of the dependent repository.  This field can be referenced
118 by the "deps.repo_name" structure to record a dependency.
119
120 - url
121
122 Specifies the URL of the repository.
123 Example: https://github.com/KhronosGroup/Vulkan-Loader.git
124
125 - sub_dir
126
127 The directory where the program clones the repository, relative to
128 the "top" directory.
129
130 - build_dir
131
132 The directory used to build the repository, relative to the "top"
133 directory.
134
135 - install_dir
136
137 The directory used to store the installed build artifacts, relative
138 to the "top" directory.
139
140 - commit
141
142 The commit used to checkout the repository.  This can be a SHA-1
143 object name or a refname used with the remote name "origin".
144 For example, this field can be set to "origin/sdk-1.1.77" to
145 select the end of the sdk-1.1.77 branch.
146
147 - deps (optional)
148
149 An array of pairs consisting of a CMake variable name and a
150 repository name to specify a dependent repo and a "link" to
151 that 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
160 which represents that this repository depends on the Vulkan-Headers
161 repository and uses the VULKAN_HEADERS_INSTALL_DIR CMake variable to
162 specify the location where it expects to find the Vulkan-Headers install
163 directory.
164 Note that the "repo_name" element must match the "name" element of some
165 other repository in the JSON file.
166
167 - prebuild (optional)
168 - prebuild_linux (optional)  (For Linux and MacOS)
169 - prebuild_windows (optional)
170
171 A list of commands to execute before building a dependent repository.
172 This is useful for repositories that require the execution of some
173 sort of "update" script or need to clone an auxillary repository like
174 googletest.
175
176 The commands listed in "prebuild" are executed first, and then the
177 commands for the specific platform are executed.
178
179 - custom_build (optional)
180
181 A list of commands to execute as a custom build instead of using
182 the built in CMake way of building. Requires "build_step" to be
183 set to "custom"
184
185 You can insert the following keywords into the commands listed in
186 "custom_build" if they require runtime information (like whether the
187 build config is "Debug" or "Release").
188
189 Keywords:
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
194 Example:
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
202 A list of options to pass to CMake during the generation phase.
203
204 - ci_only (optional)
205
206 A list of environment variables where one must be set to "true"
207 (case-insensitive) in order for this repo to be fetched and built.
208 This list can be used to specify repos that should be built only in CI.
209 Typically, this list might contain "TRAVIS" and/or "APPVEYOR" because
210 each of these CI systems sets an environment variable with its own
211 name to "true".  Note that this could also be (ab)used to control
212 the processing of the repo with any environment variable.  The default
213 is an empty list, which means that the repo is always processed.
214
215 - build_step (optional)
216
217 Specifies if the dependent repository should be built or not. This can
218 have a value of 'build', 'custom',  or 'skip'. The dependent repositories are
219 built by default.
220
221 - build_platforms (optional)
222
223 A list of platforms the repository will be built on.
224 Legal options include:
225 "windows"
226 "linux"
227 "darwin"
228
229 Builds on all platforms by default.
230
231 Note
232 ----
233
234 The "sub_dir", "build_dir", and "install_dir" elements are all relative
235 to the effective "top" directory.  Specifying absolute paths is not
236 supported.  However, the "top" directory specified with the "--dir"
237 option can be a relative or absolute path.
238
239 """
240
241 from __future__ import print_function
242
243 import argparse
244 import json
245 import os.path
246 import subprocess
247 import sys
248 import platform
249 import multiprocessing
250 import shlex
251 import shutil
252 import stat
253 import time
254
255 KNOWN_GOOD_FILE_NAME = 'known_good.json'
256
257 CONFIG_MAP = {
258     'debug': 'Debug',
259     'release': 'Release',
260     'relwithdebinfo': 'RelWithDebInfo',
261     'minsizerel': 'MinSizeRel'
262 }
263
264 VERBOSE = False
265
266 DEVNULL = open(os.devnull, 'wb')
267
268
269 def 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
277 def 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
283 def 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
303 def escape(path):
304     return path.replace('\\', '/')
305
306 class 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
544 def 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
564 def 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
585 def 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
606 def 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/main'")
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         '--skip-existing-install',
656         dest='skip_existing_install',
657         action='store_true',
658         help="Skip build if install directory exists",
659         default=False)
660     parser.add_argument(
661         '--arch',
662         dest='arch',
663         choices=['32', '64', 'x86', 'x64', 'win32', 'win64'],
664         type=str.lower,
665         help="Set build files architecture (Windows)",
666         default='64')
667     parser.add_argument(
668         '--config',
669         dest='config',
670         choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'],
671         type=str.lower,
672         help="Set build files configuration",
673         default='debug')
674     parser.add_argument(
675         '--generator',
676         dest='generator',
677         help="Set the CMake generator",
678         default=None)
679     parser.add_argument(
680         '--optional',
681         dest='optional',
682         type=lambda a: set(a.lower().split(',')),
683         help="Comma-separated list of 'optional' resources that may be skipped. Only 'tests' is currently supported as 'optional'",
684         default=set())
685
686     args = parser.parse_args()
687     save_cwd = os.getcwd()
688
689     # Create working "top" directory if needed
690     make_or_exist_dirs(args.dir)
691     abs_top_dir = os.path.abspath(args.dir)
692
693     repos = GetGoodRepos(args)
694     repo_dict = {}
695
696     print('Starting builds in {d}'.format(d=abs_top_dir))
697     for repo in repos:
698         # If the repo has a platform whitelist, skip the repo
699         # unless we are building on a whitelisted platform.
700         if not repo.on_build_platform:
701             continue
702
703         # Skip building the repo if its install directory already exists
704         # and requested via an option.  This is useful for cases where the
705         # install directory is restored from a cache that is known to be up
706         # to date.
707         if args.skip_existing_install and os.path.isdir(repo.install_dir):
708             print('Skipping build for repo {n} due to existing install directory'.format(n=repo.name))
709             continue
710
711         # Skip test-only repos if the --tests option was not passed in
712         if repo.IsOptional(args.optional):
713             continue
714
715         field_list = ('url',
716                       'sub_dir',
717                       'commit',
718                       'build_dir',
719                       'install_dir',
720                       'deps',
721                       'prebuild',
722                       'prebuild_linux',
723                       'prebuild_windows',
724                       'custom_build',
725                       'cmake_options',
726                       'ci_only',
727                       'build_step',
728                       'build_platforms',
729                       'repo_dir',
730                       'on_build_platform')
731         repo_dict[repo.name] = {field: getattr(repo, field) for field in field_list}
732
733         # If the repo has a CI whitelist, skip the repo unless
734         # one of the CI's environment variable is set to true.
735         if len(repo.ci_only):
736             do_build = False
737             for env in repo.ci_only:
738                 if not env in os.environ:
739                     continue
740                 if os.environ[env].lower() == 'true':
741                     do_build = True
742                     break
743             if not do_build:
744                 continue
745
746         # Clone/update the repository
747         repo.Checkout()
748
749         # Build the repository
750         if args.do_build and repo.build_step != 'skip':
751             repo.Build(repos, repo_dict)
752
753     # Need to restore original cwd in order for CreateHelper to find json file
754     os.chdir(save_cwd)
755     CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake'))
756
757     sys.exit(0)
758
759
760 if __name__ == '__main__':
761     main()
762