blob: e88b420d3f3fa3c58dd916764dc48dcb00bb6a4b [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
15"""Script to build only the necessary modules for general-tests along
16
17with whatever other targets are passed in.
18"""
19
20import argparse
21from collections.abc import Sequence
22import json
23import os
24import pathlib
25import re
26import subprocess
27import sys
28from typing import Any, Dict, Set, Text
29
30import test_mapping_module_retriever
31
32
33# List of modules that are always required to be in general-tests.zip
34REQUIRED_MODULES = frozenset(
35 ['cts-tradefed', 'vts-tradefed', 'compatibility-host-util', 'soong_zip']
36)
37
38
39def build_test_suites(argv):
40 args = parse_args(argv)
41
42 if not args.change_info:
43 build_everything(args)
44 return
45
46 # Call the class to map changed files to modules to build.
47 # TODO(lucafarsi): Move this into a replaceable class.
48 build_affected_modules(args)
49
50
51def parse_args(argv):
52 argparser = argparse.ArgumentParser()
53 argparser.add_argument(
54 'extra_targets', nargs='*', help='Extra test suites to build.'
55 )
56 argparser.add_argument('--target_product')
57 argparser.add_argument('--target_release')
58 argparser.add_argument(
59 '--with_dexpreopt_boot_img_and_system_server_only', action='store_true'
60 )
61 argparser.add_argument('--dist_dir')
62 argparser.add_argument('--change_info', nargs='?')
63 argparser.add_argument('--extra_required_modules', nargs='*')
64
65 return argparser.parse_args()
66
67
68def build_everything(args: argparse.Namespace):
69 build_command = base_build_command(args)
70 build_command.append('general-tests')
71
72 run_command(build_command)
73
74
75def build_affected_modules(args: argparse.Namespace):
76 modules_to_build = find_modules_to_build(
77 pathlib.Path(args.change_info), args.extra_required_modules
78 )
79
80 # Call the build command with everything.
81 build_command = base_build_command(args)
82 build_command.extend(modules_to_build)
83
84 run_command(build_command)
85
86 zip_build_outputs(modules_to_build, args.dist_dir)
87
88
89def base_build_command(args: argparse.Namespace) -> list:
90 build_command = []
91 build_command.append('time')
92 build_command.append('./build/soong/soong_ui.bash')
93 build_command.append('--make-mode')
94 build_command.append('dist')
95 build_command.append('DIST_DIR=' + args.dist_dir)
96 build_command.append('TARGET_PRODUCT=' + args.target_product)
97 build_command.append('TARGET_RELEASE=' + args.target_release)
98 build_command.extend(args.extra_targets)
99
100 return build_command
101
102
103def run_command(args: list[str]) -> str:
104 result = subprocess.run(
105 args=args,
106 text=True,
107 capture_output=True,
108 check=False,
109 )
110 # If the process failed, print its stdout and propagate the exception.
111 if not result.returncode == 0:
112 print('Build command failed! output:')
113 print('stdout: ' + result.stdout)
114 print('stderr: ' + result.stderr)
115
116 result.check_returncode()
117 return result.stdout
118
119
120def find_modules_to_build(
121 change_info: pathlib.Path, extra_required_modules: list[Text]
122) -> Set[Text]:
123 changed_files = find_changed_files(change_info)
124
125 test_mappings = test_mapping_module_retriever.GetTestMappings(
126 changed_files, set()
127 )
128
129 # Soong_zip is required to generate the output zip so always build it.
130 modules_to_build = set(REQUIRED_MODULES)
131 if extra_required_modules:
132 modules_to_build.update(extra_required_modules)
133
134 modules_to_build.update(find_affected_modules(test_mappings, changed_files))
135
136 return modules_to_build
137
138
139def find_changed_files(change_info: pathlib.Path) -> Set[Text]:
140 with open(change_info) as change_info_file:
141 change_info_contents = json.load(change_info_file)
142
143 changed_files = set()
144
145 for change in change_info_contents['changes']:
146 project_path = change.get('projectPath') + '/'
147
148 for revision in change.get('revisions'):
149 for file_info in revision.get('fileInfos'):
150 changed_files.add(project_path + file_info.get('path'))
151
152 return changed_files
153
154
155def find_affected_modules(
156 test_mappings: Dict[str, Any], changed_files: Set[Text]
157) -> Set[Text]:
158 modules = set()
159
160 # The test_mappings object returned by GetTestMappings is organized as
161 # follows:
162 # {
163 # 'test_mapping_file_path': {
164 # 'group_name' : [
165 # 'name': 'module_name',
166 # ],
167 # }
168 # }
169 for test_mapping in test_mappings.values():
170 for group in test_mapping.values():
171 for entry in group:
172 module_name = entry.get('name', None)
173
174 if not module_name:
175 continue
176
177 file_patterns = entry.get('file_patterns')
178 if not file_patterns:
179 modules.add(module_name)
180 continue
181
182 if matches_file_patterns(file_patterns, changed_files):
183 modules.add(module_name)
184 continue
185
186 return modules
187
188
189# TODO(lucafarsi): Share this logic with the original logic in
190# test_mapping_test_retriever.py
191def matches_file_patterns(
192 file_patterns: list[Text], changed_files: Set[Text]
193) -> bool:
194 for changed_file in changed_files:
195 for pattern in file_patterns:
196 if re.search(pattern, changed_file):
197 return True
198
199 return False
200
201
202def zip_build_outputs(modules_to_build: Set[Text], dist_dir: Text):
203 src_top = os.environ.get('TOP', os.getcwd())
204
205 # Call dumpvars to get the necessary things.
206 # TODO(lucafarsi): Don't call soong_ui 4 times for this, --dumpvars-mode can
207 # do it but it requires parsing.
208 host_out_testcases = get_soong_var('HOST_OUT_TESTCASES')
209 target_out_testcases = get_soong_var('TARGET_OUT_TESTCASES')
210 product_out = get_soong_var('PRODUCT_OUT')
211 soong_host_out = get_soong_var('SOONG_HOST_OUT')
212 host_out = get_soong_var('HOST_OUT')
213
214 # Call the class to package the outputs.
215 # TODO(lucafarsi): Move this code into a replaceable class.
216 host_paths = []
217 target_paths = []
218 for module in modules_to_build:
219 host_path = os.path.join(host_out_testcases, module)
220 if os.path.exists(host_path):
221 host_paths.append(host_path)
222
223 target_path = os.path.join(target_out_testcases, module)
224 if os.path.exists(target_path):
225 target_paths.append(target_path)
226
227 zip_command = ['time', os.path.join(host_out, 'bin', 'soong_zip')]
228
229 # Add host testcases.
230 zip_command.append('-C')
231 zip_command.append(os.path.join(src_top, soong_host_out))
232 zip_command.append('-P')
233 zip_command.append('host/')
234 for path in host_paths:
235 zip_command.append('-D')
236 zip_command.append(path)
237
238 # Add target testcases.
239 zip_command.append('-C')
240 zip_command.append(os.path.join(src_top, product_out))
241 zip_command.append('-P')
242 zip_command.append('target')
243 for path in target_paths:
244 zip_command.append('-D')
245 zip_command.append(path)
246
247 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
248 # Add necessary tools. These are also hardcoded in general-tests.mk.
249 framework_path = os.path.join(soong_host_out, 'framework')
250
251 zip_command.append('-C')
252 zip_command.append(framework_path)
253 zip_command.append('-P')
254 zip_command.append('host/tools')
255 zip_command.append('-f')
256 zip_command.append(os.path.join(framework_path, 'cts-tradefed.jar'))
257 zip_command.append('-f')
258 zip_command.append(
259 os.path.join(framework_path, 'compatibility-host-util.jar')
260 )
261 zip_command.append('-f')
262 zip_command.append(os.path.join(framework_path, 'vts-tradefed.jar'))
263
264 # Zip to the DIST dir.
265 zip_command.append('-o')
266 zip_command.append(os.path.join(dist_dir, 'general-tests.zip'))
267
268 run_command(zip_command)
269
270
271def get_soong_var(var: str) -> str:
272 value = run_command(
273 ['./build/soong/soong_ui.bash', '--dumpvar-mode', '--abs', var]
274 ).strip()
275 if not value:
276 raise RuntimeError('Necessary soong variable ' + var + ' not found.')
277
278 return value
279
280
281def main(argv):
282 build_test_suites(sys.argv)