| #!/usr/bin/env python | |
| # Copyright 2017 The Glslang Authors. All rights reserved. | |
| # Copyright (c) 2018 Valve Corporation | |
| # Copyright (c) 2018 LunarG, Inc. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| # This script was heavily leveraged from KhronosGroup/glslang | |
| # update_glslang_sources.py. | |
| """update_deps.py | |
| Get and build dependent repositories using known-good commits. | |
| Purpose | |
| ------- | |
| This program is intended to assist a developer of this repository | |
| (the "home" repository) by gathering and building the repositories that | |
| this home repository depend on. It also checks out each dependent | |
| repository at a "known-good" commit in order to provide stability in | |
| the dependent repositories. | |
| Python Compatibility | |
| -------------------- | |
| This program can be used with Python 2.7 and Python 3. | |
| Known-Good JSON Database | |
| ------------------------ | |
| This program expects to find a file named "known-good.json" in the | |
| same directory as the program file. This JSON file is tailored for | |
| the needs of the home repository by including its dependent repositories. | |
| Program Options | |
| --------------- | |
| See the help text (update_deps.py --help) for a complete list of options. | |
| Program Operation | |
| ----------------- | |
| The program uses the user's current directory at the time of program | |
| invocation as the location for fetching and building the dependent | |
| repositories. The user can override this by using the "--dir" option. | |
| For example, a directory named "build" in the repository's root directory | |
| is a good place to put the dependent repositories because that directory | |
| is not tracked by Git. (See the .gitignore file.) The "external" directory | |
| may also be a suitable location. | |
| A user can issue: | |
| $ cd My-Repo | |
| $ mkdir build | |
| $ cd build | |
| $ ../scripts/update_deps.py | |
| or, to do the same thing, but using the --dir option: | |
| $ cd My-Repo | |
| $ mkdir build | |
| $ scripts/update_deps.py --dir=build | |
| With these commands, the "build" directory is considered the "top" | |
| directory where the program clones the dependent repositories. The | |
| JSON file configures the build and install working directories to be | |
| within this "top" directory. | |
| Note that the "dir" option can also specify an absolute path: | |
| $ cd My-Repo | |
| $ scripts/update_deps.py --dir=/tmp/deps | |
| The "top" dir is then /tmp/deps (Linux filesystem example) and is | |
| where this program will clone and build the dependent repositories. | |
| Helper CMake Config File | |
| ------------------------ | |
| When the program finishes building the dependencies, it writes a file | |
| named "helper.cmake" to the "top" directory that contains CMake commands | |
| for setting CMake variables for locating the dependent repositories. | |
| This helper file can be used to set up the CMake build files for this | |
| "home" repository. | |
| A complete sequence might look like: | |
| $ git clone git@github.com:My-Group/My-Repo.git | |
| $ cd My-Repo | |
| $ mkdir build | |
| $ cd build | |
| $ ../scripts/update_deps.py | |
| $ cmake -C helper.cmake .. | |
| $ cmake --build . | |
| JSON File Schema | |
| ---------------- | |
| There's no formal schema for the "known-good" JSON file, but here is | |
| a description of its elements. All elements are required except those | |
| marked as optional. Please see the "known_good.json" file for | |
| examples of all of these elements. | |
| - name | |
| The name of the dependent repository. This field can be referenced | |
| by the "deps.repo_name" structure to record a dependency. | |
| - url | |
| Specifies the URL of the repository. | |
| Example: https://github.com/KhronosGroup/Vulkan-Loader.git | |
| - sub_dir | |
| The directory where the program clones the repository, relative to | |
| the "top" directory. | |
| - build_dir | |
| The directory used to build the repository, relative to the "top" | |
| directory. | |
| - install_dir | |
| The directory used to store the installed build artifacts, relative | |
| to the "top" directory. | |
| - commit | |
| The commit used to checkout the repository. This can be a SHA-1 | |
| object name or a refname used with the remote name "origin". | |
| For example, this field can be set to "origin/sdk-1.1.77" to | |
| select the end of the sdk-1.1.77 branch. | |
| - deps (optional) | |
| An array of pairs consisting of a CMake variable name and a | |
| repository name to specify a dependent repo and a "link" to | |
| that repo's install artifacts. For example: | |
| "deps" : [ | |
| { | |
| "var_name" : "VULKAN_HEADERS_INSTALL_DIR", | |
| "repo_name" : "Vulkan-Headers" | |
| } | |
| ] | |
| which represents that this repository depends on the Vulkan-Headers | |
| repository and uses the VULKAN_HEADERS_INSTALL_DIR CMake variable to | |
| specify the location where it expects to find the Vulkan-Headers install | |
| directory. | |
| Note that the "repo_name" element must match the "name" element of some | |
| other repository in the JSON file. | |
| - prebuild (optional) | |
| - prebuild_linux (optional) (For Linux and MacOS) | |
| - prebuild_windows (optional) | |
| A list of commands to execute before building a dependent repository. | |
| This is useful for repositories that require the execution of some | |
| sort of "update" script or need to clone an auxillary repository like | |
| googletest. | |
| The commands listed in "prebuild" are executed first, and then the | |
| commands for the specific platform are executed. | |
| - custom_build (optional) | |
| A list of commands to execute as a custom build instead of using | |
| the built in CMake way of building. Requires "build_step" to be | |
| set to "custom" | |
| You can insert the following keywords into the commands listed in | |
| "custom_build" if they require runtime information (like whether the | |
| build config is "Debug" or "Release"). | |
| Keywords: | |
| {0} reference to a dictionary of repos and their attributes | |
| {1} reference to the command line arguments set before start | |
| {2} reference to the CONFIG_MAP value of config. | |
| Example: | |
| {2} returns the CONFIG_MAP value of config e.g. debug -> Debug | |
| {1}.config returns the config variable set when you ran update_dep.py | |
| {0}[Vulkan-Headers][repo_root] returns the repo_root variable from | |
| the Vulkan-Headers GoodRepo object. | |
| - cmake_options (optional) | |
| A list of options to pass to CMake during the generation phase. | |
| - ci_only (optional) | |
| A list of environment variables where one must be set to "true" | |
| (case-insensitive) in order for this repo to be fetched and built. | |
| This list can be used to specify repos that should be built only in CI. | |
| Typically, this list might contain "TRAVIS" and/or "APPVEYOR" because | |
| each of these CI systems sets an environment variable with its own | |
| name to "true". Note that this could also be (ab)used to control | |
| the processing of the repo with any environment variable. The default | |
| is an empty list, which means that the repo is always processed. | |
| - build_step (optional) | |
| Specifies if the dependent repository should be built or not. This can | |
| have a value of 'build', 'custom', or 'skip'. The dependent repositories are | |
| built by default. | |
| - build_platforms (optional) | |
| A list of platforms the repository will be built on. | |
| Legal options include: | |
| "windows" | |
| "linux" | |
| "darwin" | |
| Builds on all platforms by default. | |
| Note | |
| ---- | |
| The "sub_dir", "build_dir", and "install_dir" elements are all relative | |
| to the effective "top" directory. Specifying absolute paths is not | |
| supported. However, the "top" directory specified with the "--dir" | |
| option can be a relative or absolute path. | |
| """ | |
| from __future__ import print_function | |
| import argparse | |
| import json | |
| import distutils.dir_util | |
| import os.path | |
| import subprocess | |
| import sys | |
| import platform | |
| import multiprocessing | |
| import shlex | |
| import shutil | |
| KNOWN_GOOD_FILE_NAME = 'known_good.json' | |
| CONFIG_MAP = { | |
| 'debug': 'Debug', | |
| 'release': 'Release', | |
| 'relwithdebinfo': 'RelWithDebInfo', | |
| 'minsizerel': 'MinSizeRel' | |
| } | |
| VERBOSE = False | |
| DEVNULL = open(os.devnull, 'wb') | |
| def command_output(cmd, directory, fail_ok=False): | |
| """Runs a command in a directory and returns its standard output stream. | |
| Captures the standard error stream and prints it if error. | |
| Raises a RuntimeError if the command fails to launch or otherwise fails. | |
| """ | |
| if VERBOSE: | |
| print('In {d}: {cmd}'.format(d=directory, cmd=cmd)) | |
| p = subprocess.Popen( | |
| cmd, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| (stdout, stderr) = p.communicate() | |
| if p.returncode != 0: | |
| print('*** Error ***\nstderr contents:\n{}'.format(stderr)) | |
| if not fail_ok: | |
| raise RuntimeError('Failed to run {} in {}'.format(cmd, directory)) | |
| if VERBOSE: | |
| print(stdout) | |
| return stdout | |
| class GoodRepo(object): | |
| """Represents a repository at a known-good commit.""" | |
| def __init__(self, json, args): | |
| """Initializes this good repo object. | |
| Args: | |
| 'json': A fully populated JSON object describing the repo. | |
| 'args': Results from ArgumentParser | |
| """ | |
| self._json = json | |
| self._args = args | |
| # Required JSON elements | |
| self.name = json['name'] | |
| self.url = json['url'] | |
| self.sub_dir = json['sub_dir'] | |
| self.commit = json['commit'] | |
| # Optional JSON elements | |
| self.build_dir = None | |
| self.install_dir = None | |
| if json.get('build_dir'): | |
| self.build_dir = os.path.normpath(json['build_dir']) | |
| if json.get('install_dir'): | |
| self.install_dir = os.path.normpath(json['install_dir']) | |
| self.deps = json['deps'] if ('deps' in json) else [] | |
| self.prebuild = json['prebuild'] if ('prebuild' in json) else [] | |
| self.prebuild_linux = json['prebuild_linux'] if ( | |
| 'prebuild_linux' in json) else [] | |
| self.prebuild_windows = json['prebuild_windows'] if ( | |
| 'prebuild_windows' in json) else [] | |
| self.custom_build = json['custom_build'] if ('custom_build' in json) else [] | |
| self.cmake_options = json['cmake_options'] if ( | |
| 'cmake_options' in json) else [] | |
| self.ci_only = json['ci_only'] if ('ci_only' in json) else [] | |
| self.build_step = json['build_step'] if ('build_step' in json) else 'build' | |
| self.build_platforms = json['build_platforms'] if ('build_platforms' in json) else [] | |
| # Absolute paths for a repo's directories | |
| dir_top = os.path.abspath(args.dir) | |
| self.repo_dir = os.path.join(dir_top, self.sub_dir) | |
| if self.build_dir: | |
| self.build_dir = os.path.join(dir_top, self.build_dir) | |
| if self.install_dir: | |
| self.install_dir = os.path.join(dir_top, self.install_dir) | |
| # Check if platform is one to build on | |
| self.on_build_platform = False | |
| if self.build_platforms == [] or platform.system().lower() in self.build_platforms: | |
| self.on_build_platform = True | |
| def Clone(self): | |
| distutils.dir_util.mkpath(self.repo_dir) | |
| command_output(['git', 'clone', self.url, '.'], self.repo_dir) | |
| def Fetch(self): | |
| command_output(['git', 'fetch', 'origin'], self.repo_dir) | |
| def Checkout(self): | |
| print('Checking out {n} in {d}'.format(n=self.name, d=self.repo_dir)) | |
| if self._args.do_clean_repo: | |
| shutil.rmtree(self.repo_dir) | |
| if not os.path.exists(os.path.join(self.repo_dir, '.git')): | |
| self.Clone() | |
| self.Fetch() | |
| if len(self._args.ref): | |
| command_output(['git', 'checkout', self._args.ref], self.repo_dir) | |
| else: | |
| command_output(['git', 'checkout', self.commit], self.repo_dir) | |
| print(command_output(['git', 'status'], self.repo_dir)) | |
| def CustomPreProcess(self, cmd_str, repo_dict): | |
| return cmd_str.format(repo_dict, self._args, CONFIG_MAP[self._args.config]) | |
| def PreBuild(self): | |
| """Execute any prebuild steps from the repo root""" | |
| for p in self.prebuild: | |
| command_output(shlex.split(p), self.repo_dir) | |
| if platform.system() == 'Linux' or platform.system() == 'Darwin': | |
| for p in self.prebuild_linux: | |
| command_output(shlex.split(p), self.repo_dir) | |
| if platform.system() == 'Windows': | |
| for p in self.prebuild_windows: | |
| command_output(shlex.split(p), self.repo_dir) | |
| def CustomBuild(self, repo_dict): | |
| """Execute any custom_build steps from the repo root""" | |
| for p in self.custom_build: | |
| cmd = self.CustomPreProcess(p, repo_dict) | |
| command_output(shlex.split(cmd), self.repo_dir) | |
| def CMakeConfig(self, repos): | |
| """Build CMake command for the configuration phase and execute it""" | |
| if self._args.do_clean_build: | |
| shutil.rmtree(self.build_dir) | |
| if self._args.do_clean_install: | |
| shutil.rmtree(self.install_dir) | |
| # Create and change to build directory | |
| distutils.dir_util.mkpath(self.build_dir) | |
| os.chdir(self.build_dir) | |
| cmake_cmd = [ | |
| 'cmake', self.repo_dir, | |
| '-DCMAKE_INSTALL_PREFIX=' + self.install_dir | |
| ] | |
| # For each repo this repo depends on, generate a CMake variable | |
| # definitions for "...INSTALL_DIR" that points to that dependent | |
| # repo's install dir. | |
| for d in self.deps: | |
| dep_commit = [r for r in repos if r.name == d['repo_name']] | |
| if len(dep_commit): | |
| cmake_cmd.append('-D{var_name}={install_dir}'.format( | |
| var_name=d['var_name'], | |
| install_dir=dep_commit[0].install_dir)) | |
| # Add any CMake options | |
| for option in self.cmake_options: | |
| cmake_cmd.append(option) | |
| # Set build config for single-configuration generators | |
| if platform.system() == 'Linux' or platform.system() == 'Darwin': | |
| cmake_cmd.append('-DCMAKE_BUILD_TYPE={config}'.format( | |
| config=CONFIG_MAP[self._args.config])) | |
| # Use the CMake -A option to select the platform architecture | |
| # without needing a Visual Studio generator. | |
| if platform.system() == 'Windows': | |
| if self._args.arch == '64' or self._args.arch == 'x64' or self._args.arch == 'win64': | |
| cmake_cmd.append('-A') | |
| cmake_cmd.append('x64') | |
| if VERBOSE: | |
| print("CMake command: " + " ".join(cmake_cmd)) | |
| ret_code = subprocess.call(cmake_cmd) | |
| if ret_code != 0: | |
| sys.exit(ret_code) | |
| def CMakeBuild(self): | |
| """Build CMake command for the build phase and execute it""" | |
| cmake_cmd = ['cmake', '--build', self.build_dir, '--target', 'install'] | |
| if self._args.do_clean: | |
| cmake_cmd.append('--clean-first') | |
| if platform.system() == 'Windows': | |
| cmake_cmd.append('--config') | |
| cmake_cmd.append(CONFIG_MAP[self._args.config]) | |
| # Speed up the build. | |
| if platform.system() == 'Linux' or platform.system() == 'Darwin': | |
| cmake_cmd.append('--') | |
| cmake_cmd.append('-j{ncpu}' | |
| .format(ncpu=multiprocessing.cpu_count())) | |
| if platform.system() == 'Windows': | |
| cmake_cmd.append('--') | |
| cmake_cmd.append('/maxcpucount') | |
| if VERBOSE: | |
| print("CMake command: " + " ".join(cmake_cmd)) | |
| ret_code = subprocess.call(cmake_cmd) | |
| if ret_code != 0: | |
| sys.exit(ret_code) | |
| def Build(self, repos, repo_dict): | |
| """Build the dependent repo""" | |
| print('Building {n} in {d}'.format(n=self.name, d=self.repo_dir)) | |
| print('Build dir = {b}'.format(b=self.build_dir)) | |
| print('Install dir = {i}\n'.format(i=self.install_dir)) | |
| # Run any prebuild commands | |
| self.PreBuild() | |
| if self.build_step == 'custom': | |
| self.CustomBuild(repo_dict) | |
| return | |
| # Build and execute CMake command for creating build files | |
| self.CMakeConfig(repos) | |
| # Build and execute CMake command for the build | |
| self.CMakeBuild() | |
| def GetGoodRepos(args): | |
| """Returns the latest list of GoodRepo objects. | |
| The known-good file is expected to be in the same | |
| directory as this script unless overridden by the 'known_good_dir' | |
| parameter. | |
| """ | |
| if args.known_good_dir: | |
| known_good_file = os.path.join( os.path.abspath(args.known_good_dir), | |
| KNOWN_GOOD_FILE_NAME) | |
| else: | |
| known_good_file = os.path.join( | |
| os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME) | |
| with open(known_good_file) as known_good: | |
| return [ | |
| GoodRepo(repo, args) | |
| for repo in json.loads(known_good.read())['repos'] | |
| ] | |
| def GetInstallNames(args): | |
| """Returns the install names list. | |
| The known-good file is expected to be in the same | |
| directory as this script unless overridden by the 'known_good_dir' | |
| parameter. | |
| """ | |
| if args.known_good_dir: | |
| known_good_file = os.path.join(os.path.abspath(args.known_good_dir), | |
| KNOWN_GOOD_FILE_NAME) | |
| else: | |
| known_good_file = os.path.join( | |
| os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME) | |
| with open(known_good_file) as known_good: | |
| install_info = json.loads(known_good.read()) | |
| if install_info.get('install_names'): | |
| return install_info['install_names'] | |
| else: | |
| return None | |
| def CreateHelper(args, repos, filename): | |
| """Create a CMake config helper file. | |
| The helper file is intended to be used with 'cmake -C <file>' | |
| to build this home repo using the dependencies built by this script. | |
| The install_names dictionary represents the CMake variables used by the | |
| home repo to locate the install dirs of the dependent repos. | |
| This information is baked into the CMake files of the home repo and so | |
| this dictionary is kept with the repo via the json file. | |
| """ | |
| def escape(path): | |
| return path.replace('\\', '\\\\') | |
| install_names = GetInstallNames(args) | |
| with open(filename, 'w') as helper_file: | |
| for repo in repos: | |
| if install_names and repo.name in install_names and repo.on_build_platform: | |
| helper_file.write('set({var} "{dir}" CACHE STRING "" FORCE)\n' | |
| .format( | |
| var=install_names[repo.name], | |
| dir=escape(repo.install_dir))) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Get and build dependent repos at known-good commits') | |
| parser.add_argument( | |
| '--known_good_dir', | |
| dest='known_good_dir', | |
| help="Specify directory for known_good.json file.") | |
| parser.add_argument( | |
| '--dir', | |
| dest='dir', | |
| default='.', | |
| help="Set target directory for repository roots. Default is \'.\'.") | |
| parser.add_argument( | |
| '--ref', | |
| dest='ref', | |
| default='', | |
| help="Override 'commit' with git reference. E.g., 'origin/master'") | |
| parser.add_argument( | |
| '--no-build', | |
| dest='do_build', | |
| action='store_false', | |
| help= | |
| "Clone/update repositories and generate build files without performing compilation", | |
| default=True) | |
| parser.add_argument( | |
| '--clean', | |
| dest='do_clean', | |
| action='store_true', | |
| help="Clean files generated by compiler and linker before building", | |
| default=False) | |
| parser.add_argument( | |
| '--clean-repo', | |
| dest='do_clean_repo', | |
| action='store_true', | |
| help="Delete repository directory before building", | |
| default=False) | |
| parser.add_argument( | |
| '--clean-build', | |
| dest='do_clean_build', | |
| action='store_true', | |
| help="Delete build directory before building", | |
| default=False) | |
| parser.add_argument( | |
| '--clean-install', | |
| dest='do_clean_install', | |
| action='store_true', | |
| help="Delete install directory before building", | |
| default=False) | |
| parser.add_argument( | |
| '--arch', | |
| dest='arch', | |
| choices=['32', '64', 'x86', 'x64', 'win32', 'win64'], | |
| type=str.lower, | |
| help="Set build files architecture (Windows)", | |
| default='64') | |
| parser.add_argument( | |
| '--config', | |
| dest='config', | |
| choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'], | |
| type=str.lower, | |
| help="Set build files configuration", | |
| default='debug') | |
| args = parser.parse_args() | |
| save_cwd = os.getcwd() | |
| # Create working "top" directory if needed | |
| distutils.dir_util.mkpath(args.dir) | |
| abs_top_dir = os.path.abspath(args.dir) | |
| repos = GetGoodRepos(args) | |
| repo_dict = {} | |
| print('Starting builds in {d}'.format(d=abs_top_dir)) | |
| for repo in repos: | |
| # If the repo has a platform whitelist, skip the repo | |
| # unless we are building on a whitelisted platform. | |
| if not repo.on_build_platform: | |
| continue | |
| field_list = ('url', | |
| 'sub_dir', | |
| 'commit', | |
| 'build_dir', | |
| 'install_dir', | |
| 'deps', | |
| 'prebuild', | |
| 'prebuild_linux', | |
| 'prebuild_windows', | |
| 'custom_build', | |
| 'cmake_options', | |
| 'ci_only', | |
| 'build_step', | |
| 'build_platforms', | |
| 'repo_dir', | |
| 'on_build_platform') | |
| repo_dict[repo.name] = {field: getattr(repo, field) for field in field_list}; | |
| # If the repo has a CI whitelist, skip the repo unless | |
| # one of the CI's environment variable is set to true. | |
| if len(repo.ci_only): | |
| do_build = False | |
| for env in repo.ci_only: | |
| if not env in os.environ: | |
| continue | |
| if os.environ[env].lower() == 'true': | |
| do_build = True | |
| break | |
| if not do_build: | |
| continue | |
| # Clone/update the repository | |
| repo.Checkout() | |
| # Build the repository | |
| if args.do_build and repo.build_step != 'skip': | |
| repo.Build(repos, repo_dict) | |
| # Need to restore original cwd in order for CreateHelper to find json file | |
| os.chdir(save_cwd) | |
| CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake')) | |
| sys.exit(0) | |
| if __name__ == '__main__': | |
| main() |