• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2025 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
15package ci_tests
16
17import (
18	"fmt"
19	"path/filepath"
20	"strings"
21
22	"android/soong/android"
23
24	"github.com/google/blueprint"
25	"github.com/google/blueprint/proptools"
26)
27
28func init() {
29	pctx.Import("android/soong/android")
30	registerTestPackageZipBuildComponents(android.InitRegistrationContext)
31}
32
33func registerTestPackageZipBuildComponents(ctx android.RegistrationContext) {
34	ctx.RegisterModuleType("test_package", TestPackageZipFactory)
35}
36
37type testPackageZip struct {
38	android.ModuleBase
39	android.DefaultableModuleBase
40
41	properties CITestPackageProperties
42
43	output android.Path
44}
45
46type CITestPackageProperties struct {
47	// test modules will be added as dependencies using the device os and the common architecture's variant.
48	Tests proptools.Configurable[[]string] `android:"arch_variant"`
49	// test modules that will be added as dependencies based on the first supported arch variant and the device os variant
50	Device_first_tests proptools.Configurable[[]string] `android:"arch_variant"`
51	// test modules that will be added as dependencies based on both 32bit and 64bit arch variant and the device os variant
52	Device_both_tests proptools.Configurable[[]string] `android:"arch_variant"`
53	// test modules that will be added as dependencies based on host
54	Host_tests proptools.Configurable[[]string] `android:"arch_variant"`
55	// git-main only test modules. Will only be added as dependencies using the device os and the common architecture's variant if exists.
56	Tests_if_exist_common proptools.Configurable[[]string] `android:"arch_variant"`
57	// git-main only test modules. Will only be added as dependencies based on both 32bit and 64bit arch variant and the device os variant if exists.
58	Tests_if_exist_device_both proptools.Configurable[[]string] `android:"arch_variant"`
59}
60
61type testPackageZipDepTagType struct {
62	blueprint.BaseDependencyTag
63}
64
65var testPackageZipDepTag testPackageZipDepTagType
66
67var (
68	pctx = android.NewPackageContext("android/soong/ci_tests")
69	// test_package module type should only be used for the following modules.
70	// TODO: remove "_soong" from the module names inside when eliminating the corresponding make modules
71	moduleNamesAllowed = []string{"continuous_instrumentation_tests_soong", "continuous_instrumentation_metric_tests_soong", "continuous_native_tests_soong", "continuous_native_metric_tests_soong", "platform_tests"}
72)
73
74func (p *testPackageZip) DepsMutator(ctx android.BottomUpMutatorContext) {
75	// adding tests property deps
76	for _, t := range p.properties.Tests.GetOrDefault(ctx, nil) {
77		ctx.AddVariationDependencies(ctx.Config().AndroidCommonTarget.Variations(), testPackageZipDepTag, t)
78	}
79
80	// adding device_first_tests property deps
81	for _, t := range p.properties.Device_first_tests.GetOrDefault(ctx, nil) {
82		ctx.AddVariationDependencies(ctx.Config().AndroidFirstDeviceTarget.Variations(), testPackageZipDepTag, t)
83	}
84
85	// adding device_both_tests property deps
86	p.addDeviceBothDeps(ctx, false)
87
88	// adding host_tests property deps
89	for _, t := range p.properties.Host_tests.GetOrDefault(ctx, nil) {
90		ctx.AddVariationDependencies(ctx.Config().BuildOSTarget.Variations(), testPackageZipDepTag, t)
91	}
92
93	// adding Tests_if_exist_* property deps
94	for _, t := range p.properties.Tests_if_exist_common.GetOrDefault(ctx, nil) {
95		if ctx.OtherModuleExists(t) {
96			ctx.AddVariationDependencies(ctx.Config().AndroidCommonTarget.Variations(), testPackageZipDepTag, t)
97		}
98	}
99	p.addDeviceBothDeps(ctx, true)
100}
101
102func (p *testPackageZip) addDeviceBothDeps(ctx android.BottomUpMutatorContext, checkIfExist bool) {
103	android32TargetList := android.FirstTarget(ctx.Config().Targets[android.Android], "lib32")
104	android64TargetList := android.FirstTarget(ctx.Config().Targets[android.Android], "lib64")
105	if len(android32TargetList) > 0 {
106		maybeAndroid32Target := &android32TargetList[0]
107		if checkIfExist {
108			for _, t := range p.properties.Tests_if_exist_device_both.GetOrDefault(ctx, nil) {
109				if ctx.OtherModuleExists(t) {
110					ctx.AddFarVariationDependencies(maybeAndroid32Target.Variations(), testPackageZipDepTag, t)
111				}
112			}
113		} else {
114			ctx.AddFarVariationDependencies(maybeAndroid32Target.Variations(), testPackageZipDepTag, p.properties.Device_both_tests.GetOrDefault(ctx, nil)...)
115		}
116	}
117	if len(android64TargetList) > 0 {
118		maybeAndroid64Target := &android64TargetList[0]
119		if checkIfExist {
120			for _, t := range p.properties.Tests_if_exist_device_both.GetOrDefault(ctx, nil) {
121				if ctx.OtherModuleExists(t) {
122					ctx.AddFarVariationDependencies(maybeAndroid64Target.Variations(), testPackageZipDepTag, t)
123				}
124			}
125		} else {
126			ctx.AddFarVariationDependencies(maybeAndroid64Target.Variations(), testPackageZipDepTag, p.properties.Device_both_tests.GetOrDefault(ctx, nil)...)
127		}
128	}
129}
130
131func TestPackageZipFactory() android.Module {
132	module := &testPackageZip{}
133
134	module.AddProperties(&module.properties)
135
136	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibCommon)
137	android.InitDefaultableModule(module)
138
139	return module
140}
141
142func (p *testPackageZip) GenerateAndroidBuildActions(ctx android.ModuleContext) {
143	// Never install this test package, it's for disting only
144	p.SkipInstall()
145
146	if !android.InList(ctx.ModuleName(), moduleNamesAllowed) {
147		ctx.ModuleErrorf("%s is not allowed to use module type test_package")
148	}
149
150	p.output = createOutput(ctx, pctx)
151
152	ctx.SetOutputFiles(android.Paths{p.output}, "")
153}
154
155func createOutput(ctx android.ModuleContext, pctx android.PackageContext) android.ModuleOutPath {
156	productOut := filepath.Join(ctx.Config().OutDir(), "target", "product", ctx.Config().DeviceName())
157	stagingDir := android.PathForModuleOut(ctx, "STAGING")
158	productVariables := ctx.Config().ProductVariables()
159	arch := proptools.String(productVariables.DeviceArch)
160	secondArch := proptools.String(productVariables.DeviceSecondaryArch)
161
162	builder := android.NewRuleBuilder(pctx, ctx)
163	builder.Command().Text("rm").Flag("-rf").Text(stagingDir.String())
164	builder.Command().Text("mkdir").Flag("-p").Output(stagingDir)
165	builder.Temporary(stagingDir)
166	ctx.WalkDeps(func(child, parent android.Module) bool {
167		if !child.Enabled(ctx) {
168			return false
169		}
170		if android.EqualModules(parent, ctx.Module()) && ctx.OtherModuleDependencyTag(child) == testPackageZipDepTag {
171			// handle direct deps
172			extendBuilderCommand(ctx, child, builder, stagingDir, productOut, arch, secondArch)
173			return true
174		} else if !android.EqualModules(parent, ctx.Module()) && ctx.OtherModuleDependencyTag(child) == android.RequiredDepTag {
175			// handle the "required" from deps
176			extendBuilderCommand(ctx, child, builder, stagingDir, productOut, arch, secondArch)
177			return true
178		} else {
179			return false
180		}
181	})
182
183	output := android.PathForModuleOut(ctx, ctx.ModuleName()+".zip")
184	builder.Command().
185		BuiltTool("soong_zip").
186		Flag("-o").Output(output).
187		Flag("-C").Text(stagingDir.String()).
188		Flag("-D").Text(stagingDir.String())
189	builder.Command().Text("rm").Flag("-rf").Text(stagingDir.String())
190	builder.Build("test_package", fmt.Sprintf("build test_package for %s", ctx.ModuleName()))
191	return output
192}
193
194func extendBuilderCommand(ctx android.ModuleContext, m android.Module, builder *android.RuleBuilder, stagingDir android.ModuleOutPath, productOut, arch, secondArch string) {
195	info, ok := android.OtherModuleProvider(ctx, m, android.ModuleInfoJSONProvider)
196	if !ok {
197		ctx.OtherModuleErrorf(m, "doesn't set ModuleInfoJSON provider")
198	} else if len(info) != 1 {
199		ctx.OtherModuleErrorf(m, "doesn't provide exactly one ModuleInfoJSON")
200	}
201
202	classes := info[0].GetClass()
203	if len(info[0].Class) != 1 {
204		ctx.OtherModuleErrorf(m, "doesn't have exactly one class in its ModuleInfoJSON")
205	}
206	class := strings.ToLower(classes[0])
207	if class == "apps" {
208		class = "app"
209	} else if class == "java_libraries" {
210		class = "framework"
211	}
212
213	installedFilesInfo, ok := android.OtherModuleProvider(ctx, m, android.InstallFilesProvider)
214	if !ok {
215		ctx.ModuleErrorf("Module %s doesn't set InstallFilesProvider", m.Name())
216	}
217
218	for _, spec := range installedFilesInfo.PackagingSpecs {
219		if spec.SrcPath() == nil {
220			// Probably a symlink
221			continue
222		}
223		installedFile := spec.FullInstallPath()
224
225		ext := installedFile.Ext()
226		// there are additional installed files for some app-class modules, we only need the .apk, .odex and .vdex files in the test package
227		excludeInstalledFile := ext != ".apk" && ext != ".odex" && ext != ".vdex"
228		if class == "app" && excludeInstalledFile {
229			continue
230		}
231		// only .jar files should be included for a framework dep
232		if class == "framework" && ext != ".jar" {
233			continue
234		}
235		name := removeFileExtension(installedFile.Base())
236		// some apks have other apk as installed files, these additional files shouldn't be included
237		isAppOrFramework := class == "app" || class == "framework"
238		if isAppOrFramework && name != ctx.OtherModuleName(m) {
239			continue
240		}
241
242		f := strings.TrimPrefix(installedFile.String(), productOut+"/")
243		if strings.HasPrefix(f, "out") {
244			continue
245		}
246		if strings.HasPrefix(f, "system/") {
247			f = strings.Replace(f, "system/", "DATA/", 1)
248		}
249		f = strings.ReplaceAll(f, filepath.Join("testcases", name, arch), filepath.Join("DATA", class, name))
250		f = strings.ReplaceAll(f, filepath.Join("testcases", name, secondArch), filepath.Join("DATA", class, name))
251		if strings.HasPrefix(f, "testcases") {
252			f = strings.Replace(f, "testcases", filepath.Join("DATA", class), 1)
253		}
254		if strings.HasPrefix(f, "data/") {
255			f = strings.Replace(f, "data/", "DATA/", 1)
256		}
257		f = strings.ReplaceAll(f, "DATA_other", "system_other")
258		f = strings.ReplaceAll(f, "system_other/DATA", "system_other/system")
259		dir := filepath.Dir(f)
260
261		tempOut := android.PathForModuleOut(ctx, "STAGING", f)
262		builder.Command().Text("mkdir").Flag("-p").Text(filepath.Join(stagingDir.String(), dir))
263		// Copy srcPath instead of installedFile because some rules like target-files.zip
264		// are non-hermetic and would be affected if we built the installed files.
265		builder.Command().Text("cp").Flag("-Rf").Input(spec.SrcPath()).Output(tempOut)
266		builder.Temporary(tempOut)
267	}
268}
269
270func removeFileExtension(filename string) string {
271	return strings.TrimSuffix(filename, filepath.Ext(filename))
272}
273
274// The only purpose of this method is to make sure we can build the module directly
275// without adding suffix "-soong"
276func (p *testPackageZip) AndroidMkEntries() []android.AndroidMkEntries {
277	return []android.AndroidMkEntries{
278		android.AndroidMkEntries{
279			Class:      "ETC",
280			OutputFile: android.OptionalPathForPath(p.output),
281			ExtraEntries: []android.AndroidMkExtraEntriesFunc{
282				func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
283					entries.SetBool("LOCAL_UNINSTALLABLE_MODULE", true)
284				}},
285		},
286	}
287}
288