1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Copyright 2020 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Produces a JSON object of `gn desc`'s output for each given arch. 8 9A full Chromium checkout is required in order to run this script. 10 11The result is of the form: 12{ 13 "arch1": { 14 "//gn:target": { 15 'configs": ["bar"], 16 "sources": ["foo"] 17 } 18 } 19} 20""" 21 22from __future__ import print_function 23 24import argparse 25import json 26import logging 27import os 28import subprocess 29import sys 30import tempfile 31 32 33def _find_chromium_root(search_from): 34 """Finds the chromium root directory from `search_from`.""" 35 current = search_from 36 while current != '/': 37 if os.path.isfile(os.path.join(current, '.gclient')): 38 return current 39 current = os.path.dirname(current) 40 raise ValueError( 41 "%s doesn't appear to be a Chromium subdirectory" % search_from) 42 43 44def _create_gn_args_for(arch): 45 """Creates a `gn args` listing for the given architecture.""" 46 # FIXME(gbiv): is_chromeos_device = True would be nice to support, as well. 47 # Requires playing nicely with SimpleChrome though, and this should be "close 48 # enough" for now. 49 return '\n'.join(( 50 'target_os = "chromeos"', 51 'target_cpu = "%s"' % arch, 52 'is_official_build = true', 53 'is_chrome_branded = true', 54 )) 55 56 57def _parse_gn_desc_output(output): 58 """Parses the output of `gn desc --format=json`. 59 60 Args: 61 output: a seekable file containing the JSON output of `gn desc`. 62 63 Returns: 64 A tuple of (warnings, gn_desc_json). 65 """ 66 warnings = [] 67 desc_json = None 68 while True: 69 start_pos = output.tell() 70 next_line = next(output, None) 71 if next_line is None: 72 raise ValueError('No JSON found in the given gn file') 73 74 if next_line.lstrip().startswith('{'): 75 output.seek(start_pos) 76 desc_json = json.load(output) 77 break 78 79 warnings.append(next_line) 80 81 return ''.join(warnings).strip(), desc_json 82 83 84def _run_gn_desc(in_dir, gn_args): 85 logging.info('Running `gn gen`...') 86 subprocess.check_call(['gn', 'gen', '.', '--args=' + gn_args], cwd=in_dir) 87 88 logging.info('Running `gn desc`...') 89 with tempfile.TemporaryFile(mode='r+', encoding='utf-8') as f: 90 gn_command = ['gn', 'desc', '--format=json', '.', '//*:*'] 91 exit_code = subprocess.call(gn_command, stdout=f, cwd=in_dir) 92 f.seek(0) 93 if exit_code: 94 logging.error('gn failed; stdout:\n%s', f.read()) 95 raise subprocess.CalledProcessError(exit_code, gn_command) 96 warnings, result = _parse_gn_desc_output(f) 97 98 if warnings: 99 logging.warning('Encountered warning(s) running `gn desc`:\n%s', warnings) 100 return result 101 102 103def _fix_result(rename_out, out_dir, chromium_root, gn_desc): 104 """Performs postprocessing on `gn desc` JSON.""" 105 result = {} 106 107 rel_out = '//' + os.path.relpath(out_dir, os.path.join(chromium_root, 'src')) 108 rename_out = rename_out if rename_out.endswith('/') else rename_out + '/' 109 110 def fix_source_file(f): 111 if not f.startswith(rel_out): 112 return f 113 return rename_out + f[len(rel_out) + 1:] 114 115 for target, info in gn_desc.items(): 116 sources = info.get('sources') 117 configs = info.get('configs') 118 if not sources or not configs: 119 continue 120 121 result[target] = { 122 'configs': configs, 123 'sources': [fix_source_file(f) for f in sources], 124 } 125 126 return result 127 128 129def main(args): 130 known_arches = [ 131 'arm', 132 'arm64', 133 'x64', 134 'x86', 135 ] 136 137 parser = argparse.ArgumentParser( 138 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 139 parser.add_argument( 140 'arch', 141 nargs='+', 142 help='Architecture(s) to fetch `gn desc`s for. ' 143 'Supported ones are %s' % known_arches) 144 parser.add_argument( 145 '--output', required=True, help='File to write results to.') 146 parser.add_argument( 147 '--chromium_out_dir', 148 required=True, 149 help='Chromium out/ directory for us to use. This directory will ' 150 'be clobbered by this script.') 151 parser.add_argument( 152 '--rename_out', 153 default='//out', 154 help='Directory to rename files in --chromium_out_dir to. ' 155 'Default: %(default)s') 156 opts = parser.parse_args(args) 157 158 logging.basicConfig( 159 format='%(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: %(message)s', 160 level=logging.INFO, 161 ) 162 163 arches = opts.arch 164 rename_out = opts.rename_out 165 for arch in arches: 166 if arch not in known_arches: 167 parser.error( 168 'unknown architecture: %s; try one of %s' % (arch, known_arches)) 169 170 results_file = os.path.realpath(opts.output) 171 out_dir = os.path.realpath(opts.chromium_out_dir) 172 chromium_root = _find_chromium_root(out_dir) 173 174 os.makedirs(out_dir, exist_ok=True) 175 results = {} 176 for arch in arches: 177 logging.info('Getting `gn` desc for %s...', arch) 178 179 results[arch] = _fix_result( 180 rename_out, out_dir, chromium_root, 181 _run_gn_desc( 182 in_dir=out_dir, 183 gn_args=_create_gn_args_for(arch), 184 )) 185 186 os.makedirs(os.path.dirname(results_file), exist_ok=True) 187 188 results_intermed = results_file + '.tmp' 189 with open(results_intermed, 'w', encoding='utf-8') as f: 190 json.dump(results, f) 191 os.rename(results_intermed, results_file) 192 193 194if __name__ == '__main__': 195 sys.exit(main(sys.argv[1:])) 196