blob: 7636f6a44bc8d46f3a73127780a0ee0021eaab47 [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
Luca Farsi6a346012025-02-11 00:57:53 +000019from collections import defaultdict
Luca Farsi040fabe2024-05-22 17:21:47 -070020import json
Luca Farsidb136442024-03-26 10:55:21 -070021import logging
Luca Farsi5717d6f2023-12-28 15:09:28 -080022import os
23import pathlib
Luca Farsi5dbad402024-11-07 12:43:13 -080024import re
Luca Farsi5717d6f2023-12-28 15:09:28 -080025import subprocess
26import sys
Luca Farsi8ea67422024-09-17 15:48:11 -070027from typing import Callable
Luca Farsib130e792024-08-22 12:04:41 -070028from build_context import BuildContext
Luca Farsi040fabe2024-05-22 17:21:47 -070029import optimized_targets
Luca Farsi7d859712024-11-06 16:09:16 -080030import metrics_agent
Luca Farsi5dbad402024-11-07 12:43:13 -080031import test_discovery_agent
Luca Farsi040fabe2024-05-22 17:21:47 -070032
33
Luca Farsi62035d92024-11-25 18:21:45 +000034REQUIRED_ENV_VARS = frozenset(['TARGET_PRODUCT', 'TARGET_RELEASE', 'TOP', 'DIST_DIR'])
Luca Farsi040fabe2024-05-22 17:21:47 -070035SOONG_UI_EXE_REL_PATH = 'build/soong/soong_ui.bash'
Luca Farsi2eaa5d02024-07-23 16:34:27 -070036LOG_PATH = 'logs/build_test_suites.log'
Julien Desprezcd6d27c2024-12-10 12:21:30 -080037# Currently, this prevents the removal of those tags when they exist. In the future we likely
38# want the script to supply 'dist directly
Julien Desprezdc2d3bd2024-12-16 10:11:56 -080039REQUIRED_BUILD_TARGETS = frozenset(['dist', 'droid', 'checkbuild'])
Luca Farsi5717d6f2023-12-28 15:09:28 -080040
41
Luca Farsidb136442024-03-26 10:55:21 -070042class Error(Exception):
43
44 def __init__(self, message):
45 super().__init__(message)
Luca Farsi5717d6f2023-12-28 15:09:28 -080046
47
Luca Farsidb136442024-03-26 10:55:21 -070048class BuildFailureError(Error):
49
50 def __init__(self, return_code):
51 super().__init__(f'Build command failed with return code: f{return_code}')
52 self.return_code = return_code
53
54
Luca Farsi040fabe2024-05-22 17:21:47 -070055class BuildPlanner:
56 """Class in charge of determining how to optimize build targets.
57
58 Given the build context and targets to build it will determine a final list of
59 targets to build along with getting a set of packaging functions to package up
60 any output zip files needed by the build.
61 """
62
63 def __init__(
64 self,
Luca Farsib130e792024-08-22 12:04:41 -070065 build_context: BuildContext,
Luca Farsi040fabe2024-05-22 17:21:47 -070066 args: argparse.Namespace,
67 target_optimizations: dict[str, optimized_targets.OptimizedBuildTarget],
68 ):
69 self.build_context = build_context
70 self.args = args
71 self.target_optimizations = target_optimizations
Luca Farsi6a346012025-02-11 00:57:53 +000072 self.target_to_test_infos = defaultdict(list)
Luca Farsi040fabe2024-05-22 17:21:47 -070073
74 def create_build_plan(self):
75
Luca Farsib130e792024-08-22 12:04:41 -070076 if 'optimized_build' not in self.build_context.enabled_build_features:
Luca Farsi040fabe2024-05-22 17:21:47 -070077 return BuildPlan(set(self.args.extra_targets), set())
78
Julien Desprez692dd322024-12-11 00:31:02 +000079 if not self.build_context.test_infos:
80 logging.warning('Build context has no test infos, skipping optimizations.')
81 for target in self.args.extra_targets:
82 get_metrics_agent().report_unoptimized_target(target, 'BUILD_CONTEXT has no test infos.')
83 return BuildPlan(set(self.args.extra_targets), set())
84
Luca Farsi040fabe2024-05-22 17:21:47 -070085 build_targets = set()
Luca Farsi8ea67422024-09-17 15:48:11 -070086 packaging_commands_getters = []
Luca Farsi26c0d8a2024-11-22 00:09:47 +000087 # In order to roll optimizations out differently between test suites and
88 # device builds, we have separate flags.
Luca Farsi18f6e102025-01-09 10:18:28 -080089 enable_discovery = (('test_suites_zip_test_discovery'
Luca Farsi26c0d8a2024-11-22 00:09:47 +000090 in self.build_context.enabled_build_features
91 and not self.args.device_build
92 ) or (
93 'device_zip_test_discovery'
94 in self.build_context.enabled_build_features
95 and self.args.device_build
Luca Farsi18f6e102025-01-09 10:18:28 -080096 )) and not self.args.test_discovery_info_mode
Julien Desprez254daef2024-12-19 10:04:43 -080097 logging.info(f'Discovery mode is enabled= {enable_discovery}')
98 preliminary_build_targets = self._collect_preliminary_build_targets(enable_discovery)
Luca Farsi5dbad402024-11-07 12:43:13 -080099
Luca Farsi26c0d8a2024-11-22 00:09:47 +0000100 for target in preliminary_build_targets:
Luca Farsi040fabe2024-05-22 17:21:47 -0700101 target_optimizer_getter = self.target_optimizations.get(target, None)
102 if not target_optimizer_getter:
103 build_targets.add(target)
104 continue
105
106 target_optimizer = target_optimizer_getter(
Luca Farsi6a346012025-02-11 00:57:53 +0000107 target, self.build_context, self.args, self.target_to_test_infos[target]
Luca Farsi040fabe2024-05-22 17:21:47 -0700108 )
109 build_targets.update(target_optimizer.get_build_targets())
Luca Farsi8ea67422024-09-17 15:48:11 -0700110 packaging_commands_getters.append(
111 target_optimizer.get_package_outputs_commands
112 )
Luca Farsi040fabe2024-05-22 17:21:47 -0700113
Luca Farsi8ea67422024-09-17 15:48:11 -0700114 return BuildPlan(build_targets, packaging_commands_getters)
Luca Farsidb136442024-03-26 10:55:21 -0700115
Julien Desprez254daef2024-12-19 10:04:43 -0800116 def _collect_preliminary_build_targets(self, enable_discovery: bool):
Luca Farsi26c0d8a2024-11-22 00:09:47 +0000117 build_targets = set()
118 try:
119 test_discovery_zip_regexes = self._get_test_discovery_zip_regexes()
120 logging.info(f'Discovered test discovery regexes: {test_discovery_zip_regexes}')
121 except test_discovery_agent.TestDiscoveryError as e:
122 optimization_rationale = e.message
123 logging.warning(f'Unable to perform test discovery: {optimization_rationale}')
124
125 for target in self.args.extra_targets:
126 get_metrics_agent().report_unoptimized_target(target, optimization_rationale)
127 return self._legacy_collect_preliminary_build_targets()
128
129 for target in self.args.extra_targets:
130 if target in REQUIRED_BUILD_TARGETS:
131 build_targets.add(target)
Julien Desprez1075a6b2024-12-14 17:44:18 -0800132 get_metrics_agent().report_unoptimized_target(target, 'Required build target.')
Luca Farsi26c0d8a2024-11-22 00:09:47 +0000133 continue
Julien Desprez13875332024-12-20 10:47:05 -0800134 # If nothing is discovered without error, that means nothing is needed.
135 if not test_discovery_zip_regexes:
136 get_metrics_agent().report_optimized_target(target)
137 continue
Luca Farsi26c0d8a2024-11-22 00:09:47 +0000138
139 regex = r'\b(%s.*)\b' % re.escape(target)
140 for opt in test_discovery_zip_regexes:
141 try:
142 if re.search(regex, opt):
143 get_metrics_agent().report_unoptimized_target(target, 'Test artifact used.')
144 build_targets.add(target)
Julien Despreze3879332024-12-17 17:03:57 -0800145 # proceed to next target evaluation
146 break
Luca Farsi26c0d8a2024-11-22 00:09:47 +0000147 get_metrics_agent().report_optimized_target(target)
148 except Exception as e:
149 # In case of exception report as unoptimized
150 build_targets.add(target)
151 get_metrics_agent().report_unoptimized_target(target, f'Error in parsing test discovery output for {target}: {repr(e)}')
152 logging.error(f'unable to parse test discovery output: {repr(e)}')
Julien Despreze3879332024-12-17 17:03:57 -0800153 break
Julien Desprez254daef2024-12-19 10:04:43 -0800154 # If discovery is not enabled, return the original list
155 if not enable_discovery:
156 return self._legacy_collect_preliminary_build_targets()
Luca Farsi26c0d8a2024-11-22 00:09:47 +0000157
158 return build_targets
159
160 def _legacy_collect_preliminary_build_targets(self):
161 build_targets = set()
162 for target in self.args.extra_targets:
163 if self._unused_target_exclusion_enabled(
164 target
165 ) and not self.build_context.build_target_used(target):
166 continue
167
168 build_targets.add(target)
169 return build_targets
170
Luca Farsib24c1c32024-08-01 14:47:10 -0700171 def _unused_target_exclusion_enabled(self, target: str) -> bool:
Luca Farsib130e792024-08-22 12:04:41 -0700172 return (
173 f'{target}_unused_exclusion'
174 in self.build_context.enabled_build_features
Luca Farsib24c1c32024-08-01 14:47:10 -0700175 )
176
Luca Farsi5dbad402024-11-07 12:43:13 -0800177 def _get_test_discovery_zip_regexes(self) -> set[str]:
178 build_target_regexes = set()
179 for test_info in self.build_context.test_infos:
180 tf_command = self._build_tf_command(test_info)
181 discovery_agent = test_discovery_agent.TestDiscoveryAgent(tradefed_args=tf_command)
182 for regex in discovery_agent.discover_test_zip_regexes():
Luca Farsi6a346012025-02-11 00:57:53 +0000183 for target in self.args.extra_targets:
184 target_regex = r'\b(%s.*)\b' % re.escape(target)
185 if re.search(target_regex, regex):
186 self.target_to_test_infos[target].append(test_info)
Luca Farsi5dbad402024-11-07 12:43:13 -0800187 build_target_regexes.add(regex)
188 return build_target_regexes
189
190
191 def _build_tf_command(self, test_info) -> list[str]:
192 command = [test_info.command]
193 for extra_option in test_info.extra_options:
194 if not extra_option.get('key'):
195 continue
196 arg_key = '--' + extra_option.get('key')
197 if arg_key == '--build-id':
198 command.append(arg_key)
199 command.append(os.environ.get('BUILD_NUMBER'))
200 continue
201 if extra_option.get('values'):
202 for value in extra_option.get('values'):
203 command.append(arg_key)
204 command.append(value)
205 else:
206 command.append(arg_key)
207
208 return command
Luca Farsidb136442024-03-26 10:55:21 -0700209
Luca Farsi040fabe2024-05-22 17:21:47 -0700210@dataclass(frozen=True)
211class BuildPlan:
212 build_targets: set[str]
Luca Farsi8ea67422024-09-17 15:48:11 -0700213 packaging_commands_getters: list[Callable[[], list[list[str]]]]
Luca Farsidb136442024-03-26 10:55:21 -0700214
215
216def build_test_suites(argv: list[str]) -> int:
Luca Farsi040fabe2024-05-22 17:21:47 -0700217 """Builds all test suites passed in, optimizing based on the build_context content.
Luca Farsidb136442024-03-26 10:55:21 -0700218
219 Args:
220 argv: The command line arguments passed in.
221
222 Returns:
223 The exit code of the build.
224 """
Luca Farsi7d859712024-11-06 16:09:16 -0800225 get_metrics_agent().analysis_start()
226 try:
227 args = parse_args(argv)
228 check_required_env()
229 build_context = BuildContext(load_build_context())
230 build_planner = BuildPlanner(
231 build_context, args, optimized_targets.OPTIMIZED_BUILD_TARGETS
232 )
233 build_plan = build_planner.create_build_plan()
234 except:
235 raise
236 finally:
237 get_metrics_agent().analysis_end()
Luca Farsi5717d6f2023-12-28 15:09:28 -0800238
Luca Farsidb136442024-03-26 10:55:21 -0700239 try:
Luca Farsi040fabe2024-05-22 17:21:47 -0700240 execute_build_plan(build_plan)
Luca Farsidb136442024-03-26 10:55:21 -0700241 except BuildFailureError as e:
242 logging.error('Build command failed! Check build_log for details.')
243 return e.return_code
Luca Farsi7d859712024-11-06 16:09:16 -0800244 finally:
245 get_metrics_agent().end_reporting()
Luca Farsidb136442024-03-26 10:55:21 -0700246
247 return 0
248
249
Luca Farsi040fabe2024-05-22 17:21:47 -0700250def parse_args(argv: list[str]) -> argparse.Namespace:
251 argparser = argparse.ArgumentParser()
252
253 argparser.add_argument(
254 'extra_targets', nargs='*', help='Extra test suites to build.'
255 )
Luca Farsi62035d92024-11-25 18:21:45 +0000256 argparser.add_argument(
257 '--device-build',
258 action='store_true',
259 help='Flag to indicate running a device build.',
260 )
Luca Farsi18f6e102025-01-09 10:18:28 -0800261 argparser.add_argument(
262 '--test_discovery_info_mode',
263 action='store_true',
264 help='Flag to enable running test discovery in info only mode.',
265 )
Luca Farsi040fabe2024-05-22 17:21:47 -0700266
267 return argparser.parse_args(argv)
268
269
Luca Farsidb136442024-03-26 10:55:21 -0700270def check_required_env():
271 """Check for required env vars.
272
273 Raises:
274 RuntimeError: If any required env vars are not found.
275 """
276 missing_env_vars = sorted(v for v in REQUIRED_ENV_VARS if v not in os.environ)
277
278 if not missing_env_vars:
279 return
280
281 t = ','.join(missing_env_vars)
282 raise Error(f'Missing required environment variables: {t}')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800283
284
Luca Farsi040fabe2024-05-22 17:21:47 -0700285def load_build_context():
286 build_context_path = pathlib.Path(os.environ.get('BUILD_CONTEXT', ''))
287 if build_context_path.is_file():
288 try:
289 with open(build_context_path, 'r') as f:
290 return json.load(f)
291 except json.decoder.JSONDecodeError as e:
292 raise Error(f'Failed to load JSON file: {build_context_path}')
Luca Farsidb136442024-03-26 10:55:21 -0700293
Luca Farsi040fabe2024-05-22 17:21:47 -0700294 logging.info('No BUILD_CONTEXT found, skipping optimizations.')
295 return empty_build_context()
Luca Farsi11767d52024-03-07 13:33:57 -0800296
297
Luca Farsi040fabe2024-05-22 17:21:47 -0700298def empty_build_context():
Luca Farsib24c1c32024-08-01 14:47:10 -0700299 return {'enabledBuildFeatures': []}
Luca Farsidb136442024-03-26 10:55:21 -0700300
Luca Farsidb136442024-03-26 10:55:21 -0700301
Luca Farsi040fabe2024-05-22 17:21:47 -0700302def execute_build_plan(build_plan: BuildPlan):
303 build_command = []
304 build_command.append(get_top().joinpath(SOONG_UI_EXE_REL_PATH))
305 build_command.append('--make-mode')
306 build_command.extend(build_plan.build_targets)
Julien Desprezaa4ee4b2025-01-10 20:35:11 -0800307 logging.info(f'Running build command: {build_command}')
Luca Farsidb136442024-03-26 10:55:21 -0700308 try:
309 run_command(build_command)
310 except subprocess.CalledProcessError as e:
311 raise BuildFailureError(e.returncode) from e
Luca Farsi5717d6f2023-12-28 15:09:28 -0800312
Luca Farsi7d859712024-11-06 16:09:16 -0800313 get_metrics_agent().packaging_start()
314 try:
315 for packaging_commands_getter in build_plan.packaging_commands_getters:
Luca Farsi8ea67422024-09-17 15:48:11 -0700316 for packaging_command in packaging_commands_getter():
317 run_command(packaging_command)
Luca Farsi7d859712024-11-06 16:09:16 -0800318 except subprocess.CalledProcessError as e:
319 raise BuildFailureError(e.returncode) from e
320 finally:
321 get_metrics_agent().packaging_end()
Luca Farsi5717d6f2023-12-28 15:09:28 -0800322
Luca Farsidb136442024-03-26 10:55:21 -0700323
Luca Farsi040fabe2024-05-22 17:21:47 -0700324def get_top() -> pathlib.Path:
325 return pathlib.Path(os.environ['TOP'])
Luca Farsi5717d6f2023-12-28 15:09:28 -0800326
327
Luca Farsidb136442024-03-26 10:55:21 -0700328def run_command(args: list[str], stdout=None):
329 subprocess.run(args=args, check=True, stdout=stdout)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800330
331
Luca Farsi7d859712024-11-06 16:09:16 -0800332def get_metrics_agent():
333 return metrics_agent.MetricsAgent.instance()
334
335
Luca Farsi2dc17012024-03-19 16:47:54 -0700336def main(argv):
Luca Farsi2eaa5d02024-07-23 16:34:27 -0700337 dist_dir = os.environ.get('DIST_DIR')
338 if dist_dir:
339 log_file = pathlib.Path(dist_dir) / LOG_PATH
340 logging.basicConfig(
341 level=logging.DEBUG,
342 format='%(asctime)s %(levelname)s %(message)s',
343 filename=log_file,
344 )
Luca Farsidb136442024-03-26 10:55:21 -0700345 sys.exit(build_test_suites(argv))
Luca Farsi10adf352024-11-06 14:23:36 -0800346
347
348if __name__ == '__main__':
349 main(sys.argv[1:])