blob: 933e43e387d3208d3d16da79463d1792755c9708 [file] [log] [blame]
Luca Farsi5717d6f2023-12-28 15:09:28 -08001# Copyright 2024, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Luca Farsidb136442024-03-26 10:55:21 -070015"""Build script for the CI `test_suites` target."""
Luca Farsi5717d6f2023-12-28 15:09:28 -080016
17import argparse
Luca Farsi040fabe2024-05-22 17:21:47 -070018from dataclasses import dataclass
19import json
Luca Farsidb136442024-03-26 10:55:21 -070020import logging
Luca Farsi5717d6f2023-12-28 15:09:28 -080021import os
22import pathlib
Luca Farsi5717d6f2023-12-28 15:09:28 -080023import subprocess
24import sys
Luca Farsib130e792024-08-22 12:04:41 -070025from build_context import BuildContext
Luca Farsi040fabe2024-05-22 17:21:47 -070026import optimized_targets
27
28
29REQUIRED_ENV_VARS = frozenset(['TARGET_PRODUCT', 'TARGET_RELEASE', 'TOP'])
30SOONG_UI_EXE_REL_PATH = 'build/soong/soong_ui.bash'
Luca Farsi2eaa5d02024-07-23 16:34:27 -070031LOG_PATH = 'logs/build_test_suites.log'
Luca Farsi5717d6f2023-12-28 15:09:28 -080032
33
Luca Farsidb136442024-03-26 10:55:21 -070034class Error(Exception):
35
36 def __init__(self, message):
37 super().__init__(message)
Luca Farsi5717d6f2023-12-28 15:09:28 -080038
39
Luca Farsidb136442024-03-26 10:55:21 -070040class BuildFailureError(Error):
41
42 def __init__(self, return_code):
43 super().__init__(f'Build command failed with return code: f{return_code}')
44 self.return_code = return_code
45
46
Luca Farsi040fabe2024-05-22 17:21:47 -070047class BuildPlanner:
48 """Class in charge of determining how to optimize build targets.
49
50 Given the build context and targets to build it will determine a final list of
51 targets to build along with getting a set of packaging functions to package up
52 any output zip files needed by the build.
53 """
54
55 def __init__(
56 self,
Luca Farsib130e792024-08-22 12:04:41 -070057 build_context: BuildContext,
Luca Farsi040fabe2024-05-22 17:21:47 -070058 args: argparse.Namespace,
59 target_optimizations: dict[str, optimized_targets.OptimizedBuildTarget],
60 ):
61 self.build_context = build_context
62 self.args = args
63 self.target_optimizations = target_optimizations
64
65 def create_build_plan(self):
66
Luca Farsib130e792024-08-22 12:04:41 -070067 if 'optimized_build' not in self.build_context.enabled_build_features:
Luca Farsi040fabe2024-05-22 17:21:47 -070068 return BuildPlan(set(self.args.extra_targets), set())
69
70 build_targets = set()
Luca Farsid4e4b642024-09-10 16:37:51 -070071 packaging_commands = []
Luca Farsi040fabe2024-05-22 17:21:47 -070072 for target in self.args.extra_targets:
Luca Farsib24c1c32024-08-01 14:47:10 -070073 if self._unused_target_exclusion_enabled(
74 target
Luca Farsib130e792024-08-22 12:04:41 -070075 ) and not self.build_context.build_target_used(target):
Luca Farsib24c1c32024-08-01 14:47:10 -070076 continue
77
Luca Farsi040fabe2024-05-22 17:21:47 -070078 target_optimizer_getter = self.target_optimizations.get(target, None)
79 if not target_optimizer_getter:
80 build_targets.add(target)
81 continue
82
83 target_optimizer = target_optimizer_getter(
84 target, self.build_context, self.args
85 )
86 build_targets.update(target_optimizer.get_build_targets())
Luca Farsid4e4b642024-09-10 16:37:51 -070087 packaging_commands.extend(target_optimizer.get_package_outputs_commands())
Luca Farsi040fabe2024-05-22 17:21:47 -070088
Luca Farsid4e4b642024-09-10 16:37:51 -070089 return BuildPlan(build_targets, packaging_commands)
Luca Farsidb136442024-03-26 10:55:21 -070090
Luca Farsib24c1c32024-08-01 14:47:10 -070091 def _unused_target_exclusion_enabled(self, target: str) -> bool:
Luca Farsib130e792024-08-22 12:04:41 -070092 return (
93 f'{target}_unused_exclusion'
94 in self.build_context.enabled_build_features
Luca Farsib24c1c32024-08-01 14:47:10 -070095 )
96
Luca Farsidb136442024-03-26 10:55:21 -070097
Luca Farsi040fabe2024-05-22 17:21:47 -070098@dataclass(frozen=True)
99class BuildPlan:
100 build_targets: set[str]
Luca Farsid4e4b642024-09-10 16:37:51 -0700101 packaging_commands: list[list[str]]
Luca Farsidb136442024-03-26 10:55:21 -0700102
103
104def build_test_suites(argv: list[str]) -> int:
Luca Farsi040fabe2024-05-22 17:21:47 -0700105 """Builds all test suites passed in, optimizing based on the build_context content.
Luca Farsidb136442024-03-26 10:55:21 -0700106
107 Args:
108 argv: The command line arguments passed in.
109
110 Returns:
111 The exit code of the build.
112 """
Luca Farsi5717d6f2023-12-28 15:09:28 -0800113 args = parse_args(argv)
Luca Farsidb136442024-03-26 10:55:21 -0700114 check_required_env()
Luca Farsib130e792024-08-22 12:04:41 -0700115 build_context = BuildContext(load_build_context())
Luca Farsi040fabe2024-05-22 17:21:47 -0700116 build_planner = BuildPlanner(
117 build_context, args, optimized_targets.OPTIMIZED_BUILD_TARGETS
118 )
119 build_plan = build_planner.create_build_plan()
Luca Farsi5717d6f2023-12-28 15:09:28 -0800120
Luca Farsidb136442024-03-26 10:55:21 -0700121 try:
Luca Farsi040fabe2024-05-22 17:21:47 -0700122 execute_build_plan(build_plan)
Luca Farsidb136442024-03-26 10:55:21 -0700123 except BuildFailureError as e:
124 logging.error('Build command failed! Check build_log for details.')
125 return e.return_code
126
127 return 0
128
129
Luca Farsi040fabe2024-05-22 17:21:47 -0700130def parse_args(argv: list[str]) -> argparse.Namespace:
131 argparser = argparse.ArgumentParser()
132
133 argparser.add_argument(
134 'extra_targets', nargs='*', help='Extra test suites to build.'
135 )
136
137 return argparser.parse_args(argv)
138
139
Luca Farsidb136442024-03-26 10:55:21 -0700140def check_required_env():
141 """Check for required env vars.
142
143 Raises:
144 RuntimeError: If any required env vars are not found.
145 """
146 missing_env_vars = sorted(v for v in REQUIRED_ENV_VARS if v not in os.environ)
147
148 if not missing_env_vars:
149 return
150
151 t = ','.join(missing_env_vars)
152 raise Error(f'Missing required environment variables: {t}')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800153
154
Luca Farsi040fabe2024-05-22 17:21:47 -0700155def load_build_context():
156 build_context_path = pathlib.Path(os.environ.get('BUILD_CONTEXT', ''))
157 if build_context_path.is_file():
158 try:
159 with open(build_context_path, 'r') as f:
160 return json.load(f)
161 except json.decoder.JSONDecodeError as e:
162 raise Error(f'Failed to load JSON file: {build_context_path}')
Luca Farsidb136442024-03-26 10:55:21 -0700163
Luca Farsi040fabe2024-05-22 17:21:47 -0700164 logging.info('No BUILD_CONTEXT found, skipping optimizations.')
165 return empty_build_context()
Luca Farsi11767d52024-03-07 13:33:57 -0800166
167
Luca Farsi040fabe2024-05-22 17:21:47 -0700168def empty_build_context():
Luca Farsib24c1c32024-08-01 14:47:10 -0700169 return {'enabledBuildFeatures': []}
Luca Farsidb136442024-03-26 10:55:21 -0700170
Luca Farsidb136442024-03-26 10:55:21 -0700171
Luca Farsi040fabe2024-05-22 17:21:47 -0700172def execute_build_plan(build_plan: BuildPlan):
173 build_command = []
174 build_command.append(get_top().joinpath(SOONG_UI_EXE_REL_PATH))
175 build_command.append('--make-mode')
176 build_command.extend(build_plan.build_targets)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800177
Luca Farsidb136442024-03-26 10:55:21 -0700178 try:
179 run_command(build_command)
180 except subprocess.CalledProcessError as e:
181 raise BuildFailureError(e.returncode) from e
Luca Farsi5717d6f2023-12-28 15:09:28 -0800182
Luca Farsid4e4b642024-09-10 16:37:51 -0700183 for packaging_command in build_plan.packaging_commands:
184 try:
185 run_command(packaging_command)
186 except subprocess.CalledProcessError as e:
187 raise BuildFailureError(e.returncode) from e
Luca Farsi5717d6f2023-12-28 15:09:28 -0800188
Luca Farsidb136442024-03-26 10:55:21 -0700189
Luca Farsi040fabe2024-05-22 17:21:47 -0700190def get_top() -> pathlib.Path:
191 return pathlib.Path(os.environ['TOP'])
Luca Farsi5717d6f2023-12-28 15:09:28 -0800192
193
Luca Farsidb136442024-03-26 10:55:21 -0700194def run_command(args: list[str], stdout=None):
195 subprocess.run(args=args, check=True, stdout=stdout)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800196
197
Luca Farsi2dc17012024-03-19 16:47:54 -0700198def main(argv):
Luca Farsi2eaa5d02024-07-23 16:34:27 -0700199 dist_dir = os.environ.get('DIST_DIR')
200 if dist_dir:
201 log_file = pathlib.Path(dist_dir) / LOG_PATH
202 logging.basicConfig(
203 level=logging.DEBUG,
204 format='%(asctime)s %(levelname)s %(message)s',
205 filename=log_file,
206 )
Luca Farsidb136442024-03-26 10:55:21 -0700207 sys.exit(build_test_suites(argv))