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