xref: /aosp_15_r20/build/soong/java/aapt2.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1// Copyright 2017 Google Inc. All rights reserved.
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 java
16
17import (
18	"fmt"
19	"path/filepath"
20	"regexp"
21	"sort"
22	"strconv"
23	"strings"
24
25	"github.com/google/blueprint"
26
27	"android/soong/android"
28)
29
30func isPathValueResource(res android.Path) bool {
31	subDir := filepath.Dir(res.String())
32	subDir, lastDir := filepath.Split(subDir)
33	return strings.HasPrefix(lastDir, "values")
34}
35
36func isFlagsPath(subDir string) bool {
37	re := regexp.MustCompile(`flag\(!?([a-zA-Z_-]+\.)*[a-zA-Z0-9_-]+\)`)
38	lastDir := filepath.Base(subDir)
39	return re.MatchString(lastDir)
40}
41
42// Convert input resource file path to output file path.
43// values-[config]/<file>.xml -> values-[config]_<file>.arsc.flat;
44// flag(fully.qualified.flag_name)/values-[config]/<file>.xml -> /values-[config]_<file>.(fully.qualified.flag_name).arsc.flat;
45// For other resource file, just replace the last "/" with "_" and add .flat extension.
46func pathToAapt2Path(ctx android.ModuleContext, res android.Path) android.WritablePath {
47
48	extension := filepath.Ext(res.Base())
49	name := strings.TrimSuffix(res.Base(), extension)
50	if isPathValueResource(res) {
51		extension = ".arsc"
52	}
53	subDir := filepath.Dir(res.String())
54	subDir, lastDir := filepath.Split(subDir)
55	if isFlagsPath(subDir) {
56		var flag string
57		subDir, flag = filepath.Split(filepath.Dir(subDir))
58		flag = strings.TrimPrefix(flag, "flag")
59		name = fmt.Sprintf("%s_%s.%s%s.flat", lastDir, name, flag, extension)
60	} else {
61		name = fmt.Sprintf("%s_%s%s.flat", lastDir, name, extension)
62	}
63	out := android.PathForModuleOut(ctx, "aapt2", subDir, name)
64	return out
65}
66
67// pathsToAapt2Paths Calls pathToAapt2Path on each entry of the given Paths, i.e. []Path.
68func pathsToAapt2Paths(ctx android.ModuleContext, resPaths android.Paths) android.WritablePaths {
69	outPaths := make(android.WritablePaths, len(resPaths))
70
71	for i, res := range resPaths {
72		outPaths[i] = pathToAapt2Path(ctx, res)
73	}
74
75	return outPaths
76}
77
78// Shard resource files for efficiency. See aapt2Compile for details.
79const AAPT2_SHARD_SIZE = 100
80
81var aapt2CompileRule = pctx.AndroidStaticRule("aapt2Compile",
82	blueprint.RuleParams{
83		Command:     `${config.Aapt2Cmd} compile -o $outDir $cFlags $in`,
84		CommandDeps: []string{"${config.Aapt2Cmd}"},
85	},
86	"outDir", "cFlags")
87
88// aapt2Compile compiles resources and puts the results in the requested directory.
89func aapt2Compile(ctx android.ModuleContext, dir android.Path, paths android.Paths,
90	flags []string, productToFilter string, featureFlagsPaths android.Paths) android.WritablePaths {
91	if productToFilter != "" && productToFilter != "default" {
92		// --filter-product leaves only product-specific resources. Product-specific resources only exist
93		// in value resources (values/*.xml), so filter value resource files only. Ignore other types of
94		// resources as they don't need to be in product characteristics RRO (and they will cause aapt2
95		// compile errors)
96		filteredPaths := android.Paths{}
97		for _, path := range paths {
98			if isPathValueResource(path) {
99				filteredPaths = append(filteredPaths, path)
100			}
101		}
102		paths = filteredPaths
103		flags = append([]string{"--filter-product " + productToFilter}, flags...)
104	}
105
106	for _, featureFlagsPath := range android.SortedUniquePaths(featureFlagsPaths) {
107		flags = append(flags, "--feature-flags", "@"+featureFlagsPath.String())
108	}
109
110	// Shard the input paths so that they can be processed in parallel. If we shard them into too
111	// small chunks, the additional cost of spinning up aapt2 outweighs the performance gain. The
112	// current shard size, 100, seems to be a good balance between the added cost and the gain.
113	// The aapt2 compile actions are trivially short, but each action in ninja takes on the order of
114	// ~10 ms to run. frameworks/base/core/res/res has >10k resource files, so compiling each one
115	// with an individual action could take 100 CPU seconds. Sharding them reduces the overhead of
116	// starting actions by a factor of 100, at the expense of recompiling more files when one
117	// changes.  Since the individual compiles are trivial it's a good tradeoff.
118	shards := android.ShardPaths(paths, AAPT2_SHARD_SIZE)
119
120	ret := make(android.WritablePaths, 0, len(paths))
121
122	for i, shard := range shards {
123		// This should be kept in sync with pathToAapt2Path. The aapt2 compile command takes an
124		// output directory path, but not output file paths. So, outPaths is just where we expect
125		// the output files will be located.
126		outPaths := pathsToAapt2Paths(ctx, shard)
127		ret = append(ret, outPaths...)
128
129		shardDesc := ""
130		if i != 0 {
131			shardDesc = " " + strconv.Itoa(i+1)
132		}
133
134		ctx.Build(pctx, android.BuildParams{
135			Rule:        aapt2CompileRule,
136			Description: "aapt2 compile " + dir.String() + shardDesc,
137			Implicits:   featureFlagsPaths,
138			Inputs:      shard,
139			Outputs:     outPaths,
140			Args: map[string]string{
141				// The aapt2 compile command takes an output directory path, but not output file paths.
142				// outPaths specified above is only used for dependency management purposes. In order for
143				// the outPaths values to match the actual outputs from aapt2, the dir parameter value
144				// must be a common prefix path of the paths values, and the top-level path segment used
145				// below, "aapt2", must always be kept in sync with the one in pathToAapt2Path.
146				// TODO(b/174505750): Make this easier and robust to use.
147				"outDir": android.PathForModuleOut(ctx, "aapt2", dir.String()).String(),
148				"cFlags": strings.Join(flags, " "),
149			},
150		})
151	}
152
153	sort.Slice(ret, func(i, j int) bool {
154		return ret[i].String() < ret[j].String()
155	})
156	return ret
157}
158
159var aapt2CompileZipRule = pctx.AndroidStaticRule("aapt2CompileZip",
160	blueprint.RuleParams{
161		Command: `${config.ZipSyncCmd} -d $resZipDir $zipSyncFlags $in && ` +
162			`${config.Aapt2Cmd} compile -o $out $cFlags --dir $resZipDir`,
163		CommandDeps: []string{
164			"${config.Aapt2Cmd}",
165			"${config.ZipSyncCmd}",
166		},
167	}, "cFlags", "resZipDir", "zipSyncFlags")
168
169// Unzips the given compressed file and compiles the resource source files in it. The zipPrefix
170// parameter points to the subdirectory in the zip file where the resource files are located.
171func aapt2CompileZip(ctx android.ModuleContext, flata android.WritablePath, zip android.Path, zipPrefix string,
172	flags []string) {
173
174	if zipPrefix != "" {
175		zipPrefix = "--zip-prefix " + zipPrefix
176	}
177	ctx.Build(pctx, android.BuildParams{
178		Rule:        aapt2CompileZipRule,
179		Description: "aapt2 compile zip",
180		Input:       zip,
181		Output:      flata,
182		Args: map[string]string{
183			"cFlags":       strings.Join(flags, " "),
184			"resZipDir":    android.PathForModuleOut(ctx, "aapt2", "reszip", flata.Base()).String(),
185			"zipSyncFlags": zipPrefix,
186		},
187	})
188}
189
190var aapt2LinkRule = pctx.AndroidStaticRule("aapt2Link",
191	blueprint.RuleParams{
192		Command: `$preamble` +
193			`${config.Aapt2Cmd} link -o $out $flags --proguard $proguardOptions ` +
194			`--output-text-symbols ${rTxt} $inFlags` +
195			`$postamble`,
196
197		CommandDeps: []string{
198			"${config.Aapt2Cmd}",
199			"${config.SoongZipCmd}",
200		},
201		Restat: true,
202	},
203	"flags", "inFlags", "proguardOptions", "rTxt", "extraPackages", "preamble", "postamble")
204
205var aapt2ExtractExtraPackagesRule = pctx.AndroidStaticRule("aapt2ExtractExtraPackages",
206	blueprint.RuleParams{
207		Command:     `${config.ExtractJarPackagesCmd} -i $in -o $out --prefix '--extra-packages '`,
208		CommandDeps: []string{"${config.ExtractJarPackagesCmd}"},
209		Restat:      true,
210	})
211
212var fileListToFileRule = pctx.AndroidStaticRule("fileListToFile",
213	blueprint.RuleParams{
214		Command:        `cp $out.rsp $out`,
215		Rspfile:        "$out.rsp",
216		RspfileContent: "$in",
217	})
218
219var mergeAssetsRule = pctx.AndroidStaticRule("mergeAssets",
220	blueprint.RuleParams{
221		Command:     `${config.MergeZipsCmd} ${out} ${in}`,
222		CommandDeps: []string{"${config.MergeZipsCmd}"},
223	})
224
225func aapt2Link(ctx android.ModuleContext,
226	packageRes, genJar, proguardOptions, rTxt android.WritablePath,
227	flags []string, deps android.Paths,
228	compiledRes, compiledOverlay, assetPackages android.Paths, splitPackages android.WritablePaths,
229	featureFlagsPaths android.Paths) {
230
231	var inFlags []string
232
233	if len(compiledRes) > 0 {
234		// Create a file that contains the list of all compiled resource file paths.
235		resFileList := android.PathForModuleOut(ctx, "aapt2", "res.list")
236		// Write out file lists to files
237		ctx.Build(pctx, android.BuildParams{
238			Rule:        fileListToFileRule,
239			Description: "resource file list",
240			Inputs:      compiledRes,
241			Output:      resFileList,
242		})
243
244		deps = append(deps, compiledRes...)
245		deps = append(deps, resFileList)
246		// aapt2 filepath arguments that start with "@" mean file-list files.
247		inFlags = append(inFlags, "@"+resFileList.String())
248	}
249
250	if len(compiledOverlay) > 0 {
251		// Compiled overlay files are processed the same way as compiled resources.
252		overlayFileList := android.PathForModuleOut(ctx, "aapt2", "overlay.list")
253		ctx.Build(pctx, android.BuildParams{
254			Rule:        fileListToFileRule,
255			Description: "overlay resource file list",
256			Inputs:      compiledOverlay,
257			Output:      overlayFileList,
258		})
259
260		deps = append(deps, compiledOverlay...)
261		deps = append(deps, overlayFileList)
262		// Compiled overlay files are passed over to aapt2 using -R option.
263		inFlags = append(inFlags, "-R", "@"+overlayFileList.String())
264	}
265
266	// Set auxiliary outputs as implicit outputs to establish correct dependency chains.
267	implicitOutputs := append(splitPackages, proguardOptions, rTxt)
268	linkOutput := packageRes
269
270	// AAPT2 ignores assets in overlays. Merge them after linking.
271	if len(assetPackages) > 0 {
272		linkOutput = android.PathForModuleOut(ctx, "aapt2", "package-res.apk")
273		inputZips := append(android.Paths{linkOutput}, assetPackages...)
274		ctx.Build(pctx, android.BuildParams{
275			Rule:        mergeAssetsRule,
276			Inputs:      inputZips,
277			Output:      packageRes,
278			Description: "merge assets from dependencies",
279		})
280	}
281
282	for _, featureFlagsPath := range featureFlagsPaths {
283		deps = append(deps, featureFlagsPath)
284		inFlags = append(inFlags, "--feature-flags", "@"+featureFlagsPath.String())
285	}
286
287	// Note the absence of splitPackages. The caller is supposed to compose and provide --split flag
288	// values via the flags parameter when it wants to split outputs.
289	// TODO(b/174509108): Perhaps we can process it in this func while keeping the code reasonably
290	// tidy.
291	args := map[string]string{
292		"flags":           strings.Join(flags, " "),
293		"inFlags":         strings.Join(inFlags, " "),
294		"proguardOptions": proguardOptions.String(),
295		"rTxt":            rTxt.String(),
296	}
297
298	if genJar != nil {
299		// Generating java source files from aapt2 was requested, use aapt2LinkAndGenRule and pass it
300		// genJar and genDir args.
301		genDir := android.PathForModuleGen(ctx, "aapt2", "R")
302		ctx.Variable(pctx, "aapt2GenDir", genDir.String())
303		ctx.Variable(pctx, "aapt2GenJar", genJar.String())
304		implicitOutputs = append(implicitOutputs, genJar)
305		args["preamble"] = `rm -rf $aapt2GenDir && `
306		args["postamble"] = `&& ${config.SoongZipCmd} -write_if_changed -jar -o $aapt2GenJar -C $aapt2GenDir -D $aapt2GenDir && ` +
307			`rm -rf $aapt2GenDir`
308		args["flags"] += " --java $aapt2GenDir"
309	}
310
311	ctx.Build(pctx, android.BuildParams{
312		Rule:            aapt2LinkRule,
313		Description:     "aapt2 link",
314		Implicits:       deps,
315		Output:          linkOutput,
316		ImplicitOutputs: implicitOutputs,
317		Args:            args,
318	})
319}
320
321// aapt2ExtractExtraPackages takes a srcjar generated by aapt2 or a classes jar generated by ResourceProcessorBusyBox
322// and converts it to a text file containing a list of --extra_package arguments for passing to Make modules so they
323// correctly generate R.java entries for packages provided by transitive dependencies.
324func aapt2ExtractExtraPackages(ctx android.ModuleContext, out android.WritablePath, in android.Path) {
325	ctx.Build(pctx, android.BuildParams{
326		Rule:        aapt2ExtractExtraPackagesRule,
327		Description: "aapt2 extract extra packages",
328		Input:       in,
329		Output:      out,
330	})
331}
332
333var aapt2ConvertRule = pctx.AndroidStaticRule("aapt2Convert",
334	blueprint.RuleParams{
335		Command: `${config.Aapt2Cmd} convert --enable-compact-entries ` +
336			`--output-format $format $in -o $out`,
337		CommandDeps: []string{"${config.Aapt2Cmd}"},
338	}, "format",
339)
340
341// Converts xml files and resource tables (resources.arsc) in the given jar/apk file to a proto
342// format. The proto definition is available at frameworks/base/tools/aapt2/Resources.proto.
343func aapt2Convert(ctx android.ModuleContext, out android.WritablePath, in android.Path, format string) {
344	ctx.Build(pctx, android.BuildParams{
345		Rule:        aapt2ConvertRule,
346		Input:       in,
347		Output:      out,
348		Description: "convert to " + format,
349		Args: map[string]string{
350			"format": format,
351		},
352	})
353}
354