#!/usr/bin/python3 -B # Copyright 2018 Google LLC. All Rights Reserved. This file and proprietary # source code may only be used and distributed under the Widevine Master # License Agreement. """Generates build files and builds the CDM source release.""" # Lint as: python2, python3 from __future__ import print_function import argparse import math import os import subprocess import sys # pylint: disable=C6204 # Absolute path to Widevine CDM project repository. CDM_TOP_PATH = os.path.abspath(os.path.dirname(__file__)) # If gyp has been installed locally in third_party, this will find it. # Irrelevant if gyp has been installed globally. sys.path.insert(1, os.path.join(CDM_TOP_PATH, 'third_party')) import gyp # pylint: enable=C6204 PLATFORMS_DIR_PATH = os.path.join(CDM_TOP_PATH, 'platforms') OEMCRYPTO_FUZZTEST_DIR_PATH = os.path.join(CDM_TOP_PATH, 'oemcrypto', 'test', 'fuzz_tests') # Exit status for script if failure arises. EXIT_FAILURE = 1 def LoadFields(path): """Loads variables from an external python script. Attempts to load the specified python fields from the source file specified at the given |path|. Args: path: The path (absolute to relative) to the source file containing module to be loaded. Returns: The fields dictionary if it is successfully loaded. Otherwise returns None """ fields = {} with open(path) as f: exec(f.read(), fields, fields) # pylint: disable=W0122 return fields def IsNinjaInstalled(): """Determine if ninja is installed.""" try: if hasattr(subprocess, 'DEVNULL'): # Provides better compatibility for Windows, not available in Python 2.x dev_null = subprocess.DEVNULL else: dev_null = open(os.devnull, 'w') subprocess.check_call(['ninja', '--version'], stdout=dev_null, stderr=dev_null) return True except subprocess.CalledProcessError: # Error code returned, probably not the ninja we're looking for. return False except OSError: # No such command found. return False def PlatformExists(platform, print_details=False): """Determine if specified platform exists. Args: platform: Name of the platform (ex. "x86-64") print_details: An optional flag to enable printing of the reason the platform was determined to not exist. Does not print anything if the platform is valid. Returns: True if the there exists configuration files for the specified |platform|, False otherwise. """ target_path = os.path.join(PLATFORMS_DIR_PATH, platform) platform_gypi_path = os.path.join(target_path, 'settings.gypi') platform_environment_path = os.path.join(target_path, 'environment.py') def vprint(msg): if print_details: print(msg, file=sys.stderr) if not os.path.isdir(target_path): vprint(' Target path does not exist: platform = {}'.format(platform)) return False if not os.path.isfile(platform_gypi_path): vprint(' Target platform is missing settings.gypi file: settings_path = {}' .format(platform_gypi_path)) return False if not os.path.isfile(platform_environment_path): vprint( ' Target platform is missing environment.py file: env_path = {}'.format( platform_environment_path)) return False return True def RetrieveListOfPlatforms(): """Retrieves a list of names of the support platform. Returns: A list of strings containing the name of the platform """ if not os.path.isdir(PLATFORMS_DIR_PATH): print( 'Cannot find platforms directory: expected_path = {}'.format( PLATFORMS_DIR_PATH), file=sys.stderr) return [] return sorted(filter(PlatformExists, os.listdir(PLATFORMS_DIR_PATH))) def VerboseSubprocess(args): """Print sub-process command and execute.""" print(' Running: ' + ' '.join(args)) return subprocess.call(args) def RunMake(unused_output_path, build_config, job_limit, verbose): """Run Make as build system.""" os.environ['BUILDTYPE'] = build_config if job_limit is not None: if math.isinf(job_limit): job_args = ['-j'] else: job_args = ['-j', str(job_limit)] else: job_args = [] if verbose: job_args.append('-v') job_args.extend(['all', 'widevine_ce_cdm_shared']) return VerboseSubprocess(['make', '-C', CDM_TOP_PATH] + job_args) def RunNinja(output_path, build_config, job_limit, verbose): """Run Ninja as build system.""" build_path = os.path.join(output_path, build_config) if job_limit is not None: if math.isinf(job_limit): print('Ninja cannot run an infinite number of jobs', file=sys.stderr) print('Running at most 1000 jobs') job_limit = 1000 job_args = ['-j', str(job_limit)] else: job_args = [] if verbose: job_args += ['-v'] job_args += ['all', 'widevine_ce_cdm_shared'] return VerboseSubprocess(['ninja', '-C', build_path] + job_args) # Map from generator name to generator invocation function. BUILDERS = { 'make': RunMake, 'ninja': RunNinja, } def GetBuilder(generator): return BUILDERS.get(generator) def ImportPlatform(platform, gyp_args, build_fuzz_tests): """Handles platform-specific setup for the named platform. Computes platform-specific paths, sets gyp arguments for platform-specific gypis and output paths, imports a platform-specific module, and exports platform-specific environment variables. Args: platform: The name of the platform. gyp_args: An array of gyp arguments to which this function will append. build_fuzz_tests: True when we are building OEMCrypto fuzz tests. Returns: The path to the root of the build output. """ print(' Target Platform: ' + platform) assert PlatformExists(platform) target_path = os.path.join(PLATFORMS_DIR_PATH, platform) platform_gypi_path = os.path.join(target_path, 'settings.gypi') platform_environment_path = os.path.join(target_path, 'environment.py') output_path = os.path.join(CDM_TOP_PATH, 'out', platform) gyp_args.append('--include=' + platform_gypi_path) if build_fuzz_tests: fuzzer_settings_path = os.path.join(OEMCRYPTO_FUZZTEST_DIR_PATH, 'platforms/x86-64') fuzzer_settings_gypi_path = os.path.join(fuzzer_settings_path, 'fuzzer_settings.gypi') gyp_args.append('--include=' + fuzzer_settings_gypi_path) gyp_args.append('-Goutput_dir=' + output_path) platform_environment_path = os.path.join(target_path, 'environment.py') target = LoadFields(platform_environment_path) if 'export_variables' in target: for variable, value in target['export_variables'].items(): if not os.environ.get(variable): os.environ[variable] = value print(' set {} to {}'.format(variable, value)) return output_path def main(args): if IsNinjaInstalled(): print('ninja is installed - use ninja for default generator') default_generator = 'ninja' else: print('ninja is not installed - use make for default generator') default_generator = 'make' parser = argparse.ArgumentParser() parser.add_argument( 'platform', help=('The platform to target. To add a new platform, create a new ' 'platform directory with "environment.py" and "settings.gypi" ' 'files under platforms/.'), choices=RetrieveListOfPlatforms()) build_config_group = parser.add_mutually_exclusive_group(required=True) build_config_group.add_argument( '-d', '--debug', dest='build_config', action='store_const', const='debug', help='Build a debug build. This is shorthand for "--config debug".') build_config_group.add_argument( '-r', '--release', dest='build_config', action='store_const', const='release', help='Build a release build. This is shorthand for "--config release".') build_config_group.add_argument( '-c', '--config', dest='build_config', help=('Select a build configuration to use. Any configuration defined in ' 'the chosen platform\'s "settings.gypi" file may be used.')) parser.add_argument( '-g', '--generator', default=default_generator, help='Which build system to use. Defaults to {}.'.format( default_generator), choices=BUILDERS.keys()) parser.add_argument( '-j', '--jobs', nargs='?', const=float('inf'), type=int, help=('When building, run up to this many jobs in parallel. The default ' 'is the default for your chosen generator. If this flag is ' 'specified without a value, jobs will spawn without limit.')) parser.add_argument( '-D', '--define', action='append', default=[], help=('Pass variable definitions to GYP. (May be specified multiple ' 'times.)')) parser.add_argument( '-e', '--extra_gyp', action='append', default=[], help=('External GYP file that is processed after the standard GYP files. ' '(May be specified multiple times.)')) parser.add_argument( '-ft', '--fuzz_tests', action='store_true', help='Set this flag if you want to build fuzz tests.') parser.add_argument( '-v', '--verbose', action='store_true', help=('Print verbose build output, including verbose output from the ' 'generator.')) options = parser.parse_args(args) if not PlatformExists(options.platform, print_details=True): platforms = RetrieveListOfPlatforms() if platforms: print(' Available platforms: ' + ', '.join(platforms), file=sys.stderr) return EXIT_FAILURE gyp_args = [ '--format=' + options.generator, '--depth=' + CDM_TOP_PATH, ] if options.fuzz_tests: gyp_args.append( os.path.join(OEMCRYPTO_FUZZTEST_DIR_PATH, 'oemcrypto_fuzztests.gyp')) else: gyp_args.append(os.path.join(CDM_TOP_PATH, 'cdm', 'cdm_unittests.gyp')) for var in options.extra_gyp: gyp_args.append(var) for var in options.define: gyp_args.append('-D' + var) output_path = ImportPlatform(options.platform, gyp_args, options.fuzz_tests) print(' Running: {}'.format(' '.join(['gyp'] + gyp_args))) retval = gyp.main(gyp_args) if retval != 0: return retval # The gyp argument --build=xyz only works on newer versions of gyp and # ignores the generator flag output_dir (as of 2014-05-28 with ninja). # So instead of using --build, we simply invoke the build system ourselves. builder = GetBuilder(options.generator) if builder is None: print(' Cannot automatically build with this generator', file=sys.stderr) print(' Please start the build manually', file=sys.stderr) return EXIT_FAILURE return builder(output_path, options.build_config, options.jobs, options.verbose) if __name__ == '__main__': sys.exit(main(sys.argv[1:]))