3 # Copyright 2017 The Glslang Authors. All rights reserved.
4 # Copyright (c) 2018 Valve Corporation
5 # Copyright (c) 2018-2021 LunarG, Inc.
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
11 # http://www.apache.org/licenses/LICENSE-2.0
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.
19 # This script was heavily leveraged from KhronosGroup/glslang
20 # update_glslang_sources.py.
23 Get and build dependent repositories using known-good commits.
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.
37 This program can be used with Python 2.7 and Python 3.
39 Known-Good JSON Database
40 ------------------------
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.
49 See the help text (update_deps.py --help) for a complete list of options.
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.
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.
67 $ ../scripts/update_deps.py
69 or, to do the same thing, but using the --dir option:
73 $ scripts/update_deps.py --dir=build
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.
80 Note that the "dir" option can also specify an absolute path:
83 $ scripts/update_deps.py --dir=/tmp/deps
85 The "top" dir is then /tmp/deps (Linux filesystem example) and is
86 where this program will clone and build the dependent repositories.
88 Helper CMake Config File
89 ------------------------
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
97 A complete sequence might look like:
99 $ git clone git@github.com:My-Group/My-Repo.git
103 $ ../scripts/update_deps.py
104 $ cmake -C helper.cmake ..
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.
117 The name of the dependent repository. This field can be referenced
118 by the "deps.repo_name" structure to record a dependency.
122 Specifies the URL of the repository.
123 Example: https://github.com/KhronosGroup/Vulkan-Loader.git
127 The directory where the program clones the repository, relative to
132 The directory used to build the repository, relative to the "top"
137 The directory used to store the installed build artifacts, relative
138 to the "top" directory.
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.
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:
155 "var_name" : "VULKAN_HEADERS_INSTALL_DIR",
156 "repo_name" : "Vulkan-Headers"
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
164 Note that the "repo_name" element must match the "name" element of some
165 other repository in the JSON file.
167 - prebuild (optional)
168 - prebuild_linux (optional) (For Linux and MacOS)
169 - prebuild_windows (optional)
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
176 The commands listed in "prebuild" are executed first, and then the
177 commands for the specific platform are executed.
179 - custom_build (optional)
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
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").
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.
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.
200 - cmake_options (optional)
202 A list of options to pass to CMake during the generation phase.
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.
215 - build_step (optional)
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
221 - build_platforms (optional)
223 A list of platforms the repository will be built on.
224 Legal options include:
229 Builds on all platforms by default.
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.
241 from __future__ import print_function
249 import multiprocessing
255 KNOWN_GOOD_FILE_NAME = 'known_good.json'
259 'release': 'Release',
260 'relwithdebinfo': 'RelWithDebInfo',
261 'minsizerel': 'MinSizeRel'
266 DEVNULL = open(os.devnull, 'wb')
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.
274 os.chmod( path, stat.S_IWRITE )
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):
283 def command_output(cmd, directory, fail_ok=False):
284 """Runs a command in a directory and returns its standard output stream.
286 Captures the standard error stream and prints it if error.
288 Raises a RuntimeError if the command fails to launch or otherwise fails.
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))
298 raise RuntimeError('Failed to run {} in {}'.format(cmd, directory))
304 return path.replace('\\', '/')
306 class GoodRepo(object):
307 """Represents a repository at a known-good commit."""
309 def __init__(self, json, args):
310 """Initializes this good repo object.
313 'json': A fully populated JSON object describing the repo.
314 'args': Results from ArgumentParser
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)
347 self.build_dir = os.path.join(dir_top, self.build_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
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)
360 command_output(['git', 'clone', self.url, '.'], self.repo_dir)
361 # If we get here, we didn't raise an error
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)
374 # If we get here, we've exhausted our retries.
375 print("Failed to clone {} on all retries.".format(self.url))
378 def Fetch(self, retries=10, retry_seconds=60):
379 for retry in range(retries):
381 command_output(['git', 'fetch', 'origin'], self.repo_dir)
382 # if we get here, we didn't raise an error, and we're done
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)
392 # If we get here, we've exhausted our retries.
393 print("Failed to fetch {} on all retries.".format(self.url))
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')):
404 if len(self._args.ref):
405 command_output(['git', 'checkout', self._args.ref], self.repo_dir)
407 command_output(['git', 'checkout', self.commit], self.repo_dir)
408 print(command_output(['git', 'status'], self.repo_dir))
410 def CustomPreProcess(self, cmd_str, repo_dict):
411 return cmd_str.format(repo_dict, self._args, CONFIG_MAP[self._args.config])
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)
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)
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)
437 # Create and change to build directory
438 make_or_exist_dirs(self.build_dir)
439 os.chdir(self.build_dir)
442 'cmake', self.repo_dir,
443 '-DCMAKE_INSTALL_PREFIX=' + self.install_dir
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.
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))
456 # Add any CMake options
457 for option in self.cmake_options:
458 cmake_cmd.append(escape(option.format(**self.__dict__)))
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]))
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')
472 cmake_cmd.append('-A')
473 cmake_cmd.append('Win32')
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])
482 print("CMake command: " + " ".join(cmake_cmd))
484 ret_code = subprocess.call(cmake_cmd)
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')
494 if platform.system() == 'Windows':
495 cmake_cmd.append('--config')
496 cmake_cmd.append(CONFIG_MAP[self._args.config])
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:
505 num_make_jobs = min(num_make_jobs, int(env_make_jobs))
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')
515 print("CMake command: " + " ".join(cmake_cmd))
517 ret_code = subprocess.call(cmake_cmd)
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))
527 # Run any prebuild commands
530 if self.build_step == 'custom':
531 self.CustomBuild(repo_dict)
534 # Build and execute CMake command for creating build files
535 self.CMakeConfig(repos)
537 # Build and execute CMake command for the build
540 def IsOptional(self, opts):
541 if len(self.optional.intersection(opts)) > 0: return True
544 def GetGoodRepos(args):
545 """Returns the latest list of GoodRepo objects.
547 The known-good file is expected to be in the same
548 directory as this script unless overridden by the 'known_good_dir'
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)
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:
560 for repo in json.loads(known_good.read())['repos']
564 def GetInstallNames(args):
565 """Returns the install names list.
567 The known-good file is expected to be in the same
568 directory as this script unless overridden by the 'known_good_dir'
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)
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']
585 def CreateHelper(args, repos, filename):
586 """Create a CMake config helper file.
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.
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.
596 install_names = GetInstallNames(args)
597 with open(filename, 'w') as helper_file:
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'
602 var=install_names[repo.name],
603 dir=escape(repo.install_dir)))
607 parser = argparse.ArgumentParser(
608 description='Get and build dependent repos at known-good commits')
611 dest='known_good_dir',
612 help="Specify directory for known_good.json file.")
617 help="Set target directory for repository roots. Default is \'.\'.")
622 help="Override 'commit' with git reference. E.g., 'origin/main'")
626 action='store_false',
628 "Clone/update repositories and generate build files without performing compilation",
634 help="Clean files generated by compiler and linker before building",
638 dest='do_clean_repo',
640 help="Delete repository directory before building",
644 dest='do_clean_build',
646 help="Delete build directory before building",
650 dest='do_clean_install',
652 help="Delete install directory before building",
655 '--skip-existing-install',
656 dest='skip_existing_install',
658 help="Skip build if install directory exists",
663 choices=['32', '64', 'x86', 'x64', 'win32', 'win64'],
665 help="Set build files architecture (Windows)",
670 choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'],
672 help="Set build files configuration",
677 help="Set the CMake generator",
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'",
686 args = parser.parse_args()
687 save_cwd = os.getcwd()
689 # Create working "top" directory if needed
690 make_or_exist_dirs(args.dir)
691 abs_top_dir = os.path.abspath(args.dir)
693 repos = GetGoodRepos(args)
696 print('Starting builds in {d}'.format(d=abs_top_dir))
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:
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
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))
711 # Skip test-only repos if the --tests option was not passed in
712 if repo.IsOptional(args.optional):
731 repo_dict[repo.name] = {field: getattr(repo, field) for field in field_list}
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):
737 for env in repo.ci_only:
738 if not env in os.environ:
740 if os.environ[env].lower() == 'true':
746 # Clone/update the repository
749 # Build the repository
750 if args.do_build and repo.build_step != 'skip':
751 repo.Build(repos, repo_dict)
753 # Need to restore original cwd in order for CreateHelper to find json file
755 CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake'))
760 if __name__ == '__main__':