blob: dbc468268a1ea39ee7d6391cd6c0ccbfe0bc18b5 [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 Farsib24c1c32024-08-01 14:47:10 -070023import re
Luca Farsi5717d6f2023-12-28 15:09:28 -080024import subprocess
25import sys
Luca Farsi040fabe2024-05-22 17:21:47 -070026from typing import Callable
27import optimized_targets
28
29
30REQUIRED_ENV_VARS = frozenset(['TARGET_PRODUCT', 'TARGET_RELEASE', 'TOP'])
31SOONG_UI_EXE_REL_PATH = 'build/soong/soong_ui.bash'
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
Luca Farsib24c1c32024-08-01 14:47:10 -070055 _DOWNLOAD_OPTS = {
56 'test-config-only-zip',
57 'test-zip-file-filter',
58 'extra-host-shared-lib-zip',
59 'sandbox-tests-zips',
60 'additional-files-filter',
61 'cts-package-name',
62 }
63
Luca Farsi040fabe2024-05-22 17:21:47 -070064 def __init__(
65 self,
66 build_context: dict[str, any],
67 args: argparse.Namespace,
68 target_optimizations: dict[str, optimized_targets.OptimizedBuildTarget],
69 ):
70 self.build_context = build_context
71 self.args = args
72 self.target_optimizations = target_optimizations
73
74 def create_build_plan(self):
75
Luca Farsib24c1c32024-08-01 14:47:10 -070076 if 'optimized_build' not in self.build_context.get(
77 'enabledBuildFeatures', []
78 ):
Luca Farsi040fabe2024-05-22 17:21:47 -070079 return BuildPlan(set(self.args.extra_targets), set())
80
81 build_targets = set()
82 packaging_functions = set()
83 for target in self.args.extra_targets:
Luca Farsib24c1c32024-08-01 14:47:10 -070084 if self._unused_target_exclusion_enabled(
85 target
86 ) and not self._build_target_used(target):
87 continue
88
Luca Farsi040fabe2024-05-22 17:21:47 -070089 target_optimizer_getter = self.target_optimizations.get(target, None)
90 if not target_optimizer_getter:
91 build_targets.add(target)
92 continue
93
94 target_optimizer = target_optimizer_getter(
95 target, self.build_context, self.args
96 )
97 build_targets.update(target_optimizer.get_build_targets())
98 packaging_functions.add(target_optimizer.package_outputs)
99
100 return BuildPlan(build_targets, packaging_functions)
Luca Farsidb136442024-03-26 10:55:21 -0700101
Luca Farsib24c1c32024-08-01 14:47:10 -0700102 def _unused_target_exclusion_enabled(self, target: str) -> bool:
103 return f'{target}_unused_exclusion' in self.build_context.get(
104 'enabledBuildFeatures', []
105 )
106
107 def _build_target_used(self, target: str) -> bool:
108 """Determines whether this target's outputs are used by the test configurations listed in the build context."""
109 file_download_regexes = self._aggregate_file_download_regexes()
110 # For all of a targets' outputs, check if any of the regexes used by tests
111 # to download artifacts would match it. If any of them do then this target
112 # is necessary.
113 for artifact in self._get_target_potential_outputs(target):
114 for regex in file_download_regexes:
115 if re.match(regex, artifact):
116 return True
117 return False
118
119 def _get_target_potential_outputs(self, target: str) -> set[str]:
120 tests_suffix = '-tests'
121 if target.endswith('tests'):
122 tests_suffix = ''
123 # This is a list of all the potential zips output by the test suite targets.
124 # If the test downloads artifacts from any of these zips, we will be
125 # conservative and avoid skipping the tests.
126 return {
127 f'{target}.zip',
128 f'android-{target}.zip',
129 f'android-{target}-verifier.zip',
130 f'{target}{tests_suffix}_list.zip',
131 f'android-{target}{tests_suffix}_list.zip',
132 f'{target}{tests_suffix}_host-shared-libs.zip',
133 f'android-{target}{tests_suffix}_host-shared-libs.zip',
134 f'{target}{tests_suffix}_configs.zip',
135 f'android-{target}{tests_suffix}_configs.zip',
136 }
137
138 def _aggregate_file_download_regexes(self) -> set[re.Pattern]:
139 """Lists out all test config options to specify targets to download.
140
141 These come in the form of regexes.
142 """
143 all_regexes = set()
144 for test_info in self._get_test_infos():
145 for opt in test_info.get('extraOptions', []):
146 # check the known list of options for downloading files.
147 if opt.get('key') in self._DOWNLOAD_OPTS:
148 all_regexes.update(
149 re.compile(value) for value in opt.get('values', [])
150 )
151 return all_regexes
152
153 def _get_test_infos(self):
154 return self.build_context.get('testContext', dict()).get('testInfos', [])
155
Luca Farsidb136442024-03-26 10:55:21 -0700156
Luca Farsi040fabe2024-05-22 17:21:47 -0700157@dataclass(frozen=True)
158class BuildPlan:
159 build_targets: set[str]
160 packaging_functions: set[Callable[..., None]]
Luca Farsidb136442024-03-26 10:55:21 -0700161
162
163def build_test_suites(argv: list[str]) -> int:
Luca Farsi040fabe2024-05-22 17:21:47 -0700164 """Builds all test suites passed in, optimizing based on the build_context content.
Luca Farsidb136442024-03-26 10:55:21 -0700165
166 Args:
167 argv: The command line arguments passed in.
168
169 Returns:
170 The exit code of the build.
171 """
Luca Farsi5717d6f2023-12-28 15:09:28 -0800172 args = parse_args(argv)
Luca Farsidb136442024-03-26 10:55:21 -0700173 check_required_env()
Luca Farsi040fabe2024-05-22 17:21:47 -0700174 build_context = load_build_context()
175 build_planner = BuildPlanner(
176 build_context, args, optimized_targets.OPTIMIZED_BUILD_TARGETS
177 )
178 build_plan = build_planner.create_build_plan()
Luca Farsi5717d6f2023-12-28 15:09:28 -0800179
Luca Farsidb136442024-03-26 10:55:21 -0700180 try:
Luca Farsi040fabe2024-05-22 17:21:47 -0700181 execute_build_plan(build_plan)
Luca Farsidb136442024-03-26 10:55:21 -0700182 except BuildFailureError as e:
183 logging.error('Build command failed! Check build_log for details.')
184 return e.return_code
185
186 return 0
187
188
Luca Farsi040fabe2024-05-22 17:21:47 -0700189def parse_args(argv: list[str]) -> argparse.Namespace:
190 argparser = argparse.ArgumentParser()
191
192 argparser.add_argument(
193 'extra_targets', nargs='*', help='Extra test suites to build.'
194 )
195
196 return argparser.parse_args(argv)
197
198
Luca Farsidb136442024-03-26 10:55:21 -0700199def check_required_env():
200 """Check for required env vars.
201
202 Raises:
203 RuntimeError: If any required env vars are not found.
204 """
205 missing_env_vars = sorted(v for v in REQUIRED_ENV_VARS if v not in os.environ)
206
207 if not missing_env_vars:
208 return
209
210 t = ','.join(missing_env_vars)
211 raise Error(f'Missing required environment variables: {t}')
Luca Farsi5717d6f2023-12-28 15:09:28 -0800212
213
Luca Farsi040fabe2024-05-22 17:21:47 -0700214def load_build_context():
215 build_context_path = pathlib.Path(os.environ.get('BUILD_CONTEXT', ''))
216 if build_context_path.is_file():
217 try:
218 with open(build_context_path, 'r') as f:
219 return json.load(f)
220 except json.decoder.JSONDecodeError as e:
221 raise Error(f'Failed to load JSON file: {build_context_path}')
Luca Farsidb136442024-03-26 10:55:21 -0700222
Luca Farsi040fabe2024-05-22 17:21:47 -0700223 logging.info('No BUILD_CONTEXT found, skipping optimizations.')
224 return empty_build_context()
Luca Farsi11767d52024-03-07 13:33:57 -0800225
226
Luca Farsi040fabe2024-05-22 17:21:47 -0700227def empty_build_context():
Luca Farsib24c1c32024-08-01 14:47:10 -0700228 return {'enabledBuildFeatures': []}
Luca Farsidb136442024-03-26 10:55:21 -0700229
Luca Farsidb136442024-03-26 10:55:21 -0700230
Luca Farsi040fabe2024-05-22 17:21:47 -0700231def execute_build_plan(build_plan: BuildPlan):
232 build_command = []
233 build_command.append(get_top().joinpath(SOONG_UI_EXE_REL_PATH))
234 build_command.append('--make-mode')
235 build_command.extend(build_plan.build_targets)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800236
Luca Farsidb136442024-03-26 10:55:21 -0700237 try:
238 run_command(build_command)
239 except subprocess.CalledProcessError as e:
240 raise BuildFailureError(e.returncode) from e
Luca Farsi5717d6f2023-12-28 15:09:28 -0800241
Luca Farsi040fabe2024-05-22 17:21:47 -0700242 for packaging_function in build_plan.packaging_functions:
243 packaging_function()
Luca Farsi5717d6f2023-12-28 15:09:28 -0800244
Luca Farsidb136442024-03-26 10:55:21 -0700245
Luca Farsi040fabe2024-05-22 17:21:47 -0700246def get_top() -> pathlib.Path:
247 return pathlib.Path(os.environ['TOP'])
Luca Farsi5717d6f2023-12-28 15:09:28 -0800248
249
Luca Farsidb136442024-03-26 10:55:21 -0700250def run_command(args: list[str], stdout=None):
251 subprocess.run(args=args, check=True, stdout=stdout)
Luca Farsi5717d6f2023-12-28 15:09:28 -0800252
253
Luca Farsi2dc17012024-03-19 16:47:54 -0700254def main(argv):
Luca Farsidb136442024-03-26 10:55:21 -0700255 sys.exit(build_test_suites(argv))