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