blob: af2bd7d97a0d3aeb6c20e8c779a8729b6aec5146 [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
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- 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
194Note
195----
196
197The "sub_dir", "build_dir", and "install_dir" elements are all relative
198to the effective "top" directory. Specifying absolute paths is not
199supported. However, the "top" directory specified with the "--dir"
200option can be a relative or absolute path.
201
202"""
203
204from __future__ import print_function
205
206import argparse
207import json
208import distutils.dir_util
209import os.path
210import subprocess
211import sys
212import platform
213import multiprocessing
214import shutil
215
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600216KNOWN_GOOD_FILE_NAME = 'known_good.json'
Karl Schultz5ff6f742018-06-21 17:38:56 -0600217
218CONFIG_MAP = {
219 'debug': 'Debug',
220 'release': 'Release',
221 'relwithdebinfo': 'RelWithDebInfo',
222 'minsizerel': 'MinSizeRel'
223}
224
225VERBOSE = False
226
227DEVNULL = open(os.devnull, 'wb')
228
229
230def command_output(cmd, directory, fail_ok=False):
231 """Runs a command in a directory and returns its standard output stream.
232
233 Captures the standard error stream and prints it if error.
234
235 Raises a RuntimeError if the command fails to launch or otherwise fails.
236 """
237 if VERBOSE:
238 print('In {d}: {cmd}'.format(d=directory, cmd=cmd))
239 p = subprocess.Popen(
240 cmd, cwd=directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
241 (stdout, stderr) = p.communicate()
242 if p.returncode != 0:
243 print('*** Error ***\nstderr contents:\n{}'.format(stderr))
244 if not fail_ok:
245 raise RuntimeError('Failed to run {} in {}'.format(cmd, directory))
246 if VERBOSE:
247 print(stdout)
248 return stdout
249
250
251class GoodRepo(object):
252 """Represents a repository at a known-good commit."""
253
254 def __init__(self, json, args):
255 """Initializes this good repo object.
256
257 Args:
258 'json': A fully populated JSON object describing the repo.
259 'args': Results from ArgumentParser
260 """
261 self._json = json
262 self._args = args
263 # Required JSON elements
264 self.name = json['name']
265 self.url = json['url']
266 self.sub_dir = json['sub_dir']
267 self.build_dir = json['build_dir']
268 self.install_dir = json['install_dir']
269 self.commit = json['commit']
270 # Optional JSON elements
271 self.deps = json['deps'] if ('deps' in json) else []
272 self.prebuild = json['prebuild'] if ('prebuild' in json) else []
273 self.prebuild_linux = json['prebuild_linux'] if (
274 'prebuild_linux' in json) else []
275 self.prebuild_windows = json['prebuild_windows'] if (
276 'prebuild_windows' in json) else []
277 self.cmake_options = json['cmake_options'] if (
278 'cmake_options' in json) else []
279 self.ci_only = json['ci_only'] if ('ci_only' in json) else []
280 # Absolute paths for a repo's directories
281 dir_top = os.path.abspath(args.dir)
282 self.repo_dir = os.path.join(dir_top, self.sub_dir)
283 self.build_dir = os.path.join(dir_top, self.build_dir)
284 self.install_dir = os.path.join(dir_top, self.install_dir)
285
286 def Clone(self):
287 distutils.dir_util.mkpath(self.repo_dir)
288 command_output(['git', 'clone', self.url, '.'], self.repo_dir)
289
290 def Fetch(self):
291 command_output(['git', 'fetch', 'origin'], self.repo_dir)
292
293 def Checkout(self):
294 print('Checking out {n} in {d}'.format(n=self.name, d=self.repo_dir))
295 if self._args.do_clean_repo:
296 shutil.rmtree(self.repo_dir)
297 if not os.path.exists(os.path.join(self.repo_dir, '.git')):
298 self.Clone()
299 self.Fetch()
300 if len(self._args.ref):
301 command_output(['git', 'checkout', self._args.ref], self.repo_dir)
302 else:
303 command_output(['git', 'checkout', self.commit], self.repo_dir)
304 print(command_output(['git', 'status'], self.repo_dir))
305
306 def PreBuild(self):
307 """Execute any prebuild steps from the repo root"""
308 for p in self.prebuild:
309 command_output(p.split(), self.repo_dir)
310 if platform.system() == 'Linux' or platform.system() == 'Darwin':
311 for p in self.prebuild_linux:
312 command_output(p.split(), self.repo_dir)
313 if platform.system() == 'Windows':
314 for p in self.prebuild_windows:
315 command_output(p.split(), self.repo_dir)
316
317 def CMakeConfig(self, repos):
318 """Build CMake command for the configuration phase and execute it"""
319 if self._args.do_clean_build:
320 shutil.rmtree(self.build_dir)
321 if self._args.do_clean_install:
322 shutil.rmtree(self.install_dir)
323
324 # Create and change to build directory
325 distutils.dir_util.mkpath(self.build_dir)
326 os.chdir(self.build_dir)
327
328 cmake_cmd = [
329 'cmake', self.repo_dir,
330 '-DCMAKE_INSTALL_PREFIX=' + self.install_dir
331 ]
332
333 # For each repo this repo depends on, generate a CMake variable
334 # definitions for "...INSTALL_DIR" that points to that dependent
335 # repo's install dir.
336 for d in self.deps:
337 dep_commit = [r for r in repos if r.name == d['repo_name']]
338 if len(dep_commit):
339 cmake_cmd.append('-D{var_name}={install_dir}'.format(
340 var_name=d['var_name'],
341 install_dir=dep_commit[0].install_dir))
342
343 # Add any CMake options
344 for option in self.cmake_options:
345 cmake_cmd.append(option)
346
347 # Set build config for single-configuration generators
348 if platform.system() == 'Linux' or platform.system() == 'Darwin':
349 cmake_cmd.append('-DCMAKE_BUILD_TYPE={config}'.format(
350 config=CONFIG_MAP[self._args.config]))
351
352 # Use the CMake -A option to select the platform architecture
353 # without needing a Visual Studio generator.
354 if platform.system() == 'Windows':
355 if self._args.arch == '64' or self._args.arch == 'x64' or self._args.arch == 'win64':
356 cmake_cmd.append('-A')
357 cmake_cmd.append('x64')
358
359 if VERBOSE:
360 print("CMake command: " + " ".join(cmake_cmd))
361
362 ret_code = subprocess.call(cmake_cmd)
363 if ret_code != 0:
364 sys.exit(ret_code)
365
366 def CMakeBuild(self):
367 """Build CMake command for the build phase and execute it"""
368 cmake_cmd = ['cmake', '--build', self.build_dir, '--target', 'install']
369 if self._args.do_clean:
370 cmake_cmd.append('--clean-first')
371
372 if platform.system() == 'Windows':
373 cmake_cmd.append('--config')
374 cmake_cmd.append(CONFIG_MAP[self._args.config])
375
376 # Speed up the build.
377 if platform.system() == 'Linux' or platform.system() == 'Darwin':
378 cmake_cmd.append('--')
379 cmake_cmd.append('-j{ncpu}'
380 .format(ncpu=multiprocessing.cpu_count()))
381 if platform.system() == 'Windows':
382 cmake_cmd.append('--')
383 cmake_cmd.append('/maxcpucount')
384
385 if VERBOSE:
386 print("CMake command: " + " ".join(cmake_cmd))
387
388 ret_code = subprocess.call(cmake_cmd)
389 if ret_code != 0:
390 sys.exit(ret_code)
391
392 def Build(self, repos):
393 """Build the dependent repo"""
394 print('Building {n} in {d}'.format(n=self.name, d=self.repo_dir))
395 print('Build dir = {b}'.format(b=self.build_dir))
396 print('Install dir = {i}\n'.format(i=self.install_dir))
397
398 # Run any prebuild commands
399 self.PreBuild()
400
401 # Build and execute CMake command for creating build files
402 self.CMakeConfig(repos)
403
404 # Build and execute CMake command for the build
405 self.CMakeBuild()
406
407
408def GetGoodRepos(args):
409 """Returns the latest list of GoodRepo objects.
410
411 The known-good file is expected to be in the same
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600412 directory as this script unless overridden by the 'known_good_dir'
413 parameter.
Karl Schultz5ff6f742018-06-21 17:38:56 -0600414 """
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600415 if args.known_good_dir:
416 known_good_file = os.path.join( os.path.abspath(args.known_good_dir),
417 KNOWN_GOOD_FILE_NAME)
418 else:
419 known_good_file = os.path.join(
420 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600421 with open(known_good_file) as known_good:
422 return [
423 GoodRepo(repo, args)
424 for repo in json.loads(known_good.read())['repos']
425 ]
426
427
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600428def GetInstallNames(args):
Karl Schultz5ff6f742018-06-21 17:38:56 -0600429 """Returns the install names list.
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600430
Karl Schultz5ff6f742018-06-21 17:38:56 -0600431 The known-good file is expected to be in the same
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600432 directory as this script unless overridden by the 'known_good_dir'
433 parameter.
Karl Schultz5ff6f742018-06-21 17:38:56 -0600434 """
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600435 if args.known_good_dir:
436 known_good_file = os.path.join(os.path.abspath(args.known_good_dir),
437 KNOWN_GOOD_FILE_NAME)
438 else:
439 known_good_file = os.path.join(
440 os.path.dirname(os.path.abspath(__file__)), KNOWN_GOOD_FILE_NAME)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600441 with open(known_good_file) as known_good:
442 return json.loads(known_good.read())['install_names']
443
444
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600445def CreateHelper(args, repos, filename):
Karl Schultz5ff6f742018-06-21 17:38:56 -0600446 """Create a CMake config helper file.
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600447
Karl Schultz5ff6f742018-06-21 17:38:56 -0600448 The helper file is intended to be used with 'cmake -C <file>'
449 to build this home repo using the dependencies built by this script.
450
451 The install_names dictionary represents the CMake variables used by the
452 home repo to locate the install dirs of the dependent repos.
453 This information is baked into the CMake files of the home repo and so
454 this dictionary is kept with the repo via the json file.
455 """
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600456 install_names = GetInstallNames(args)
Karl Schultz5ff6f742018-06-21 17:38:56 -0600457 with open(filename, 'w') as helper_file:
458 for repo in repos:
459 if repo.name in install_names:
460 helper_file.write('set({var} "{dir}" CACHE STRING "" FORCE)\n'
461 .format(
462 var=install_names[repo.name],
463 dir=repo.install_dir))
464
465
466def main():
467 parser = argparse.ArgumentParser(
468 description='Get and build dependent repos at known-good commits')
469 parser.add_argument(
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600470 '--known_good_dir',
471 dest='known_good_dir',
472 help="Specify directory for known_good.json file.")
473 parser.add_argument(
Karl Schultz5ff6f742018-06-21 17:38:56 -0600474 '--dir',
475 dest='dir',
476 default='.',
477 help="Set target directory for repository roots. Default is \'.\'.")
478 parser.add_argument(
479 '--ref',
480 dest='ref',
481 default='',
482 help="Override 'commit' with git reference. E.g., 'origin/master'")
483 parser.add_argument(
484 '--no-build',
485 dest='do_build',
486 action='store_false',
487 help=
488 "Clone/update repositories and generate build files without performing compilation",
489 default=True)
490 parser.add_argument(
491 '--clean',
492 dest='do_clean',
493 action='store_true',
494 help="Clean files generated by compiler and linker before building",
495 default=False)
496 parser.add_argument(
497 '--clean-repo',
498 dest='do_clean_repo',
499 action='store_true',
500 help="Delete repository directory before building",
501 default=False)
502 parser.add_argument(
503 '--clean-build',
504 dest='do_clean_build',
505 action='store_true',
506 help="Delete build directory before building",
507 default=False)
508 parser.add_argument(
509 '--clean-install',
510 dest='do_clean_install',
511 action='store_true',
512 help="Delete install directory before building",
513 default=False)
514 parser.add_argument(
515 '--arch',
516 dest='arch',
517 choices=['32', '64', 'x86', 'x64', 'Win32', 'Win64'],
518 type=str.lower,
519 help="Set build files architecture (Windows)",
520 default='64')
521 parser.add_argument(
522 '--config',
523 dest='config',
524 choices=['debug', 'release', 'relwithdebinfo', 'minsizerel'],
525 type=str.lower,
526 help="Set build files configuration",
527 default='debug')
528
529 args = parser.parse_args()
530 save_cwd = os.getcwd()
531
532 # Create working "top" directory if needed
533 distutils.dir_util.mkpath(args.dir)
534 abs_top_dir = os.path.abspath(args.dir)
535
536 repos = GetGoodRepos(args)
537
538 print('Starting builds in {d}'.format(d=abs_top_dir))
539 for repo in repos:
540 # If the repo has a CI whitelist, skip the repo unless
541 # one of the CI's environment variable is set to true.
542 if len(repo.ci_only):
543 do_build = False
544 for env in repo.ci_only:
545 if not env in os.environ:
546 continue
547 if os.environ[env].lower() == 'true':
548 do_build = True
549 break
550 if not do_build:
551 continue
552
553 # Clone/update the repository
554 repo.Checkout()
555
556 # Build the repository
557 if args.do_build:
558 repo.Build(repos)
559
560 # Need to restore original cwd in order for CreateHelper to find json file
561 os.chdir(save_cwd)
Mark Lobodzinskiac1043c2018-07-19 13:20:46 -0600562 CreateHelper(args, repos, os.path.join(abs_top_dir, 'helper.cmake'))
Karl Schultz5ff6f742018-06-21 17:38:56 -0600563
564 sys.exit(0)
565
566
567if __name__ == '__main__':
568 main()