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 LunarG, Inc. 6# 7# Licensed under the Apache License, Version 2.0 (the "License"); 8# you may not use this file except in compliance with the License. 9# You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, software 14# distributed under the License is distributed on an "AS IS" BASIS, 15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16# See the License for the specific language governing permissions and 17# limitations under the License. 18 19# This script was heavily leveraged from KhronosGroup/glslang 20# update_glslang_sources.py. 21"""update_deps.py 22 23Get and build dependent repositories using known-good commits. 24 25Purpose 26------- 27 28This program is intended to assist a developer of this repository 29(the "home" repository) by gathering and building the repositories that 30this home repository depend on. It also checks out each dependent 31repository at a "known-good" commit in order to provide stability in 32the dependent repositories. 33 34Python Compatibility 35-------------------- 36 37This program can be used with Python 2.7 and Python 3. 38 39Known-Good JSON Database 40------------------------ 41 42This program expects to find a file named "known-good.json" in the 43same directory as the program file. This JSON file is tailored for 44the needs of the home repository by including its dependent repositories. 45 46Program Options 47--------------- 48 49See the help text (update_deps.py --help) for a complete list of options. 50 51Program Operation 52----------------- 53 54The program uses the user's current directory at the time of program 55invocation as the location for fetching and building the dependent 56repositories. The user can override this by using the "--dir" option. 57 58For example, a directory named "build" in the repository's root directory 59is a good place to put the dependent repositories because that directory 60is not tracked by Git. (See the .gitignore file.) The "external" directory 61may also be a suitable location. 62A user can issue: 63 64$ cd My-Repo 65$ mkdir build 66$ cd build 67$ ../scripts/update_deps.py 68 69or, to do the same thing, but using the --dir option: 70 71$ cd My-Repo 72$ mkdir build 73$ scripts/update_deps.py --dir=build 74 75With these commands, the "build" directory is considered the "top" 76directory where the program clones the dependent repositories. The 77JSON file configures the build and install working directories to be 78within this "top" directory. 79 80Note that the "dir" option can also specify an absolute path: 81 82$ cd My-Repo 83$ scripts/update_deps.py --dir=/tmp/deps 84 85The "top" dir is then /tmp/deps (Linux filesystem example) and is 86where this program will clone and build the dependent repositories. 87 88Helper CMake Config File 89------------------------ 90 91When the program finishes building the dependencies, it writes a file 92named "helper.cmake" to the "top" directory that contains CMake commands 93for setting CMake variables for locating the dependent repositories. 94This helper file can be used to set up the CMake build files for this 95"home" repository. 96 97A complete sequence might look like: 98 99$ git clone git@github.com:My-Group/My-Repo.git 100$ cd My-Repo 101$ mkdir build 102$ cd build 103$ ../scripts/update_deps.py 104$ cmake -C helper.cmake .. 105$ cmake --build . 106 107JSON File Schema 108---------------- 109 110There's no formal schema for the "known-good" JSON file, but here is 111a description of its elements. All elements are required except those 112marked as optional. Please see the "known_good.json" file for 113examples of all of these elements. 114 115- name 116 117The name of the dependent repository. This field can be referenced 118by the "deps.repo_name" structure to record a dependency. 119 120- url 121 122Specifies the URL of the repository. 123Example: https://github.com/KhronosGroup/Vulkan-Loader.git 124 125- sub_dir 126 127The directory where the program clones the repository, relative to 128the "top" directory. 129 130- build_dir 131 132The directory used to build the repository, relative to the "top" 133directory. 134 135- install_dir 136 137The directory used to store the installed build artifacts, relative 138to the "top" directory. 139 140- commit 141 142The commit used to checkout the repository. This can be a SHA-1 143object name or a refname used with the remote name "origin". 144For example, this field can be set to "origin/sdk-1.1.77" to 145select the end of the sdk-1.1.77 branch. 146 147- deps (optional) 148 149An array of pairs consisting of a CMake variable name and a 150repository name to specify a dependent repo and a "link" to 151that repo's install artifacts. For example: 152 153"deps" : [ 154 { 155 "var_name" : "VULKAN_HEADERS_INSTALL_DIR", 156 "repo_name" : "Vulkan-Headers" 157 } 158] 159 160which represents that this repository depends on the Vulkan-Headers 161repository and uses the VULKAN_HEADERS_INSTALL_DIR CMake variable to 162specify the location where it expects to find the Vulkan-Headers install 163directory. 164Note that the "repo_name" element must match the "name" element of some 165other repository in the JSON file. 166 167- prebuild (optional) 168- prebuild_linux (optional) (For Linux and MacOS) 169- prebuild_windows (optional) 170 171A list of commands to execute before building a dependent repository. 172This is useful for repositories that require the execution of some 173sort of "update" script or need to clone an auxillary repository like 174googletest. 175 176The commands listed in "prebuild" are executed first, and then the 177commands for the specific platform are executed. 178 179- custom_build (optional) 180 181A list of commands to execute as a custom build instead of using 182the built in CMake way of building. Requires "build_step" to be 183set to "custom" 184 185You can insert the following keywords into the commands listed in 186"custom_build" if they require runtime information (like whether the 187build config is "Debug" or "Release"). 188 189Keywords: 190{0} reference to a dictionary of repos and their attributes 191{1} reference to the command line arguments set before start 192{2} reference to the CONFIG_MAP value of config. 193 194Example: 195{2} returns the CONFIG_MAP value of config e.g. debug -> Debug 196{1}.config returns the config variable set when you ran update_dep.py 197{0}[Vulkan-Headers][repo_root] returns the repo_root variable from 198 the Vulkan-Headers GoodRepo object. 199 200- cmake_options (optional) 201 202A list of options to pass to CMake during the generation phase. 203 204- ci_only (optional) 205 206A list of environment variables where one must be set to "true" 207(case-insensitive) in order for this repo to be fetched and built. 208This list can be used to specify repos that should be built only in CI. 209Typically, this list might contain "TRAVIS" and/or "APPVEYOR" because 210each of these CI systems sets an environment variable with its own 211name to "true". Note that this could also be (ab)used to control 212the processing of the repo with any environment variable. The default 213is an empty list, which means that the repo is always processed. 214 215- build_step (optional) 216 217Specifies if the dependent repository should be built or not. This can 218have a value of 'build', 'custom', or 'skip'. The dependent repositories are 219built by default. 220 221- build_platforms (optional) 222 223A list of platforms the repository will be built on. 224Legal options include: 225"windows" 226"linux" 227"darwin" 228 229Builds on all platforms by default. 230 231Note 232---- 233 234The "sub_dir", "build_dir", and "install_dir" elements are all relative 235to the effective "top" directory. Specifying absolute paths is not 236supported. However, the "top" directory specified with the "--dir" 237option can be a relative or absolute path. 238 239""" 240 241from __future__ import print_function 242 243import argparse 244import json 245import distutils.dir_util 246import os.path 247import subprocess 248import sys 249import platform 250import multiprocessing 251import shlex 252import shutil 253 254KNOWN_GOOD_FILE_NAME = 'known_good.json' 255 256CONFIG_MAP = { 257 'debug': 'Debug', 258 'release': 'Release', 259 'relwithdebinfo': 'RelWithDebInfo', 260 'minsizerel': 'MinSizeRel' 261} 262 263VERBOSE = False 264 265DEVNULL = open(os.devnull, 'wb') 266 267 268def command_output(cmd, directory, fail_ok=False): 269 """Runs a command in a directory and returns its standard output stream. 270 271 Captures the standard error stream and prints it if error. 272 273 Raises a RuntimeError if the command fails to launch or otherwise fails. 274 """ 275 if VERBOSE: 276 print('In {d}: {cmd}'.format(d=directory, cmd=cmd)) 277 p = subprocess.Popen( 278 cmd, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 279 (stdout, stderr) = p.communicate() 280 if p.returncode != 0: 281 print('*** Error ***\nstderr contents:\n{}'.format(stderr)) 282 if not fail_ok: 283 raise RuntimeError('Failed to run {} in {}'.format(cmd, directory)) 284 if VERBOSE: 285 print(stdout) 286 return stdout 287 288class GoodRepo(object): 289 """Represents a repository at a known-good commit.""" 290 291 def __init__(self, json, args): 292 """Initializes this good repo object. 293 294 Args: 295 'json': A fully populated JSON object describing the repo. 296 'args': Results from ArgumentParser 297 """ 298 self._json = json 299 self._args = args 300 # Required JSON elements 301 self.name = json['name'] 302 self.url = json['url'] 303 self.sub_dir = json['sub_dir'] 304 self.commit = json['commit'] 305 # Optional JSON elements 306 self.build_dir = None 307 self.install_dir = None 308 if json.get('build_dir'): 309 self.build_dir = os.path.normpath(json['build_dir']) 310 if json.get('install_dir'): 311 self.install_dir = os.path.normpath(json['install_dir']) 312 self.deps = json['deps'] if ('deps' in json) else [] 313 self.prebuild = json['prebuild'] if ('prebuild' in json) else [] 314 self.prebuild_linux = json['prebuild_linux'] if ( 315 'prebuild_linux' in json) else [] 316 self.prebuild_windows = json['prebuild_windows'] if ( 317 'prebuild_windows' in json) else [] 318 self.custom_build = json['custom_build'] if ('custom_build' in json) else [] 319 self.cmake_options = json['cmake_options'] if ( 320 'cmake_options' in json) else [] 321 self.ci_only = json['ci_only'] if ('ci_only' in json) else [] 322 self.build_step = json['build_step'] if ('build_step' in json) else 'build' 323 self.build_platforms = json['build_platforms'] if ('build_platforms' in json) else [] 324 # Absolute paths for a repo's directories 325 dir_top = os.path.abspath(args.dir) 326 self.repo_dir = os.path.join(dir_top, self.sub_dir) 327 if self.build_dir: 328 self.build_dir = os.path.join(dir_top, self.build_dir) 329 if self.install_dir: 330 self.install_dir = os.path.join(dir_top, self.install_dir) 331 # Check if platform is one to build on 332 self.on_build_platform = False 333 if self.build_platforms == [] or platform.system().lower() in self.build_platforms: 334 self.on_build_platform = True 335 336 def Clone(self): 337 distutils.dir_util.mkpath(self.repo_dir) 338 command_output(['git', 'clone', self.url, '.'], self.repo_dir) 339 340 def Fetch(self): 341 command_output(['git', 'fetch', 'origin'], self.repo_dir) 342 343 def Checkout(self): 344 print('Checking out {n} in {d}'.format(n=self.name, d=self.repo_dir)) 345 if self._args.do_clean_repo: 346 shutil.rmtree(self.repo_dir, ignore_errors=True) 347 if not os.path.exists(os.path.join(self.repo_dir, '.git')): 348 self.Clone() 349 self.Fetch() 350 if len(self._args.ref): 351 command_output(['git', 'checkout', self._args.ref], self.repo_dir) 352 else: 353 command_output(['git', 'checkout', self.commit], self.repo_dir) 354 print(command_output(['git', 'status'], self.repo_dir)) 355 356 def CustomPreProcess(self, cmd_str, repo_dict): 357 return cmd_str.format(repo_dict, self._args, CONFIG_MAP[self._args.config]) 358 359 def PreBuild(self): 360 """Execute any prebuild steps from the repo root""" 361 for p in self.prebuild: 362 command_output(shlex.split(p), self.repo_dir) 363 if platform.system() == 'Linux' or platform.system() == 'Darwin': 364 for p in self.prebuild_linux: 365 command_output(shlex.split(p), self.repo_dir) 366 if platform.system() == 'Windows': 367 for p in self.prebuild_windows: 368 command_output(shlex.split(p), self.repo_dir) 369 370 def CustomBuild(self, repo_dict): 371 """Execute any custom_build steps from the repo root""" 372 for p in self.custom_build: 373 cmd = self.CustomPreProcess(p, repo_dict) 374 command_output(shlex.split(cmd), self.repo_dir) 375 376 def CMakeConfig(self, repos): 377 """Build CMake command for the configuration phase and execute it""" 378 if self._args.do_clean_build: 379 shutil.rmtree(self.build_dir) 380 if self._args.do_clean_install: 381 shutil.rmtree(self.install_dir) 382 383 # Create and change to build directory 384 distutils.dir_util.mkpath(self.build_dir) 385 os.chdir(self.build_dir) 386 387 cmake_cmd = [ 388 'cmake', self.repo_dir, 389 '-DCMAKE_INSTALL_PREFIX=' + self.install_dir 390 ] 391 392 # For each repo this repo depends on, generate a CMake variable 393 # definitions for "...INSTALL_DIR" that points to that dependent 394 # repo's install dir. 395 for d in self.deps: 396 dep_commit = [r for r in repos if r.name == d['repo_name']] 397 if len(dep_commit): 398 cmake_cmd.append('-D{var_name}={install_dir}'.format( 399 var_name=d['var_name'], 400 install_dir=dep_commit[0].install_dir)) 401 402 # Add any CMake options 403 for option in self.cmake_options: 404 cmake_cmd.append(option) 405 406 # Set build config for single-configuration generators 407 if platform.system() == 'Linux' or platform.system() == 'Darwin': 408 cmake_cmd.append('-DCMAKE_BUILD_TYPE={config}'.format( 409 config=CONFIG_MAP[self._args.config])) 410 411 # Use the CMake -A option to select the platform architecture 412 # without needing a Visual Studio generator. 413 if platform.system() == 'Windows': 414 if self._args.arch == '64' or self._args.arch == 'x64' or self._args.arch == 'win64': 415 cmake_cmd.append('-A') 416 cmake_cmd.append('x64') 417 418 # Apply a generator, if one is specified. This can be used to supply 419 # a specific generator for the dependent repositories to match 420 # that of the main repository. 421 if self._args.generator is not None: 422 cmake_cmd.extend(['-G', self._args.generator]) 423 424 if VERBOSE: 425 print("CMake command: " + " ".join(cmake_cmd)) 426 427 ret_code = subprocess.call(cmake_cmd) 428 if ret_code != 0: 429 sys.exit(ret_code) 430 431 def CMakeBuild(self): 432 """Build CMake command for the build phase and execute it""" 433 cmake_cmd = ['cmake', '--build', self.build_dir, '--target', 'install'] 434 if self._args.do_clean: 435 cmake_cmd.append('--clean-first') 436 437 if platform.system() == 'Windows': 438 cmake_cmd.append('--config') 439 cmake_cmd.append(CONFIG_MAP[self._args.config]) 440 441 # Speed up the build. 442 if platform.system() == 'Linux' or platform.system() == 'Darwin': 443 cmake_cmd.append('--') 444 num_make_jobs = multiprocessing.cpu_count() 445 env_make_jobs = os.environ.get('MAKE_JOBS', None) 446 if env_make_jobs is not None: 447 try: 448 num_make_jobs = min(num_make_jobs, int(env_make_jobs)) 449 except ValueError: 450 print('warning: environment variable MAKE_JOBS has non-numeric value "{}". ' 451 'Using {} (CPU count) instead.'.format(env_make_jobs, num_make_jobs)) 452 cmake_cmd.append('-j{}'.format(num_make_jobs)) 453 if platform.system() == 'Windows': 454 cmake_cmd.append('--') 455 cmake_cmd.append('/maxcpucount') 456 457 if VERBOSE: 458 print("CMake command: " + " ".join(cmake_cmd)) 459 460 ret_code = subprocess.call(cmake_cmd) 461 if ret_code != 0: 462 sys.exit(ret_code) 463 464 def Build(self, repos, repo_dict): 465 """Build the dependent repo""" 466 print('Building {n} in {d}'.format(n=self.name, d=self.repo_dir)) 467 print('Build dir = {b}'.format(b=self.build_dir)) 468 print('Install dir = {i}\n'.format(i=self.install_dir)) 469 470 # Run any prebuild commands 471 self.PreBuild() 472 473 if self.build_step == 'custom': 474 self.CustomBuild(repo_dict) 475 return 476 477 # Build and execute CMake command for creating build files 478 self.CMakeConfig(repos) 479 480 # Build and execute CMake command for the build 481 self.CMakeBuild() 482 483 484def GetGoodRepos(args): 485 """Returns the latest list of GoodRepo objects. 486 487 The known-good file is expected to be in the same 488 directory as this script unless overridden by the 'known_good_dir' 489 parameter. 490 """ 491 if args.known_good_dir: 492 known_good_file = os.path.join( os.path.abspath(args.known_good_dir), 493 KNOWN_GOOD_FILE_NAME) 494 else: 495 known_good_file = os.path.join( 496 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME) 497 with open(known_good_file) as known_good: 498 return [ 499 GoodRepo(repo, args) 500 for repo in json.loads(known_good.read())['repos'] 501 ] 502 503 504def GetInstallNames(args): 505 """Returns the install names list. 506 507 The known-good file is expected to be in the same 508 directory as this script unless overridden by the 'known_good_dir' 509 parameter. 510 """ 511 if args.known_good_dir: 512 known_good_file = os.path.join(os.path.abspath(args.known_good_dir), 513 KNOWN_GOOD_FILE_NAME) 514 else: 515 known_good_file = os.path.join( 516 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME) 517 with open(known_good_file) as known_good: 518 install_info = json.loads(known_good.read()) 519 if install_info.get('install_names'): 520 return install_info['install_names'] 521 else: 522 return None 523 524 525def CreateHelper(args, repos, filename): 526 """Create a CMake config helper file. 527 528 The helper file is intended to be used with 'cmake -C <file>' 529 to build this home repo using the dependencies built by this script. 530 531 The install_names dictionary represents the CMake variables used by the 532 home repo to locate the install dirs of the dependent repos. 533 This information is baked into the CMake files of the home repo and so 534 this dictionary is kept with the repo via the json file. 535 """ 536 def escape(path): 537 return path.replace('\\', '\\\\') 538 install_names = GetInstallNames(args) 539 with open(filename, 'w') as helper_file: 540 for repo in repos: 541 if install_names and repo.name in install_names and repo.on_build_platform: 542 helper_file.write('set({var} "{dir}" CACHE STRING "" FORCE)\n' 543 .format( 544 var=install_names[repo.name], 545 dir=escape(repo.install_dir))) 546 547 548def main(): 549 parser = argparse.ArgumentParser( 550 description='Get and build dependent repos at known-good commits') 551 parser.add_argument( 552 '--known_good_dir', 553 dest='known_good_dir', 554 help="Specify directory for known_good.json file.") 555 parser.add_argument( 556 '--dir', 557 dest='dir', 558 default='.', 559 help="Set target directory for repository roots. Default is \'.\'.") 560 parser.add_argument( 561 '--ref', 562 dest='ref', 563 default='', 564 help="Override 'commit' with git reference. E.g., 'origin/master'") 565 parser.add_argument( 566 '--no-build', 567 dest='do_build', 568 action='store_false', 569 help= 570 "Clone/update repositories and generate build files without performing compilation", 571 default=True) 572 parser.add_argument( 573 '--clean', 574 dest='do_clean', 575 action='store_true', 576 help="Clean files generated by compiler and linker before building", 577 default=False) 578 parser.add_argument( 579 '--clean-repo', 580 dest='do_clean_repo', 581 action='store_true', 582 help="Delete repository directory before building", 583 default=False) 584 parser.add_argument( 585 '--clean-build', 586 dest='do_clean_build', 587 action='store_true', 588 help="Delete build directory before building", 589 default=False) 590 parser.add_argument( 591 '--clean-install', 592 dest='do_clean_install', 593 action='store_true', 594 help="Delete install directory before building", 595 default=False) 596 parser.add_argument( 597 '--arch', 598 dest='arch', 599 choices=['32', '64', 'x86', 'x64', 'win32', 'win64'], 600 type=str.lower, 601 help="Set build files architecture (Windows)", 602 default='64') 603 parser.add_argument( 604 '--config', 605 dest='config', 606 choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'], 607 type=str.lower, 608 help="Set build files configuration", 609 default='debug') 610 parser.add_argument( 611 '--generator', 612 dest='generator', 613 help="Set the CMake generator", 614 default=None) 615 616 args = parser.parse_args() 617 save_cwd = os.getcwd() 618 619 # Create working "top" directory if needed 620 distutils.dir_util.mkpath(args.dir) 621 abs_top_dir = os.path.abspath(args.dir) 622 623 repos = GetGoodRepos(args) 624 repo_dict = {} 625 626 print('Starting builds in {d}'.format(d=abs_top_dir)) 627 for repo in repos: 628 # If the repo has a platform whitelist, skip the repo 629 # unless we are building on a whitelisted platform. 630 if not repo.on_build_platform: 631 continue 632 633 field_list = ('url', 634 'sub_dir', 635 'commit', 636 'build_dir', 637 'install_dir', 638 'deps', 639 'prebuild', 640 'prebuild_linux', 641 'prebuild_windows', 642 'custom_build', 643 'cmake_options', 644 'ci_only', 645 'build_step', 646 'build_platforms', 647 'repo_dir', 648 'on_build_platform') 649 repo_dict[repo.name] = {field: getattr(repo, field) for field in field_list} 650 651 # If the repo has a CI whitelist, skip the repo unless 652 # one of the CI's environment variable is set to true. 653 if len(repo.ci_only): 654 do_build = False 655 for env in repo.ci_only: 656 if not env in os.environ: 657 continue 658 if os.environ[env].lower() == 'true': 659 do_build = True 660 break 661 if not do_build: 662 continue 663 664 # Clone/update the repository 665 repo.Checkout() 666 667 # Build the repository 668 if args.do_build and repo.build_step != 'skip': 669 repo.Build(repos, repo_dict) 670 671 # Need to restore original cwd in order for CreateHelper to find json file 672 os.chdir(save_cwd) 673 CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake')) 674 675 sys.exit(0) 676 677 678if __name__ == '__main__': 679 main() 680