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