PageRenderTime 46ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/third_party/crashpad/crashpad/build/ios/setup_ios_gn.py

https://github.com/chromium/chromium
Python | 405 lines | 369 code | 15 blank | 21 comment | 8 complexity | b68f72bb6dc0adb1a21a1cec5e43f8be MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause
  1. #!/usr/bin/env python3
  2. # Copyright 2020 The Crashpad Authors. All rights reserved.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import argparse
  16. import configparser
  17. import convert_gn_xcodeproj
  18. import errno
  19. import io
  20. import os
  21. import platform
  22. import re
  23. import shutil
  24. import subprocess
  25. import sys
  26. import tempfile
  27. SUPPORTED_TARGETS = ('iphoneos', 'iphonesimulator', 'maccatalyst')
  28. SUPPORTED_CONFIGS = ('Debug', 'Release', 'Profile', 'Official')
  29. # Pattern matching lines from ~/.lldbinit that must not be copied to the
  30. # generated .lldbinit file. They match what the user were told to add to
  31. # their global ~/.lldbinit file before setup-gn.py was updated to generate
  32. # a project specific file and thus must not be copied as they would cause
  33. # the settings to be overwritten.
  34. LLDBINIT_SKIP_PATTERNS = (
  35. re.compile('^script sys.path\\[:0\\] = \\[\'.*/src/tools/lldb\'\\]$'),
  36. re.compile('^script import lldbinit$'),
  37. re.compile('^settings append target.source-map .* /google/src/.*$'),
  38. )
  39. def HostCpuArch():
  40. '''Returns the arch of the host cpu for GN.'''
  41. HOST_CPU_ARCH = {
  42. 'arm64': '"arm64"',
  43. 'x86_64': '"x64"',
  44. }
  45. return HOST_CPU_ARCH[platform.machine()]
  46. class ConfigParserWithStringInterpolation(configparser.ConfigParser):
  47. '''A .ini file parser that supports strings and environment variables.'''
  48. ENV_VAR_PATTERN = re.compile(r'\$([A-Za-z0-9_]+)')
  49. def values(self, section):
  50. return filter(
  51. lambda val: val != '',
  52. map(lambda kv: self._UnquoteString(self._ExpandEnvVar(kv[1])),
  53. configparser.ConfigParser.items(self, section)))
  54. def getstring(self, section, option, fallback=''):
  55. try:
  56. raw_value = self.get(section, option)
  57. except configparser.NoOptionError:
  58. return fallback
  59. return self._UnquoteString(self._ExpandEnvVar(raw_value))
  60. def _UnquoteString(self, string):
  61. if not string or string[0] != '"' or string[-1] != '"':
  62. return string
  63. return string[1:-1]
  64. def _ExpandEnvVar(self, value):
  65. match = self.ENV_VAR_PATTERN.search(value)
  66. if not match:
  67. return value
  68. name, (begin, end) = match.group(1), match.span(0)
  69. prefix, suffix = value[:begin], self._ExpandEnvVar(value[end:])
  70. return prefix + os.environ.get(name, '') + suffix
  71. class GnGenerator(object):
  72. '''Holds configuration for a build and method to generate gn default files.'''
  73. FAT_BUILD_DEFAULT_ARCH = '64-bit'
  74. TARGET_CPU_VALUES = {
  75. 'iphoneos': '"arm64"',
  76. 'iphonesimulator': HostCpuArch(),
  77. 'maccatalyst': HostCpuArch(),
  78. }
  79. TARGET_ENVIRONMENT_VALUES = {
  80. 'iphoneos': '"device"',
  81. 'iphonesimulator': '"simulator"',
  82. 'maccatalyst': '"catalyst"'
  83. }
  84. def __init__(self, settings, config, target):
  85. assert target in SUPPORTED_TARGETS
  86. assert config in SUPPORTED_CONFIGS
  87. self._settings = settings
  88. self._config = config
  89. self._target = target
  90. def _GetGnArgs(self, extra_args=None):
  91. """Build the list of arguments to pass to gn.
  92. Returns:
  93. A list of tuple containing gn variable names and variable values (it
  94. is not a dictionary as the order needs to be preserved).
  95. """
  96. args = []
  97. is_debug = self._config == 'Debug'
  98. official = self._config == 'Official'
  99. is_optim = self._config in ('Profile', 'Official')
  100. args.append(('target_os', '"ios"'))
  101. args.append(('is_debug', is_debug))
  102. if os.environ.get('FORCE_MAC_TOOLCHAIN', '0') == '1':
  103. args.append(('use_system_xcode', False))
  104. args.append(('target_cpu', self.TARGET_CPU_VALUES[self._target]))
  105. args.append((
  106. 'target_environment',
  107. self.TARGET_ENVIRONMENT_VALUES[self._target]))
  108. # If extra arguments are passed to the function, pass them before the
  109. # user overrides (if any).
  110. if extra_args is not None:
  111. args.extend(extra_args)
  112. # Add user overrides after the other configurations so that they can
  113. # refer to them and override them.
  114. args.extend(self._settings.items('gn_args'))
  115. return args
  116. def Generate(self, gn_path, proj_name, root_path, build_dir):
  117. self.WriteArgsGn(build_dir, xcode_project_name=proj_name)
  118. subprocess.check_call(self.GetGnCommand(
  119. gn_path, root_path, build_dir, xcode_project_name=proj_name))
  120. def CreateGnRules(self, gn_path, root_path, build_dir):
  121. gn_command = self.GetGnCommand(gn_path, root_path, build_dir)
  122. self.WriteArgsGn(build_dir)
  123. self.WriteBuildNinja(gn_command, build_dir)
  124. self.WriteBuildNinjaDeps(build_dir)
  125. def WriteArgsGn(self, build_dir, xcode_project_name=None):
  126. with open(os.path.join(build_dir, 'args.gn'), 'w') as stream:
  127. stream.write('# This file was generated by setup-gn.py. Do not edit\n')
  128. stream.write('# but instead use ~/.setup-gn or $repo/.setup-gn files\n')
  129. stream.write('# to configure settings.\n')
  130. stream.write('\n')
  131. if self._target != 'maccatalyst':
  132. if self._settings.has_section('$imports$'):
  133. for import_rule in self._settings.values('$imports$'):
  134. stream.write('import("%s")\n' % import_rule)
  135. stream.write('\n')
  136. gn_args = self._GetGnArgs()
  137. for name, value in gn_args:
  138. if isinstance(value, bool):
  139. stream.write('%s = %s\n' % (name, str(value).lower()))
  140. elif isinstance(value, list):
  141. stream.write('%s = [%s' % (name, '\n' if len(value) > 1 else ''))
  142. if len(value) == 1:
  143. prefix = ' '
  144. suffix = ' '
  145. else:
  146. prefix = ' '
  147. suffix = ',\n'
  148. for item in value:
  149. if isinstance(item, bool):
  150. stream.write('%s%s%s' % (prefix, str(item).lower(), suffix))
  151. else:
  152. stream.write('%s%s%s' % (prefix, item, suffix))
  153. stream.write(']\n')
  154. else:
  155. # ConfigParser removes quote around empty string which confuse
  156. # `gn gen` so restore them.
  157. if not value:
  158. value = '""'
  159. stream.write('%s = %s\n' % (name, value))
  160. def WriteBuildNinja(self, gn_command, build_dir):
  161. with open(os.path.join(build_dir, 'build.ninja'), 'w') as stream:
  162. stream.write('ninja_required_version = 1.7.2\n')
  163. stream.write('\n')
  164. stream.write('rule gn\n')
  165. stream.write(' command = %s\n' % NinjaEscapeCommand(gn_command))
  166. stream.write(' description = Regenerating ninja files\n')
  167. stream.write('\n')
  168. stream.write('build build.ninja: gn\n')
  169. stream.write(' generator = 1\n')
  170. stream.write(' depfile = build.ninja.d\n')
  171. def WriteBuildNinjaDeps(self, build_dir):
  172. with open(os.path.join(build_dir, 'build.ninja.d'), 'w') as stream:
  173. stream.write('build.ninja: nonexistant_file.gn\n')
  174. def GetGnCommand(self, gn_path, src_path, out_path, xcode_project_name=None):
  175. gn_command = [ gn_path, '--root=%s' % os.path.realpath(src_path), '-q' ]
  176. if xcode_project_name is not None:
  177. gn_command.append('--ide=xcode')
  178. gn_command.append('--ninja-executable=autoninja')
  179. gn_command.append('--xcode-build-system=new')
  180. gn_command.append('--xcode-project=%s' % xcode_project_name)
  181. if self._settings.has_section('filters'):
  182. target_filters = self._settings.values('filters')
  183. if target_filters:
  184. gn_command.append('--filters=%s' % ';'.join(target_filters))
  185. else:
  186. gn_command.append('--check')
  187. gn_command.append('gen')
  188. gn_command.append('//%s' %
  189. os.path.relpath(os.path.abspath(out_path), os.path.abspath(src_path)))
  190. return gn_command
  191. def NinjaNeedEscape(arg):
  192. '''Returns True if |arg| needs to be escaped when written to .ninja file.'''
  193. return ':' in arg or '*' in arg or ';' in arg
  194. def NinjaEscapeCommand(command):
  195. '''Escapes |command| in order to write it to .ninja file.'''
  196. result = []
  197. for arg in command:
  198. if NinjaNeedEscape(arg):
  199. arg = arg.replace(':', '$:')
  200. arg = arg.replace(';', '\\;')
  201. arg = arg.replace('*', '\\*')
  202. else:
  203. result.append(arg)
  204. return ' '.join(result)
  205. def FindGn():
  206. '''Returns absolute path to gn binary looking at the PATH env variable.'''
  207. for path in os.environ['PATH'].split(os.path.pathsep):
  208. gn_path = os.path.join(path, 'gn')
  209. if os.path.isfile(gn_path) and os.access(gn_path, os.X_OK):
  210. return gn_path
  211. return None
  212. def GenerateXcodeProject(gn_path, root_dir, proj_name, out_dir, settings):
  213. '''Generate Xcode project with Xcode and convert to multi-configurations.'''
  214. prefix = os.path.abspath(os.path.join(out_dir, '_temp'))
  215. temp_path = tempfile.mkdtemp(prefix=prefix)
  216. try:
  217. generator = GnGenerator(settings, 'Debug', 'iphonesimulator')
  218. generator.Generate(gn_path, proj_name, root_dir, temp_path)
  219. convert_gn_xcodeproj.ConvertGnXcodeProject(
  220. root_dir,
  221. '%s.xcodeproj' % proj_name,
  222. os.path.join(temp_path),
  223. os.path.join(out_dir, 'build'),
  224. SUPPORTED_CONFIGS)
  225. finally:
  226. if os.path.exists(temp_path):
  227. shutil.rmtree(temp_path)
  228. def CreateLLDBInitFile(root_dir, out_dir, settings):
  229. '''
  230. Generate an .lldbinit file for the project that load the script that fixes
  231. the mapping of source files (see docs/ios/build_instructions.md#debugging).
  232. '''
  233. with open(os.path.join(out_dir, 'build', '.lldbinit'), 'w') as lldbinit:
  234. lldb_script_dir = os.path.join(os.path.abspath(root_dir), 'tools', 'lldb')
  235. lldbinit.write('script sys.path[:0] = [\'%s\']\n' % lldb_script_dir)
  236. lldbinit.write('script import lldbinit\n')
  237. workspace_name = settings.getstring(
  238. 'gn_args',
  239. 'ios_internal_citc_workspace_name')
  240. if workspace_name != '':
  241. username = os.environ['USER']
  242. for shortname in ('googlemac', 'third_party', 'blaze-out'):
  243. lldbinit.write('settings append target.source-map %s %s\n' % (
  244. shortname,
  245. '/google/src/cloud/%s/%s/google3/%s' % (
  246. username, workspace_name, shortname)))
  247. # Append the content of //ios/build/tools/lldbinit.defaults if it exists.
  248. tools_dir = os.path.join(root_dir, 'ios', 'build', 'tools')
  249. defaults_lldbinit_path = os.path.join(tools_dir, 'lldbinit.defaults')
  250. if os.path.isfile(defaults_lldbinit_path):
  251. with open(defaults_lldbinit_path) as defaults_lldbinit:
  252. for line in defaults_lldbinit:
  253. lldbinit.write(line)
  254. # Append the content of ~/.lldbinit if it exists. Line that look like they
  255. # are trying to configure source mapping are skipped as they probably date
  256. # back from when setup-gn.py was not generating an .lldbinit file.
  257. global_lldbinit_path = os.path.join(os.environ['HOME'], '.lldbinit')
  258. if os.path.isfile(global_lldbinit_path):
  259. with open(global_lldbinit_path) as global_lldbinit:
  260. for line in global_lldbinit:
  261. if any(pattern.match(line) for pattern in LLDBINIT_SKIP_PATTERNS):
  262. continue
  263. lldbinit.write(line)
  264. def GenerateGnBuildRules(gn_path, root_dir, out_dir, settings):
  265. '''Generates all template configurations for gn.'''
  266. for config in SUPPORTED_CONFIGS:
  267. for target in SUPPORTED_TARGETS:
  268. build_dir = os.path.join(out_dir, '%s-%s' % (config, target))
  269. if not os.path.isdir(build_dir):
  270. os.makedirs(build_dir)
  271. generator = GnGenerator(settings, config, target)
  272. generator.CreateGnRules(gn_path, root_dir, build_dir)
  273. def Main(args):
  274. default_root = os.path.normpath(os.path.join(
  275. os.path.dirname(__file__), os.pardir, os.pardir))
  276. parser = argparse.ArgumentParser(
  277. description='Generate build directories for use with gn.')
  278. parser.add_argument(
  279. 'root', default=default_root, nargs='?',
  280. help='root directory where to generate multiple out configurations')
  281. parser.add_argument(
  282. '--import', action='append', dest='import_rules', default=[],
  283. help='path to file defining default gn variables')
  284. parser.add_argument(
  285. '--gn-path', default=None,
  286. help='path to gn binary (default: look up in $PATH)')
  287. parser.add_argument(
  288. '--build-dir', default='out',
  289. help='path where the build should be created (default: %(default)s)')
  290. parser.add_argument(
  291. '--config-path', default=os.path.expanduser('~/.setup-gn'),
  292. help='path to the user config file (default: %(default)s)')
  293. parser.add_argument(
  294. '--system-config-path', default=os.path.splitext(__file__)[0] + '.config',
  295. help='path to the default config file (default: %(default)s)')
  296. parser.add_argument(
  297. '--project-name', default='all', dest='proj_name',
  298. help='name of the generated Xcode project (default: %(default)s)')
  299. parser.add_argument(
  300. '--no-xcode-project', action='store_true', default=False,
  301. help='do not generate the build directory with XCode project')
  302. args = parser.parse_args(args)
  303. # Load configuration (first global and then any user overrides).
  304. settings = ConfigParserWithStringInterpolation()
  305. settings.read([
  306. args.system_config_path,
  307. args.config_path,
  308. ])
  309. # Add private sections corresponding to --import argument.
  310. if args.import_rules:
  311. settings.add_section('$imports$')
  312. for i, import_rule in enumerate(args.import_rules):
  313. if not import_rule.startswith('//'):
  314. import_rule = '//%s' % os.path.relpath(
  315. os.path.abspath(import_rule), os.path.abspath(args.root))
  316. settings.set('$imports$', '$rule%d$' % i, import_rule)
  317. # Validate settings.
  318. if settings.getstring('build', 'arch') not in ('64-bit', '32-bit', 'fat'):
  319. sys.stderr.write('ERROR: invalid value for build.arch: %s\n' %
  320. settings.getstring('build', 'arch'))
  321. sys.exit(1)
  322. # Find path to gn binary either from command-line or in PATH.
  323. if args.gn_path:
  324. gn_path = args.gn_path
  325. else:
  326. gn_path = FindGn()
  327. if gn_path is None:
  328. sys.stderr.write('ERROR: cannot find gn in PATH\n')
  329. sys.exit(1)
  330. out_dir = os.path.join(args.root, args.build_dir)
  331. if not os.path.isdir(out_dir):
  332. os.makedirs(out_dir)
  333. if not args.no_xcode_project:
  334. GenerateXcodeProject(gn_path, args.root, args.proj_name, out_dir, settings)
  335. CreateLLDBInitFile(args.root, out_dir, settings)
  336. GenerateGnBuildRules(gn_path, args.root, out_dir, settings)
  337. if __name__ == '__main__':
  338. sys.exit(Main(sys.argv[1:]))