xref: /aosp_15_r20/external/toolchain-utils/cwp/cr-os/fetch_gn_descs.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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