blob: ca9a202e38767a8d10d003e3e0b8dcd74f456fc4 [file] [log] [blame]
Karl Schultz5ff6f742018-06-21 17:38:56 -06001#!/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
Shannon McPhersonfabe2c22018-07-31 09:41:15 -0600110There's no formal schema for the "known-good" JSON file, but here is
Karl Schultz5ff6f742018-06-21 17:38:56 -0600111a 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- cmake_options (optional)
180
181A list of options to pass to CMake during the generation phase.
182
183- ci_only (optional)
184
185A list of environment variables where one must be set to "true"
186(case-insensitive) in order for this repo to be fetched and built.
187This list can be used to specify repos that should be built only in CI.
188Typically, this list might contain "TRAVIS" and/or "APPVEYOR" because
189each of these CI systems sets an environment variable with its own
190name to "true". Note that this could also be (ab)used to control
191the processing of the repo with any environment variable. The default
192is an empty list, which means that the repo is always processed.
193
Shannon McPhersonfabe2c22018-07-31 09:41:15 -0600194- build_step (optional)
195
196Specifies if the dependent repository should be built or not. This can
197have a value of 'build' or 'skip'. The dependent repositories are
198built by default.
199
Karl Schultz5ff6f742018-06-21 17:38:56 -0600200Note
201----
202
203The "sub_dir", "build_dir", and "install_dir" elements are all relative
204to the effective "top" directory. Specifying absolute paths is not
205supported. However, the "top" directory specified with the "--dir"
206option can be a relative or absolute path.
207
208"""
209
210from __future__ import print_function
211
212import argparse
213import json
214import distutils.dir_util
215import os.path
216import subprocess
217import sys
218import platform
219import multiprocessing
220import shutil
221
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600222KNOWN_GOOD_FILE_NAME = 'known_good.json'
Karl Schultz5ff6f742018-06-21 17:38:56 -0600223
224CONFIG_MAP = {
225 'debug': 'Debug',
226 'release': 'Release',
227 'relwithdebinfo': 'RelWithDebInfo',
228 'minsizerel': 'MinSizeRel'
229}
230
231VERBOSE = False
232
233DEVNULL = open(os.devnull, 'wb')
234
235
236def command_output(cmd, directory, fail_ok=False):
237 """Runs a command in a directory and returns its standard output stream.
238
239 Captures the standard error stream and prints it if error.
240
241 Raises a RuntimeError if the command fails to launch or otherwise fails.
242 """
243 if VERBOSE:
244 print('In {d}: {cmd}'.format(d=directory, cmd=cmd))
245 p = subprocess.Popen(
246 cmd, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
247 (stdout, stderr) = p.communicate()
248 if p.returncode != 0:
249 print('*** Error ***\nstderr contents:\n{}'.format(stderr))
250 if not fail_ok:
251 raise RuntimeError('Failed to run {} in {}'.format(cmd, directory))
252 if VERBOSE:
253 print(stdout)
254 return stdout
255
256
257class GoodRepo(object):
258 """Represents a repository at a known-good commit."""
259
260 def __init__(self, json, args):
261 """Initializes this good repo object.
262
263 Args:
264 'json': A fully populated JSON object describing the repo.
265 'args': Results from ArgumentParser
266 """
267 self._json = json
268 self._args = args
269 # Required JSON elements
270 self.name = json['name']
271 self.url = json['url']
272 self.sub_dir = json['sub_dir']
Karl Schultz5ff6f742018-06-21 17:38:56 -0600273 self.commit = json['commit']
274 # Optional JSON elements
Mark Lobodzinski3fe96b62018-07-19 14:45:10 -0600275 self.build_dir = None
276 self.install_dir = None
277 if json.get('build_dir'):
278 self.build_dir = json['build_dir']
279 if json.get('install_dir'):
280 self.install_dir = json['install_dir']
Karl Schultz5ff6f742018-06-21 17:38:56 -0600281 self.deps = json['deps'] if ('deps' in json) else []
282 self.prebuild = json['prebuild'] if ('prebuild' in json) else []
283 self.prebuild_linux = json['prebuild_linux'] if (
284 'prebuild_linux' in json) else []
285 self.prebuild_windows = json['prebuild_windows'] if (
286 'prebuild_windows' in json) else []
287 self.cmake_options = json['cmake_options'] if (
288 'cmake_options' in json) else []
289 self.ci_only = json['ci_only'] if ('ci_only' in json) else []
Shannon McPhersonfabe2c22018-07-31 09:41:15 -0600290 self.build_step = json['build_step'] if ('build_step' in json) else 'build'
Karl Schultz5ff6f742018-06-21 17:38:56 -0600291 # Absolute paths for a repo's directories
292 dir_top = os.path.abspath(args.dir)
293 self.repo_dir = os.path.join(dir_top, self.sub_dir)
Mark Lobodzinski3fe96b62018-07-19 14:45:10 -0600294 if self.build_dir:
295 self.build_dir = os.path.join(dir_top, self.build_dir)
296 if self.install_dir:
297 self.install_dir = os.path.join(dir_top, self.install_dir)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600298
299 def Clone(self):
300 distutils.dir_util.mkpath(self.repo_dir)
301 command_output(['git', 'clone', self.url, '.'], self.repo_dir)
302
303 def Fetch(self):
304 command_output(['git', 'fetch', 'origin'], self.repo_dir)
305
306 def Checkout(self):
307 print('Checking out {n} in {d}'.format(n=self.name, d=self.repo_dir))
308 if self._args.do_clean_repo:
309 shutil.rmtree(self.repo_dir)
310 if not os.path.exists(os.path.join(self.repo_dir, '.git')):
311 self.Clone()
312 self.Fetch()
313 if len(self._args.ref):
314 command_output(['git', 'checkout', self._args.ref], self.repo_dir)
315 else:
316 command_output(['git', 'checkout', self.commit], self.repo_dir)
317 print(command_output(['git', 'status'], self.repo_dir))
318
319 def PreBuild(self):
320 """Execute any prebuild steps from the repo root"""
321 for p in self.prebuild:
322 command_output(p.split(), self.repo_dir)
323 if platform.system() == 'Linux' or platform.system() == 'Darwin':
324 for p in self.prebuild_linux:
325 command_output(p.split(), self.repo_dir)
326 if platform.system() == 'Windows':
327 for p in self.prebuild_windows:
328 command_output(p.split(), self.repo_dir)
329
330 def CMakeConfig(self, repos):
331 """Build CMake command for the configuration phase and execute it"""
332 if self._args.do_clean_build:
333 shutil.rmtree(self.build_dir)
334 if self._args.do_clean_install:
335 shutil.rmtree(self.install_dir)
336
337 # Create and change to build directory
338 distutils.dir_util.mkpath(self.build_dir)
339 os.chdir(self.build_dir)
340
341 cmake_cmd = [
342 'cmake', self.repo_dir,
343 '-DCMAKE_INSTALL_PREFIX=' + self.install_dir
344 ]
345
346 # For each repo this repo depends on, generate a CMake variable
347 # definitions for "...INSTALL_DIR" that points to that dependent
348 # repo's install dir.
349 for d in self.deps:
350 dep_commit = [r for r in repos if r.name == d['repo_name']]
351 if len(dep_commit):
352 cmake_cmd.append('-D{var_name}={install_dir}'.format(
353 var_name=d['var_name'],
354 install_dir=dep_commit[0].install_dir))
355
356 # Add any CMake options
357 for option in self.cmake_options:
358 cmake_cmd.append(option)
359
360 # Set build config for single-configuration generators
361 if platform.system() == 'Linux' or platform.system() == 'Darwin':
362 cmake_cmd.append('-DCMAKE_BUILD_TYPE={config}'.format(
363 config=CONFIG_MAP[self._args.config]))
364
365 # Use the CMake -A option to select the platform architecture
366 # without needing a Visual Studio generator.
367 if platform.system() == 'Windows':
368 if self._args.arch == '64' or self._args.arch == 'x64' or self._args.arch == 'win64':
369 cmake_cmd.append('-A')
370 cmake_cmd.append('x64')
371
372 if VERBOSE:
373 print("CMake command: " + " ".join(cmake_cmd))
374
375 ret_code = subprocess.call(cmake_cmd)
376 if ret_code != 0:
377 sys.exit(ret_code)
378
379 def CMakeBuild(self):
380 """Build CMake command for the build phase and execute it"""
381 cmake_cmd = ['cmake', '--build', self.build_dir, '--target', 'install']
382 if self._args.do_clean:
383 cmake_cmd.append('--clean-first')
384
385 if platform.system() == 'Windows':
386 cmake_cmd.append('--config')
387 cmake_cmd.append(CONFIG_MAP[self._args.config])
388
389 # Speed up the build.
390 if platform.system() == 'Linux' or platform.system() == 'Darwin':
391 cmake_cmd.append('--')
392 cmake_cmd.append('-j{ncpu}'
393 .format(ncpu=multiprocessing.cpu_count()))
394 if platform.system() == 'Windows':
395 cmake_cmd.append('--')
396 cmake_cmd.append('/maxcpucount')
397
398 if VERBOSE:
399 print("CMake command: " + " ".join(cmake_cmd))
400
401 ret_code = subprocess.call(cmake_cmd)
402 if ret_code != 0:
403 sys.exit(ret_code)
404
405 def Build(self, repos):
406 """Build the dependent repo"""
407 print('Building {n} in {d}'.format(n=self.name, d=self.repo_dir))
408 print('Build dir = {b}'.format(b=self.build_dir))
409 print('Install dir = {i}\n'.format(i=self.install_dir))
410
411 # Run any prebuild commands
412 self.PreBuild()
413
414 # Build and execute CMake command for creating build files
415 self.CMakeConfig(repos)
416
417 # Build and execute CMake command for the build
418 self.CMakeBuild()
419
420
421def GetGoodRepos(args):
422 """Returns the latest list of GoodRepo objects.
423
424 The known-good file is expected to be in the same
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600425 directory as this script unless overridden by the 'known_good_dir'
426 parameter.
Karl Schultz5ff6f742018-06-21 17:38:56 -0600427 """
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600428 if args.known_good_dir:
429 known_good_file = os.path.join( os.path.abspath(args.known_good_dir),
430 KNOWN_GOOD_FILE_NAME)
431 else:
432 known_good_file = os.path.join(
433 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600434 with open(known_good_file) as known_good:
435 return [
436 GoodRepo(repo, args)
437 for repo in json.loads(known_good.read())['repos']
438 ]
439
440
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600441def GetInstallNames(args):
Karl Schultz5ff6f742018-06-21 17:38:56 -0600442 """Returns the install names list.
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600443
Karl Schultz5ff6f742018-06-21 17:38:56 -0600444 The known-good file is expected to be in the same
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600445 directory as this script unless overridden by the 'known_good_dir'
446 parameter.
Karl Schultz5ff6f742018-06-21 17:38:56 -0600447 """
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600448 if args.known_good_dir:
449 known_good_file = os.path.join(os.path.abspath(args.known_good_dir),
450 KNOWN_GOOD_FILE_NAME)
451 else:
452 known_good_file = os.path.join(
453 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600454 with open(known_good_file) as known_good:
Mark Lobodzinski3fe96b62018-07-19 14:45:10 -0600455 install_info = json.loads(known_good.read())
456 if install_info.get('install_names'):
457 return install_info['install_names']
458 else:
459 return None
Karl Schultz5ff6f742018-06-21 17:38:56 -0600460
461
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600462def CreateHelper(args, repos, filename):
Karl Schultz5ff6f742018-06-21 17:38:56 -0600463 """Create a CMake config helper file.
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600464
Karl Schultz5ff6f742018-06-21 17:38:56 -0600465 The helper file is intended to be used with 'cmake -C <file>'
466 to build this home repo using the dependencies built by this script.
467
468 The install_names dictionary represents the CMake variables used by the
469 home repo to locate the install dirs of the dependent repos.
470 This information is baked into the CMake files of the home repo and so
471 this dictionary is kept with the repo via the json file.
472 """
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600473 install_names = GetInstallNames(args)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600474 with open(filename, 'w') as helper_file:
475 for repo in repos:
Mark Lobodzinski3fe96b62018-07-19 14:45:10 -0600476 if install_names and repo.name in install_names:
Karl Schultz5ff6f742018-06-21 17:38:56 -0600477 helper_file.write('set({var} "{dir}" CACHE STRING "" FORCE)\n'
478 .format(
479 var=install_names[repo.name],
480 dir=repo.install_dir))
481
482
483def main():
484 parser = argparse.ArgumentParser(
485 description='Get and build dependent repos at known-good commits')
486 parser.add_argument(
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600487 '--known_good_dir',
488 dest='known_good_dir',
489 help="Specify directory for known_good.json file.")
490 parser.add_argument(
Karl Schultz5ff6f742018-06-21 17:38:56 -0600491 '--dir',
492 dest='dir',
493 default='.',
494 help="Set target directory for repository roots. Default is \'.\'.")
495 parser.add_argument(
496 '--ref',
497 dest='ref',
498 default='',
499 help="Override 'commit' with git reference. E.g., 'origin/master'")
500 parser.add_argument(
501 '--no-build',
502 dest='do_build',
503 action='store_false',
504 help=
505 "Clone/update repositories and generate build files without performing compilation",
506 default=True)
507 parser.add_argument(
508 '--clean',
509 dest='do_clean',
510 action='store_true',
511 help="Clean files generated by compiler and linker before building",
512 default=False)
513 parser.add_argument(
514 '--clean-repo',
515 dest='do_clean_repo',
516 action='store_true',
517 help="Delete repository directory before building",
518 default=False)
519 parser.add_argument(
520 '--clean-build',
521 dest='do_clean_build',
522 action='store_true',
523 help="Delete build directory before building",
524 default=False)
525 parser.add_argument(
526 '--clean-install',
527 dest='do_clean_install',
528 action='store_true',
529 help="Delete install directory before building",
530 default=False)
531 parser.add_argument(
532 '--arch',
533 dest='arch',
Tony-LunarGf445ff12018-07-24 16:31:20 -0600534 choices=['32', '64', 'x86', 'x64', 'win32', 'win64'],
Karl Schultz5ff6f742018-06-21 17:38:56 -0600535 type=str.lower,
536 help="Set build files architecture (Windows)",
537 default='64')
538 parser.add_argument(
539 '--config',
540 dest='config',
541 choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'],
542 type=str.lower,
543 help="Set build files configuration",
544 default='debug')
545
546 args = parser.parse_args()
547 save_cwd = os.getcwd()
548
549 # Create working "top" directory if needed
550 distutils.dir_util.mkpath(args.dir)
551 abs_top_dir = os.path.abspath(args.dir)
552
553 repos = GetGoodRepos(args)
554
555 print('Starting builds in {d}'.format(d=abs_top_dir))
556 for repo in repos:
557 # If the repo has a CI whitelist, skip the repo unless
558 # one of the CI's environment variable is set to true.
559 if len(repo.ci_only):
560 do_build = False
561 for env in repo.ci_only:
562 if not env in os.environ:
563 continue
564 if os.environ[env].lower() == 'true':
565 do_build = True
566 break
567 if not do_build:
568 continue
569
570 # Clone/update the repository
571 repo.Checkout()
572
573 # Build the repository
Shannon McPhersonfabe2c22018-07-31 09:41:15 -0600574 if args.do_build and repo.build_step == 'build':
Karl Schultz5ff6f742018-06-21 17:38:56 -0600575 repo.Build(repos)
576
577 # Need to restore original cwd in order for CreateHelper to find json file
578 os.chdir(save_cwd)
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600579 CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake'))
Karl Schultz5ff6f742018-06-21 17:38:56 -0600580
581 sys.exit(0)
582
583
584if __name__ == '__main__':
585 main()