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