349 lines
12 KiB
Python
349 lines
12 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2020 The Crashpad Authors. All rights reserved.
|
|
#
|
|
# 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.
|
|
|
|
import argparse
|
|
import convert_gn_xcodeproj
|
|
import errno
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
try:
|
|
import configparser
|
|
except ImportError:
|
|
import ConfigParser as configparser
|
|
|
|
try:
|
|
import StringIO as io
|
|
except ImportError:
|
|
import io
|
|
|
|
SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator')
|
|
SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official', 'Coverage')
|
|
|
|
|
|
class ConfigParserWithStringInterpolation(configparser.SafeConfigParser):
|
|
'''A .ini file parser that supports strings and environment variables.'''
|
|
|
|
ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)')
|
|
|
|
def values(self, section):
|
|
return map(lambda kv: self._UnquoteString(self._ExpandEnvVar(kv[1])),
|
|
configparser.ConfigParser.items(self, section))
|
|
|
|
def getstring(self, section, option):
|
|
return self._UnquoteString(self._ExpandEnvVar(self.get(section,
|
|
option)))
|
|
|
|
def _UnquoteString(self, string):
|
|
if not string or string[0] != '"' or string[-1] != '"':
|
|
return string
|
|
return string[1:-1]
|
|
|
|
def _ExpandEnvVar(self, value):
|
|
match = self.ENV_VAR_PATTERN.search(value)
|
|
if not match:
|
|
return value
|
|
name, (begin, end) = match.group(1), match.span(0)
|
|
prefix, suffix = value[:begin], self._ExpandEnvVar(value[end:])
|
|
return prefix + os.environ.get(name, '') + suffix
|
|
|
|
|
|
class GnGenerator(object):
|
|
'''Holds configuration for a build and method to generate gn default
|
|
files.'''
|
|
|
|
FAT_BUILD_DEFAULT_ARCH = '64-bit'
|
|
|
|
TARGET_CPU_VALUES = {
|
|
'iphoneos': {
|
|
'32-bit': '"arm"',
|
|
'64-bit': '"arm64"',
|
|
},
|
|
'iphonesimulator': {
|
|
'32-bit': '"x86"',
|
|
'64-bit': '"x64"',
|
|
}
|
|
}
|
|
|
|
def __init__(self, settings, config, target):
|
|
assert target in SUPPORTED_TARGETS
|
|
assert config in SUPPORTED_CONFIGS
|
|
self._settings = settings
|
|
self._config = config
|
|
self._target = target
|
|
|
|
def _GetGnArgs(self):
|
|
"""Build the list of arguments to pass to gn.
|
|
|
|
Returns:
|
|
A list of tuple containing gn variable names and variable values (it
|
|
is not a dictionary as the order needs to be preserved).
|
|
"""
|
|
args = []
|
|
|
|
args.append(('is_debug', self._config in ('Debug', 'Coverage')))
|
|
|
|
if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1':
|
|
args.append(('use_system_xcode', False))
|
|
|
|
cpu_values = self.TARGET_CPU_VALUES[self._target]
|
|
build_arch = self._settings.getstring('build', 'arch')
|
|
if build_arch == 'fat':
|
|
target_cpu = cpu_values[self.FAT_BUILD_DEFAULT_ARCH]
|
|
args.append(('target_cpu', target_cpu))
|
|
args.append(
|
|
('additional_target_cpus',
|
|
[cpu for cpu in cpu_values.itervalues() if cpu != target_cpu]))
|
|
else:
|
|
args.append(('target_cpu', cpu_values[build_arch]))
|
|
|
|
# Add user overrides after the other configurations so that they can
|
|
# refer to them and override them.
|
|
args.extend(self._settings.items('gn_args'))
|
|
return args
|
|
|
|
def Generate(self, gn_path, root_path, out_path):
|
|
buf = io.StringIO()
|
|
self.WriteArgsGn(buf)
|
|
WriteToFileIfChanged(os.path.join(out_path, 'args.gn'),
|
|
buf.getvalue(),
|
|
overwrite=True)
|
|
|
|
subprocess.check_call(
|
|
self.GetGnCommand(gn_path, root_path, out_path, True))
|
|
|
|
def CreateGnRules(self, gn_path, root_path, out_path):
|
|
buf = io.StringIO()
|
|
self.WriteArgsGn(buf)
|
|
WriteToFileIfChanged(os.path.join(out_path, 'args.gn'),
|
|
buf.getvalue(),
|
|
overwrite=True)
|
|
|
|
buf = io.StringIO()
|
|
gn_command = self.GetGnCommand(gn_path, root_path, out_path, False)
|
|
self.WriteBuildNinja(buf, gn_command)
|
|
WriteToFileIfChanged(os.path.join(out_path, 'build.ninja'),
|
|
buf.getvalue(),
|
|
overwrite=False)
|
|
|
|
buf = io.StringIO()
|
|
self.WriteBuildNinjaDeps(buf)
|
|
WriteToFileIfChanged(os.path.join(out_path, 'build.ninja.d'),
|
|
buf.getvalue(),
|
|
overwrite=False)
|
|
|
|
def WriteArgsGn(self, stream):
|
|
stream.write('# This file was generated by setup-gn.py. Do not edit\n')
|
|
stream.write('# but instead use ~/.setup-gn or $repo/.setup-gn files\n')
|
|
stream.write('# to configure settings.\n')
|
|
stream.write('\n')
|
|
|
|
if self._settings.has_section('$imports$'):
|
|
for import_rule in self._settings.values('$imports$'):
|
|
stream.write('import("%s")\n' % import_rule)
|
|
stream.write('\n')
|
|
|
|
gn_args = self._GetGnArgs()
|
|
for name, value in gn_args:
|
|
if isinstance(value, bool):
|
|
stream.write('%s = %s\n' % (name, str(value).lower()))
|
|
elif isinstance(value, list):
|
|
stream.write('%s = [%s' %
|
|
(name, '\n' if len(value) > 1 else ''))
|
|
if len(value) == 1:
|
|
prefix = ' '
|
|
suffix = ' '
|
|
else:
|
|
prefix = ' '
|
|
suffix = ',\n'
|
|
for item in value:
|
|
if isinstance(item, bool):
|
|
stream.write('%s%s%s' %
|
|
(prefix, str(item).lower(), suffix))
|
|
else:
|
|
stream.write('%s%s%s' % (prefix, item, suffix))
|
|
stream.write(']\n')
|
|
else:
|
|
stream.write('%s = %s\n' % (name, value))
|
|
|
|
def WriteBuildNinja(self, stream, gn_command):
|
|
stream.write('rule gn\n')
|
|
stream.write(' command = %s\n' % NinjaEscapeCommand(gn_command))
|
|
stream.write(' description = Regenerating ninja files\n')
|
|
stream.write('\n')
|
|
stream.write('build build.ninja: gn\n')
|
|
stream.write(' generator = 1\n')
|
|
stream.write(' depfile = build.ninja.d\n')
|
|
|
|
def WriteBuildNinjaDeps(self, stream):
|
|
stream.write('build.ninja: nonexistant_file.gn\n')
|
|
|
|
def GetGnCommand(self, gn_path, src_path, out_path, generate_xcode_project):
|
|
gn_command = [gn_path, '--root=%s' % os.path.realpath(src_path), '-q']
|
|
if generate_xcode_project:
|
|
gn_command.append('--ide=xcode')
|
|
gn_command.append('--ninja-executable=autoninja')
|
|
if self._settings.has_section('filters'):
|
|
target_filters = self._settings.values('filters')
|
|
if target_filters:
|
|
gn_command.append('--filters=%s' % ';'.join(target_filters))
|
|
else:
|
|
gn_command.append('--check')
|
|
gn_command.append('gen')
|
|
gn_command.append('//%s' % os.path.relpath(os.path.abspath(out_path),
|
|
os.path.abspath(src_path)))
|
|
return gn_command
|
|
|
|
|
|
def WriteToFileIfChanged(filename, content, overwrite):
|
|
'''Write |content| to |filename| if different. If |overwrite| is False
|
|
and the file already exists it is left untouched.'''
|
|
if os.path.exists(filename):
|
|
if not overwrite:
|
|
return
|
|
with open(filename) as file:
|
|
if file.read() == content:
|
|
return
|
|
if not os.path.isdir(os.path.dirname(filename)):
|
|
os.makedirs(os.path.dirname(filename))
|
|
with open(filename, 'w') as file:
|
|
file.write(content)
|
|
|
|
|
|
def NinjaNeedEscape(arg):
|
|
'''Returns True if |arg| needs to be escaped when written to .ninja file.'''
|
|
return ':' in arg or '*' in arg or ';' in arg
|
|
|
|
|
|
def NinjaEscapeCommand(command):
|
|
'''Escapes |command| in order to write it to .ninja file.'''
|
|
result = []
|
|
for arg in command:
|
|
if NinjaNeedEscape(arg):
|
|
arg = arg.replace(':', '$:')
|
|
arg = arg.replace(';', '\\;')
|
|
arg = arg.replace('*', '\\*')
|
|
else:
|
|
result.append(arg)
|
|
return ' '.join(result)
|
|
|
|
|
|
def FindGn():
|
|
'''Returns absolute path to gn binary looking at the PATH env variable.'''
|
|
for path in os.environ['PATH'].split(os.path.pathsep):
|
|
gn_path = os.path.join(path, 'gn')
|
|
if os.path.isfile(gn_path) and os.access(gn_path, os.X_OK):
|
|
return gn_path
|
|
return None
|
|
|
|
|
|
def GenerateXcodeProject(gn_path, root_dir, out_dir, settings):
|
|
'''Convert GN generated Xcode project into multi-configuration Xcode
|
|
project.'''
|
|
|
|
temp_path = tempfile.mkdtemp(
|
|
prefix=os.path.abspath(os.path.join(out_dir, '_temp')))
|
|
try:
|
|
generator = GnGenerator(settings, 'Debug', 'iphonesimulator')
|
|
generator.Generate(gn_path, root_dir, temp_path)
|
|
convert_gn_xcodeproj.ConvertGnXcodeProject(
|
|
root_dir, os.path.join(temp_path), os.path.join(out_dir, 'build'),
|
|
SUPPORTED_CONFIGS)
|
|
finally:
|
|
if os.path.exists(temp_path):
|
|
shutil.rmtree(temp_path)
|
|
|
|
|
|
def GenerateGnBuildRules(gn_path, root_dir, out_dir, settings):
|
|
'''Generates all template configurations for gn.'''
|
|
for config in SUPPORTED_CONFIGS:
|
|
for target in SUPPORTED_TARGETS:
|
|
build_dir = os.path.join(out_dir, '%s-%s' % (config, target))
|
|
generator = GnGenerator(settings, config, target)
|
|
generator.CreateGnRules(gn_path, root_dir, build_dir)
|
|
|
|
|
|
def Main(args):
|
|
default_root = os.path.normpath(
|
|
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description='Generate build directories for use with gn.')
|
|
parser.add_argument(
|
|
'root',
|
|
default=default_root,
|
|
nargs='?',
|
|
help='root directory where to generate multiple out configurations')
|
|
parser.add_argument('--import',
|
|
action='append',
|
|
dest='import_rules',
|
|
default=[],
|
|
help='path to file defining default gn variables')
|
|
parser.add_argument('--gn-path',
|
|
default=None,
|
|
help='path to gn binary (default: look up in $PATH)')
|
|
parser.add_argument(
|
|
'--build-dir',
|
|
default='out',
|
|
help='path where the build should be created (default: %(default)s)')
|
|
args = parser.parse_args(args)
|
|
|
|
# Load configuration (first global and then any user overrides).
|
|
settings = ConfigParserWithStringInterpolation()
|
|
settings.read([
|
|
os.path.splitext(__file__)[0] + '.config',
|
|
os.path.expanduser('~/.setup-gn'),
|
|
])
|
|
|
|
# Add private sections corresponding to --import argument.
|
|
if args.import_rules:
|
|
settings.add_section('$imports$')
|
|
for i, import_rule in enumerate(args.import_rules):
|
|
if not import_rule.startswith('//'):
|
|
import_rule = '//%s' % os.path.relpath(
|
|
os.path.abspath(import_rule), os.path.abspath(args.root))
|
|
settings.set('$imports$', '$rule%d$' % i, import_rule)
|
|
|
|
# Validate settings.
|
|
if settings.getstring('build', 'arch') not in ('64-bit', '32-bit', 'fat'):
|
|
sys.stderr.write('ERROR: invalid value for build.arch: %s\n' %
|
|
settings.getstring('build', 'arch'))
|
|
sys.exit(1)
|
|
|
|
# Find path to gn binary either from command-line or in PATH.
|
|
if args.gn_path:
|
|
gn_path = args.gn_path
|
|
else:
|
|
gn_path = FindGn()
|
|
if gn_path is None:
|
|
sys.stderr.write('ERROR: cannot find gn in PATH\n')
|
|
sys.exit(1)
|
|
|
|
out_dir = os.path.join(args.root, args.build_dir)
|
|
if not os.path.isdir(out_dir):
|
|
os.makedirs(out_dir)
|
|
|
|
GenerateXcodeProject(gn_path, args.root, out_dir, settings)
|
|
GenerateGnBuildRules(gn_path, args.root, out_dir, settings)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(Main(sys.argv[1:]))
|