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