1#
2# Copyright (C) 2021 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16"""APIs for interacting with Soong."""
17import logging
18import os
19from pathlib import Path
20import shlex
21import shutil
22import subprocess
23
24
25def logger() -> logging.Logger:
26    """Returns the module level logger."""
27    return logging.getLogger(__name__)
28
29
30class Soong:
31    """Interface for interacting with Soong."""
32
33    def __init__(self, build_top: Path, out_dir: Path) -> None:
34        self.out_dir = out_dir
35        self.soong_ui_path = build_top / "build/soong/soong_ui.bash"
36
37    def soong_ui(
38        self,
39        args: list[str],
40        env: dict[str, str] | None = None,
41        capture_output: bool = False,
42    ) -> str:
43        """Executes soong_ui.bash and returns the output on success.
44
45        Args:
46            args: List of string arguments to pass to soong_ui.bash.
47            env: Additional environment variables to set when running soong_ui.
48            capture_output: True if the output of the command should be captured and
49                returned. If not, the output will be printed to stdout/stderr and an
50                empty string will be returned.
51
52        Raises:
53            subprocess.CalledProcessError: The subprocess failure if the soong command
54            failed.
55
56        Returns:
57            The interleaved contents of stdout/stderr if capture_output is True, else an
58            empty string.
59        """
60        if env is None:
61            env = {}
62
63        # Use a (mostly) clean environment to avoid the caller's lunch
64        # environment affecting the build.
65        exec_env = {
66            # Newer versions of golang require the go cache, which defaults to somewhere
67            # in HOME if not set.
68            "HOME": os.environ["HOME"],
69            "OUT_DIR": str(self.out_dir.resolve()),
70            "PATH": os.environ["PATH"],
71        }
72        exec_env.update(env)
73        env_prefix = " ".join(f"{k}={v}" for k, v in exec_env.items())
74        cmd = [str(self.soong_ui_path)] + args
75        logger().debug(f"running in {os.getcwd()}: {env_prefix} {shlex.join(cmd)}")
76        result = subprocess.run(
77            cmd,
78            check=True,
79            capture_output=capture_output,
80            encoding="utf-8",
81            env=exec_env,
82        )
83        return result.stdout
84
85    def get_make_var(self, name: str) -> str:
86        """Queries the build system for the value of a make variable.
87
88        Args:
89            name: The name of the build variable to query.
90
91        Returns:
92            The value of the build variable in string form.
93        """
94        return self.soong_ui(["--dumpvar-mode", name], capture_output=True).rstrip("\n")
95
96    def clean(self) -> None:
97        """Removes the output directory, if it exists."""
98        if self.out_dir.exists():
99            shutil.rmtree(self.out_dir)
100
101    def build(self, targets: list[str], env: dict[str, str] | None = None) -> None:
102        """Builds the given targets.
103
104        The out directory will be created if it does not already exist. Existing
105        contents will not be removed, but affected outputs will be modified.
106
107        Args:
108            targets: A list of target names to build.
109            env: Additional environment variables to set when running the build.
110        """
111        self.out_dir.mkdir(parents=True, exist_ok=True)
112        self.soong_ui(["--make-mode", "--soong-only"] + targets, env=env)
113