build: Add dependent repo fetch script [skip ci]
Add a Python program script that clones and builds other
repositories that this repository depends on.
The script is accompanied by a JSON database that contains
the repository information, "known-good" revision information,
and other build parameters for each dependent repository.
The dependent repositories are checked out to the "known-good"
revisions to ensure that they are compatible with the checked-out
revision of this repository.
See the BUILD.md file, the update_deps.py --help output, and
the documentation found inside of the update_deps.py file for
more information.
Fixes #161
Fixes #169
diff --git a/scripts/known_good.json b/scripts/known_good.json
new file mode 100644
index 0000000..10c16be
--- /dev/null
+++ b/scripts/known_good.json
@@ -0,0 +1,78 @@
+{
+ "repos" : [
+ {
+ "name" : "glslang",
+ "url" : "https://github.com/KhronosGroup/glslang.git",
+ "sub_dir" : "glslang",
+ "build_dir" : "glslang/build",
+ "install_dir" : "glslang/build/install",
+ "commit" : "2c8265bb620300047bb9241e39c60c7afe3a81cb",
+ "prebuild" : [
+ "python update_glslang_sources.py"
+ ]
+ },
+ {
+ "name" : "Vulkan-Headers",
+ "url" : "https://github.com/KhronosGroup/Vulkan-Headers.git",
+ "sub_dir" : "Vulkan-Headers",
+ "build_dir" : "Vulkan-Headers/build",
+ "install_dir" : "Vulkan-Headers/build/install",
+ "commit" : "origin/sdk-1.1.77"
+ },
+ {
+ "name" : "Vulkan-Loader",
+ "url" : "https://github.com/KhronosGroup/Vulkan-Loader.git",
+ "sub_dir" : "Vulkan-Loader",
+ "build_dir" : "Vulkan-Loader/build",
+ "install_dir" : "Vulkan-Loader/build/install",
+ "commit" : "origin/sdk-1.1.77",
+ "deps" : [
+ {
+ "var_name" : "VULKAN_HEADERS_INSTALL_DIR",
+ "repo_name" : "Vulkan-Headers"
+ }
+ ],
+ "cmake_options" : [
+ "-DBUILD_TESTS=NO"
+ ]
+ },
+ {
+ "name" : "VulkanTools",
+ "url" : "https://github.com/LunarG/VulkanTools.git",
+ "sub_dir" : "VulkanTools",
+ "build_dir" : "VulkanTools/build",
+ "install_dir" : "VulkanTools/build/install",
+ "commit" : "origin/sdk-1.1.77",
+ "deps" : [
+ {
+ "var_name" : "VULKAN_HEADERS_INSTALL_DIR",
+ "repo_name" : "Vulkan-Headers"
+ },
+ {
+ "var_name" : "GLSLANG_INSTALL_DIR",
+ "repo_name" : "glslang"
+ }
+ ],
+ "prebuild_linux" : [
+ "bash update_external_sources.sh"
+ ],
+ "prebuild_windows" : [
+ ".\\update_external_sources.bat"
+ ],
+ "cmake_options" : [
+ "-DBUILD_TESTS=NO",
+ "-DBUILD_VKTRACE=NO",
+ "-DBUILD_VLF=NO",
+ "-DBUILD_VIA=NO"
+ ],
+ "ci_only" : [
+ "TRAVIS"
+ ]
+ }
+ ],
+ "install_names" : {
+ "glslang" : "GLSLANG_INSTALL_DIR",
+ "Vulkan-Headers" : "VULKAN_HEADERS_INSTALL_DIR",
+ "Vulkan-Loader" : "VULKAN_LOADER_INSTALL_DIR"
+ }
+}
diff --git a/scripts/update_deps.py b/scripts/update_deps.py
new file mode 100755
index 0000000..0c2f698
--- /dev/null
+++ b/scripts/update_deps.py
@@ -0,0 +1,554 @@
+#!/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.
+
+- 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.
+
+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 shutil
+
+KNOWN_GOOD_FILE = '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.build_dir = json['build_dir']
+ self.install_dir = json['install_dir']
+ self.commit = json['commit']
+ # Optional JSON elements
+ 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.cmake_options = json['cmake_options'] if (
+ 'cmake_options' in json) else []
+ self.ci_only = json['ci_only'] if ('ci_only' 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)
+ self.build_dir = os.path.join(dir_top, self.build_dir)
+ self.install_dir = os.path.join(dir_top, self.install_dir)
+
+ 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 PreBuild(self):
+ """Execute any prebuild steps from the repo root"""
+ for p in self.prebuild:
+ command_output(p.split(), self.repo_dir)
+ if platform.system() == 'Linux' or platform.system() == 'Darwin':
+ for p in self.prebuild_linux:
+ command_output(p.split(), self.repo_dir)
+ if platform.system() == 'Windows':
+ for p in self.prebuild_windows:
+ command_output(p.split(), 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):
+ """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()
+
+ # 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.
+ """
+ known_good_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE)
+ with open(known_good_file) as known_good:
+ return [
+ GoodRepo(repo, args)
+ for repo in json.loads(known_good.read())['repos']
+ ]
+
+
+def GetInstallNames():
+ """Returns the install names list.
+
+ The known-good file is expected to be in the same
+ directory as this script.
+ """
+ known_good_file = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE)
+ with open(known_good_file) as known_good:
+ return json.loads(known_good.read())['install_names']
+
+
+def CreateHelper(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.
+ """
+ install_names = GetInstallNames()
+ with open(filename, 'w') as helper_file:
+ for repo in repos:
+ if repo.name in install_names:
+ helper_file.write('set({var} "{dir}" CACHE STRING "" FORCE)\n'
+ .format(
+ var=install_names[repo.name],
+ dir=repo.install_dir))
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Get and build dependent repos at known-good commits')
+ 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)
+
+ print('Starting builds in {d}'.format(d=abs_top_dir))
+ for repo in repos:
+ # 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:
+ repo.Build(repos)
+
+ # Need to restore original cwd in order for CreateHelper to find json file
+ os.chdir(save_cwd)
+ CreateHelper(repos, os.path.join(abs_top_dir, 'helper.cmake'))
+
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ main()