xref: /aosp_15_r20/external/toolchain-utils/pgo_tools/pgo_tools.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li# Copyright 2023 The ChromiumOS Authors
2*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
3*760c253cSXin Li# found in the LICENSE file.
4*760c253cSXin Li
5*760c253cSXin Li"""A collection of tools used by the PGO scripts here."""
6*760c253cSXin Li
7*760c253cSXin Liimport contextlib
8*760c253cSXin Liimport logging
9*760c253cSXin Liimport os
10*760c253cSXin Lifrom pathlib import Path
11*760c253cSXin Liimport re
12*760c253cSXin Liimport shlex
13*760c253cSXin Liimport subprocess
14*760c253cSXin Liimport sys
15*760c253cSXin Liimport tempfile
16*760c253cSXin Lifrom typing import Any, Dict, Generator, IO, List, Optional, Union
17*760c253cSXin Li
18*760c253cSXin Li
19*760c253cSXin LiCommand = List[Union[str, Path]]
20*760c253cSXin Li
21*760c253cSXin Li
22*760c253cSXin Lidef run(
23*760c253cSXin Li    command: Command,
24*760c253cSXin Li    cwd: Optional[Path] = None,
25*760c253cSXin Li    check: bool = True,
26*760c253cSXin Li    extra_env: Optional[Dict[str, str]] = None,
27*760c253cSXin Li    stdout: Union[IO[Any], int, None] = None,
28*760c253cSXin Li    stderr: Union[IO[Any], int, None] = None,
29*760c253cSXin Li) -> subprocess.CompletedProcess:
30*760c253cSXin Li    """Convenient wrapper around subprocess.run."""
31*760c253cSXin Li    if extra_env:
32*760c253cSXin Li        env = dict(os.environ)
33*760c253cSXin Li        env.update(extra_env)
34*760c253cSXin Li    else:
35*760c253cSXin Li        env = None
36*760c253cSXin Li
37*760c253cSXin Li    if logging.getLogger().isEnabledFor(logging.DEBUG):
38*760c253cSXin Li        c = shlex.join(str(x) for x in command)
39*760c253cSXin Li        dir_extra = f" in {cwd}" if cwd is not None else ""
40*760c253cSXin Li        logging.debug("Running `%s`%s", c, dir_extra)
41*760c253cSXin Li
42*760c253cSXin Li    return subprocess.run(
43*760c253cSXin Li        command,
44*760c253cSXin Li        check=check,
45*760c253cSXin Li        cwd=cwd,
46*760c253cSXin Li        env=env,
47*760c253cSXin Li        encoding="utf-8",
48*760c253cSXin Li        stdin=subprocess.DEVNULL,
49*760c253cSXin Li        stdout=stdout,
50*760c253cSXin Li        stderr=stderr,
51*760c253cSXin Li    )
52*760c253cSXin Li
53*760c253cSXin Li
54*760c253cSXin Lidef installed_llvm_has_pgo_generate_enabled() -> bool:
55*760c253cSXin Li    """Returns whether the currently-installed LLVM has USE=pgo_generate."""
56*760c253cSXin Li    equery_output = run(
57*760c253cSXin Li        ["equery", "--no-color", "--no-pipe", "u", "sys-devel/llvm"],
58*760c253cSXin Li        stdout=subprocess.PIPE,
59*760c253cSXin Li    ).stdout
60*760c253cSXin Li
61*760c253cSXin Li    # The output of `equery` is in the format:
62*760c253cSXin Li    # `${default_state_if_emerged} ${state_of_installed_pkg} llvm_pgo_generate`
63*760c253cSXin Li    #
64*760c253cSXin Li    # The relevant bit is the second.
65*760c253cSXin Li    r = re.compile(r"^ [+-] ([+-]) llvm_pgo_generate\s", re.MULTILINE)
66*760c253cSXin Li    results = r.findall(equery_output)
67*760c253cSXin Li    if not results:
68*760c253cSXin Li        raise ValueError(
69*760c253cSXin Li            "No llvm_pgo_generate line found in USE for sys-devel/llvm"
70*760c253cSXin Li        )
71*760c253cSXin Li
72*760c253cSXin Li    if len(results) > 1:
73*760c253cSXin Li        raise ValueError(
74*760c253cSXin Li            "Multiple llvm_pgo_generate line found in USE for sys-devel/llvm"
75*760c253cSXin Li        )
76*760c253cSXin Li
77*760c253cSXin Li    return results[0] == "+"
78*760c253cSXin Li
79*760c253cSXin Li
80*760c253cSXin Lidef quickpkg_llvm() -> Path:
81*760c253cSXin Li    """Runs quickpkg to generate an LLVM binpkg."""
82*760c253cSXin Li    if installed_llvm_has_pgo_generate_enabled():
83*760c253cSXin Li        # If you do want this, feel free to find this check and bypass it.
84*760c253cSXin Li        # There's nothing _inherently wrong_ with using a +pgo_generate LLVM.
85*760c253cSXin Li        # It'll just take *a lot* of extra time (2.5x+) for no reason. If you
86*760c253cSXin Li        # want to start fresh:
87*760c253cSXin Li        # ```
88*760c253cSXin Li        # sudo rm -rf /var/lib/portage/pkgs/sys-devel/llvm*tbz2 && \
89*760c253cSXin Li        #    sudo emerge -G sys-devel/llvm
90*760c253cSXin Li        # ```
91*760c253cSXin Li        raise ValueError(
92*760c253cSXin Li            "Base LLVM version has pgo_generate enabled; this is "
93*760c253cSXin Li            "almost definitely not what you want. You can "
94*760c253cSXin Li            "quickly restore to a non-pgo_generate LLVM by "
95*760c253cSXin Li            "running `sudo emerge -G sys-devel/llvm`."
96*760c253cSXin Li        )
97*760c253cSXin Li
98*760c253cSXin Li    logging.info("Building binpackage for existing sys-devel/llvm installation")
99*760c253cSXin Li    quickpkg_result = run(
100*760c253cSXin Li        ["quickpkg", "sys-devel/llvm"], stdout=subprocess.PIPE
101*760c253cSXin Li    ).stdout
102*760c253cSXin Li    # We have to scrape for the package's name, since the package generated is
103*760c253cSXin Li    # for the _installed_ version of LLVM, which might not match the current
104*760c253cSXin Li    # ebuild's version.
105*760c253cSXin Li    matches = re.findall(
106*760c253cSXin Li        r"Building package for sys-devel/(llvm-[^ ]+) ", quickpkg_result
107*760c253cSXin Li    )
108*760c253cSXin Li    if len(matches) != 1:
109*760c253cSXin Li        raise ValueError(
110*760c253cSXin Li            f"Couldn't determine LLVM version from {quickpkg_result!r};"
111*760c253cSXin Li            f"candidates: {matches}"
112*760c253cSXin Li        )
113*760c253cSXin Li
114*760c253cSXin Li    llvm_ver = matches[0]
115*760c253cSXin Li    pkg = Path("/var/lib/portage/pkgs/sys-devel", llvm_ver + ".tbz2")
116*760c253cSXin Li    assert pkg.exists(), f"expected binpkg at {pkg} not found"
117*760c253cSXin Li    return pkg
118*760c253cSXin Li
119*760c253cSXin Li
120*760c253cSXin Lidef generate_quickpkg_restoration_command(quickpkg_path: Path) -> Command:
121*760c253cSXin Li    """Returns a command you can run to restore the quickpkg'ed package."""
122*760c253cSXin Li    package_ver = quickpkg_path.stem
123*760c253cSXin Li    category = quickpkg_path.parent.name
124*760c253cSXin Li    return ["sudo", "emerge", "--usepkgonly", f"={category}/{package_ver}"]
125*760c253cSXin Li
126*760c253cSXin Li
127*760c253cSXin Lidef is_in_chroot() -> bool:
128*760c253cSXin Li    """Returns whether this script was invoked inside of the chroot."""
129*760c253cSXin Li    return Path("/etc/cros_chroot_version").exists()
130*760c253cSXin Li
131*760c253cSXin Li
132*760c253cSXin Lidef exit_if_not_in_chroot():
133*760c253cSXin Li    """Calls sys.exit if this script was not run inside of the chroot."""
134*760c253cSXin Li    if not is_in_chroot():
135*760c253cSXin Li        sys.exit("Run me inside of the chroot.")
136*760c253cSXin Li
137*760c253cSXin Li
138*760c253cSXin Lidef exit_if_in_chroot():
139*760c253cSXin Li    """Calls sys.exit if this script was run inside of the chroot."""
140*760c253cSXin Li    if is_in_chroot():
141*760c253cSXin Li        sys.exit("Run me outside of the chroot.")
142*760c253cSXin Li
143*760c253cSXin Li
144*760c253cSXin Li@contextlib.contextmanager
145*760c253cSXin Lidef temporary_file(prefix: Optional[str] = None) -> Generator[Path, None, None]:
146*760c253cSXin Li    """Yields a temporary file name, and cleans it up on exit.
147*760c253cSXin Li
148*760c253cSXin Li    This differs from NamedTemporaryFile in that it doesn't keep a handle to
149*760c253cSXin Li    the file open. This is useful if the temporary file is intended to be
150*760c253cSXin Li    passed to another process (e.g., through a flag), since that other process
151*760c253cSXin Li    might/might not overwite the file, leading to subtle bugs about reusing the
152*760c253cSXin Li    File-like aspects of NamedTemporaryFile.
153*760c253cSXin Li    """
154*760c253cSXin Li    fd, tmp = tempfile.mkstemp(prefix=prefix)
155*760c253cSXin Li    tmp_path = Path(tmp)
156*760c253cSXin Li    try:
157*760c253cSXin Li        os.close(fd)
158*760c253cSXin Li        yield tmp_path
159*760c253cSXin Li    finally:
160*760c253cSXin Li        tmp_path.unlink(missing_ok=True)
161