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