blob: e9a850ee83f91f86d29bb8d9783b25968befdcbb [file] [log] [blame]
Sasha Smundak7a894a62020-05-06 21:23:08 -07001// Copyright 2020 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
15// Copies all the entries (APKs/APEXes) matching the target configuration from the given
16// APK set into a zip file. Run it without arguments to see usage details.
17package main
18
19import (
20 "flag"
21 "fmt"
22 "io"
23 "log"
Jaewoong Jungfa00c062020-05-14 14:15:24 -070024 "math"
Sasha Smundak7a894a62020-05-06 21:23:08 -070025 "os"
26 "regexp"
27 "strings"
28
29 "github.com/golang/protobuf/proto"
30
31 "android/soong/cmd/extract_apks/bundle_proto"
32 "android/soong/third_party/zip"
33)
34
35type TargetConfig struct {
Jaewoong Jungfa00c062020-05-14 14:15:24 -070036 sdkVersion int32
37 screenDpi map[android_bundle_proto.ScreenDensity_DensityAlias]bool
38 // Map holding <ABI alias>:<its sequence number in the flag> info.
39 abis map[android_bundle_proto.Abi_AbiAlias]int
Sasha Smundak7a894a62020-05-06 21:23:08 -070040 allowPrereleased bool
41 stem string
42}
43
44// An APK set is a zip archive. An entry 'toc.pb' describes its contents.
45// It is a protobuf message BuildApkResult.
46type Toc *android_bundle_proto.BuildApksResult
47
48type ApkSet struct {
49 path string
50 reader *zip.ReadCloser
51 entries map[string]*zip.File
52}
53
54func newApkSet(path string) (*ApkSet, error) {
55 apkSet := &ApkSet{path: path, entries: make(map[string]*zip.File)}
56 var err error
57 if apkSet.reader, err = zip.OpenReader(apkSet.path); err != nil {
58 return nil, err
59 }
60 for _, f := range apkSet.reader.File {
61 apkSet.entries[f.Name] = f
62 }
63 return apkSet, nil
64}
65
66func (apkSet *ApkSet) getToc() (Toc, error) {
67 var err error
68 tocFile, ok := apkSet.entries["toc.pb"]
69 if !ok {
70 return nil, fmt.Errorf("%s: APK set should have toc.pb entry", apkSet.path)
71 }
72 rc, err := tocFile.Open()
73 if err != nil {
74 return nil, err
75 }
76 bytes := make([]byte, tocFile.FileHeader.UncompressedSize64)
77 if _, err := rc.Read(bytes); err != io.EOF {
78 return nil, err
79 }
80 rc.Close()
81 buildApksResult := new(android_bundle_proto.BuildApksResult)
82 if err = proto.Unmarshal(bytes, buildApksResult); err != nil {
83 return nil, err
84 }
85 return buildApksResult, nil
86}
87
88func (apkSet *ApkSet) close() {
89 apkSet.reader.Close()
90}
91
92// Matchers for selection criteria
Jaewoong Jungfa00c062020-05-14 14:15:24 -070093
Sasha Smundak7a894a62020-05-06 21:23:08 -070094type abiTargetingMatcher struct {
95 *android_bundle_proto.AbiTargeting
96}
97
98func (m abiTargetingMatcher) matches(config TargetConfig) bool {
99 if m.AbiTargeting == nil {
100 return true
101 }
102 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
103 return true
104 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700105 // Find the one that appears first in the abis flags.
106 abiIdx := math.MaxInt32
Sasha Smundak7a894a62020-05-06 21:23:08 -0700107 for _, v := range m.GetValue() {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700108 if i, ok := config.abis[v.Alias]; ok {
109 if i < abiIdx {
110 abiIdx = i
111 }
Sasha Smundak7a894a62020-05-06 21:23:08 -0700112 }
113 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700114 if abiIdx == math.MaxInt32 {
115 return false
116 }
117 // See if any alternatives appear before the above one.
118 for _, a := range m.GetAlternatives() {
119 if i, ok := config.abis[a.Alias]; ok {
120 if i < abiIdx {
121 // There is a better alternative. Skip this one.
122 return false
123 }
124 }
125 }
126 return true
Sasha Smundak7a894a62020-05-06 21:23:08 -0700127}
128
129type apkDescriptionMatcher struct {
130 *android_bundle_proto.ApkDescription
131}
132
133func (m apkDescriptionMatcher) matches(config TargetConfig) bool {
134 return m.ApkDescription == nil || (apkTargetingMatcher{m.Targeting}).matches(config)
135}
136
137type apkTargetingMatcher struct {
138 *android_bundle_proto.ApkTargeting
139}
140
141func (m apkTargetingMatcher) matches(config TargetConfig) bool {
142 return m.ApkTargeting == nil ||
143 (abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
144 languageTargetingMatcher{m.LanguageTargeting}.matches(config) &&
145 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
146 sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
147 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config))
148}
149
150type languageTargetingMatcher struct {
151 *android_bundle_proto.LanguageTargeting
152}
153
154func (m languageTargetingMatcher) matches(_ TargetConfig) bool {
155 if m.LanguageTargeting == nil {
156 return true
157 }
158 log.Fatal("language based entry selection is not implemented")
159 return false
160}
161
162type moduleMetadataMatcher struct {
163 *android_bundle_proto.ModuleMetadata
164}
165
166func (m moduleMetadataMatcher) matches(config TargetConfig) bool {
167 return m.ModuleMetadata == nil ||
168 (m.GetDeliveryType() == android_bundle_proto.DeliveryType_INSTALL_TIME &&
169 moduleTargetingMatcher{m.Targeting}.matches(config) &&
170 !m.IsInstant)
171}
172
173type moduleTargetingMatcher struct {
174 *android_bundle_proto.ModuleTargeting
175}
176
177func (m moduleTargetingMatcher) matches(config TargetConfig) bool {
178 return m.ModuleTargeting == nil ||
179 (sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
180 userCountriesTargetingMatcher{m.UserCountriesTargeting}.matches(config))
181}
182
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700183// A higher number means a higher priority.
184// This order must be kept identical to bundletool's.
185var multiAbiPriorities = map[android_bundle_proto.Abi_AbiAlias]int{
186 android_bundle_proto.Abi_ARMEABI: 1,
187 android_bundle_proto.Abi_ARMEABI_V7A: 2,
188 android_bundle_proto.Abi_ARM64_V8A: 3,
189 android_bundle_proto.Abi_X86: 4,
190 android_bundle_proto.Abi_X86_64: 5,
191 android_bundle_proto.Abi_MIPS: 6,
192 android_bundle_proto.Abi_MIPS64: 7,
193}
194
Sasha Smundak7a894a62020-05-06 21:23:08 -0700195type multiAbiTargetingMatcher struct {
196 *android_bundle_proto.MultiAbiTargeting
197}
198
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700199func (t multiAbiTargetingMatcher) matches(config TargetConfig) bool {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700200 if t.MultiAbiTargeting == nil {
201 return true
202 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700203 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
204 return true
205 }
206 // Find the one with the highest priority.
207 highestPriority := 0
208 for _, v := range t.GetValue() {
209 for _, a := range v.GetAbi() {
210 if _, ok := config.abis[a.Alias]; ok {
211 if highestPriority < multiAbiPriorities[a.Alias] {
212 highestPriority = multiAbiPriorities[a.Alias]
213 }
214 }
215 }
216 }
217 if highestPriority == 0 {
218 return false
219 }
220 // See if there are any matching alternatives with a higher priority.
221 for _, v := range t.GetAlternatives() {
222 for _, a := range v.GetAbi() {
223 if _, ok := config.abis[a.Alias]; ok {
224 if highestPriority < multiAbiPriorities[a.Alias] {
225 // There's a better one. Skip this one.
226 return false
227 }
228 }
229 }
230 }
231 return true
Sasha Smundak7a894a62020-05-06 21:23:08 -0700232}
233
234type screenDensityTargetingMatcher struct {
235 *android_bundle_proto.ScreenDensityTargeting
236}
237
238func (m screenDensityTargetingMatcher) matches(config TargetConfig) bool {
239 if m.ScreenDensityTargeting == nil {
240 return true
241 }
242 if _, ok := config.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED]; ok {
243 return true
244 }
245 for _, v := range m.GetValue() {
246 switch x := v.GetDensityOneof().(type) {
247 case *android_bundle_proto.ScreenDensity_DensityAlias_:
248 if _, ok := config.screenDpi[x.DensityAlias]; ok {
249 return true
250 }
251 default:
252 log.Fatal("For screen density, only DPI name based entry selection (e.g. HDPI, XHDPI) is implemented")
253 }
254 }
255 return false
256}
257
258type sdkVersionTargetingMatcher struct {
259 *android_bundle_proto.SdkVersionTargeting
260}
261
262func (m sdkVersionTargetingMatcher) matches(config TargetConfig) bool {
263 const preReleaseVersion = 10000
264 if m.SdkVersionTargeting == nil {
265 return true
266 }
267 if len(m.Value) > 1 {
268 log.Fatal(fmt.Sprintf("sdk_version_targeting should not have multiple values:%#v", m.Value))
269 }
270 // Inspect only sdkVersionTargeting.Value.
271 // Even though one of the SdkVersionTargeting.Alternatives values may be
272 // better matching, we will select all of them
273 return m.Value[0].Min == nil ||
274 m.Value[0].Min.Value <= config.sdkVersion ||
275 (config.allowPrereleased && m.Value[0].Min.Value == preReleaseVersion)
276}
277
278type textureCompressionFormatTargetingMatcher struct {
279 *android_bundle_proto.TextureCompressionFormatTargeting
280}
281
282func (m textureCompressionFormatTargetingMatcher) matches(_ TargetConfig) bool {
283 if m.TextureCompressionFormatTargeting == nil {
284 return true
285 }
286 log.Fatal("texture based entry selection is not implemented")
287 return false
288}
289
290type userCountriesTargetingMatcher struct {
291 *android_bundle_proto.UserCountriesTargeting
292}
293
294func (m userCountriesTargetingMatcher) matches(_ TargetConfig) bool {
295 if m.UserCountriesTargeting == nil {
296 return true
297 }
298 log.Fatal("country based entry selection is not implemented")
299 return false
300}
301
302type variantTargetingMatcher struct {
303 *android_bundle_proto.VariantTargeting
304}
305
306func (m variantTargetingMatcher) matches(config TargetConfig) bool {
307 if m.VariantTargeting == nil {
308 return true
309 }
310 return sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
311 abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
312 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config) &&
313 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
314 textureCompressionFormatTargetingMatcher{m.TextureCompressionFormatTargeting}.matches(config)
315}
316
317type SelectionResult struct {
318 moduleName string
319 entries []string
320}
321
322// Return all entries matching target configuration
323func selectApks(toc Toc, targetConfig TargetConfig) SelectionResult {
324 var result SelectionResult
325 for _, variant := range (*toc).GetVariant() {
326 if !(variantTargetingMatcher{variant.GetTargeting()}.matches(targetConfig)) {
327 continue
328 }
329 for _, as := range variant.GetApkSet() {
330 if !(moduleMetadataMatcher{as.ModuleMetadata}.matches(targetConfig)) {
331 continue
332 }
333 for _, apkdesc := range as.GetApkDescription() {
334 if (apkDescriptionMatcher{apkdesc}).matches(targetConfig) {
335 result.entries = append(result.entries, apkdesc.GetPath())
336 // TODO(asmundak): As it turns out, moduleName which we get from
337 // the ModuleMetadata matches the module names of the generated
338 // entry paths just by coincidence, only for the split APKs. We
339 // need to discuss this with bundletool folks.
340 result.moduleName = as.GetModuleMetadata().GetName()
341 }
342 }
343 // we allow only a single module, so bail out here if we found one
344 if result.moduleName != "" {
345 return result
346 }
347 }
348 }
349 return result
350}
351
352type Zip2ZipWriter interface {
353 CopyFrom(file *zip.File, name string) error
354}
355
356// Writes out selected entries, renaming them as needed
357func (apkSet *ApkSet) writeApks(selected SelectionResult, config TargetConfig,
358 writer Zip2ZipWriter) error {
359 // Renaming rules:
360 // splits/MODULE-master.apk to STEM.apk
361 // else
362 // splits/MODULE-*.apk to STEM>-$1.apk
363 // TODO(asmundak):
364 // add more rules, for .apex files
365 renameRules := []struct {
366 rex *regexp.Regexp
367 repl string
368 }{
369 {
370 regexp.MustCompile(`^.*/` + selected.moduleName + `-master\.apk$`),
371 config.stem + `.apk`,
372 },
373 {
374 regexp.MustCompile(`^.*/` + selected.moduleName + `(-.*\.apk)$`),
375 config.stem + `$1`,
376 },
Sasha Smundak827c55f2020-05-20 13:10:59 -0700377 {
378 regexp.MustCompile(`^universal\.apk$`),
379 config.stem + ".apk",
380 },
Sasha Smundak7a894a62020-05-06 21:23:08 -0700381 }
382 renamer := func(path string) (string, bool) {
383 for _, rr := range renameRules {
384 if rr.rex.MatchString(path) {
385 return rr.rex.ReplaceAllString(path, rr.repl), true
386 }
387 }
388 return "", false
389 }
390
391 entryOrigin := make(map[string]string) // output entry to input entry
392 for _, apk := range selected.entries {
393 apkFile, ok := apkSet.entries[apk]
394 if !ok {
395 return fmt.Errorf("TOC refers to an entry %s which does not exist", apk)
396 }
397 inName := apkFile.Name
398 outName, ok := renamer(inName)
399 if !ok {
400 log.Fatalf("selected an entry with unexpected name %s", inName)
401 }
402 if origin, ok := entryOrigin[inName]; ok {
403 log.Fatalf("selected entries %s and %s will have the same output name %s",
404 origin, inName, outName)
405 }
406 entryOrigin[outName] = inName
407 if err := writer.CopyFrom(apkFile, outName); err != nil {
408 return err
409 }
410 }
411 return nil
412}
413
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700414func (apkSet *ApkSet) extractAndCopySingle(selected SelectionResult, outFile *os.File) error {
415 if len(selected.entries) != 1 {
416 return fmt.Errorf("Too many matching entries for extract-single:\n%v", selected.entries)
417 }
418 apk, ok := apkSet.entries[selected.entries[0]]
419 if !ok {
420 return fmt.Errorf("Couldn't find apk path %s", selected.entries[0])
421 }
422 inputReader, _ := apk.Open()
423 _, err := io.Copy(outFile, inputReader)
424 return err
425}
426
Sasha Smundak7a894a62020-05-06 21:23:08 -0700427// Arguments parsing
428var (
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700429 outputFile = flag.String("o", "", "output file containing extracted entries")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700430 targetConfig = TargetConfig{
431 screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{},
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700432 abis: map[android_bundle_proto.Abi_AbiAlias]int{},
Sasha Smundak7a894a62020-05-06 21:23:08 -0700433 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700434 extractSingle = flag.Bool("extract-single", false,
435 "extract a single target and output it uncompressed. only available for standalone apks and apexes.")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700436)
437
438// Parse abi values
439type abiFlagValue struct {
440 targetConfig *TargetConfig
441}
442
443func (a abiFlagValue) String() string {
444 return "all"
445}
446
447func (a abiFlagValue) Set(abiList string) error {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700448 for i, abi := range strings.Split(abiList, ",") {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700449 v, ok := android_bundle_proto.Abi_AbiAlias_value[abi]
450 if !ok {
451 return fmt.Errorf("bad ABI value: %q", abi)
452 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700453 targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = i
Sasha Smundak7a894a62020-05-06 21:23:08 -0700454 }
455 return nil
456}
457
458// Parse screen density values
459type screenDensityFlagValue struct {
460 targetConfig *TargetConfig
461}
462
463func (s screenDensityFlagValue) String() string {
464 return "none"
465}
466
467func (s screenDensityFlagValue) Set(densityList string) error {
468 if densityList == "none" {
469 return nil
470 }
471 if densityList == "all" {
472 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true
473 return nil
474 }
475 for _, density := range strings.Split(densityList, ",") {
476 v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density]
477 if !found {
478 return fmt.Errorf("bad screen density value: %q", density)
479 }
480 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true
481 }
482 return nil
483}
484
485func processArgs() {
486 flag.Usage = func() {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700487 fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-file> -sdk-version value -abis value `+
488 `-screen-densities value {-stem value | -extract-single} [-allow-prereleased] <APK set>`)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700489 flag.PrintDefaults()
490 os.Exit(2)
491 }
492 version := flag.Uint("sdk-version", 0, "SDK version")
493 flag.Var(abiFlagValue{&targetConfig}, "abis",
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700494 "comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700495 flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities",
496 "'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)")
497 flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false,
498 "allow prereleased")
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700499 flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name in the output zip file")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700500 flag.Parse()
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700501 if (*outputFile == "") || len(flag.Args()) != 1 || *version == 0 || (targetConfig.stem == "" && !*extractSingle) {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700502 flag.Usage()
503 }
504 targetConfig.sdkVersion = int32(*version)
505
506}
507
508func main() {
509 processArgs()
510 var toc Toc
511 apkSet, err := newApkSet(flag.Arg(0))
512 if err == nil {
513 defer apkSet.close()
514 toc, err = apkSet.getToc()
515 }
516 if err != nil {
517 log.Fatal(err)
518 }
519 sel := selectApks(toc, targetConfig)
520 if len(sel.entries) == 0 {
521 log.Fatalf("there are no entries for the target configuration: %#v", targetConfig)
522 }
523
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700524 outFile, err := os.Create(*outputFile)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700525 if err != nil {
526 log.Fatal(err)
527 }
528 defer outFile.Close()
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700529
530 if *extractSingle {
531 err = apkSet.extractAndCopySingle(sel, outFile)
532 } else {
533 writer := zip.NewWriter(outFile)
534 defer func() {
535 if err := writer.Close(); err != nil {
536 log.Fatal(err)
537 }
538 }()
539 err = apkSet.writeApks(sel, targetConfig, writer)
540 }
541 if err != nil {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700542 log.Fatal(err)
543 }
544}