1// 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" 24 "math" 25 "os" 26 "regexp" 27 "sort" 28 "strings" 29 30 "google.golang.org/protobuf/proto" 31 32 "android/soong/cmd/extract_apks/bundle_proto" 33 android_bundle_proto "android/soong/cmd/extract_apks/bundle_proto" 34 "android/soong/third_party/zip" 35) 36 37type TargetConfig struct { 38 sdkVersion int32 39 screenDpi map[android_bundle_proto.ScreenDensity_DensityAlias]bool 40 // Map holding <ABI alias>:<its sequence number in the flag> info. 41 abis map[android_bundle_proto.Abi_AbiAlias]int 42 allowPrereleased bool 43 stem string 44} 45 46// An APK set is a zip archive. An entry 'toc.pb' describes its contents. 47// It is a protobuf message BuildApkResult. 48type Toc *android_bundle_proto.BuildApksResult 49 50type ApkSet struct { 51 path string 52 reader *zip.ReadCloser 53 entries map[string]*zip.File 54} 55 56func newApkSet(path string) (*ApkSet, error) { 57 apkSet := &ApkSet{path: path, entries: make(map[string]*zip.File)} 58 var err error 59 if apkSet.reader, err = zip.OpenReader(apkSet.path); err != nil { 60 return nil, err 61 } 62 for _, f := range apkSet.reader.File { 63 apkSet.entries[f.Name] = f 64 } 65 return apkSet, nil 66} 67 68func (apkSet *ApkSet) getToc() (Toc, error) { 69 var err error 70 tocFile, ok := apkSet.entries["toc.pb"] 71 if !ok { 72 return nil, fmt.Errorf("%s: APK set should have toc.pb entry", apkSet.path) 73 } 74 rc, err := tocFile.Open() 75 if err != nil { 76 return nil, err 77 } 78 bytes := make([]byte, tocFile.FileHeader.UncompressedSize64) 79 if _, err := rc.Read(bytes); err != io.EOF { 80 return nil, err 81 } 82 rc.Close() 83 buildApksResult := new(android_bundle_proto.BuildApksResult) 84 if err = proto.Unmarshal(bytes, buildApksResult); err != nil { 85 return nil, err 86 } 87 return buildApksResult, nil 88} 89 90func (apkSet *ApkSet) close() { 91 apkSet.reader.Close() 92} 93 94// Matchers for selection criteria 95 96type abiTargetingMatcher struct { 97 *android_bundle_proto.AbiTargeting 98} 99 100func (m abiTargetingMatcher) matches(config TargetConfig) bool { 101 if m.AbiTargeting == nil { 102 return true 103 } 104 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok { 105 return true 106 } 107 // Find the one that appears first in the abis flags. 108 abiIdx := math.MaxInt32 109 for _, v := range m.GetValue() { 110 if i, ok := config.abis[v.Alias]; ok { 111 if i < abiIdx { 112 abiIdx = i 113 } 114 } 115 } 116 if abiIdx == math.MaxInt32 { 117 return false 118 } 119 // See if any alternatives appear before the above one. 120 for _, a := range m.GetAlternatives() { 121 if i, ok := config.abis[a.Alias]; ok { 122 if i < abiIdx { 123 // There is a better alternative. Skip this one. 124 return false 125 } 126 } 127 } 128 return true 129} 130 131type apkDescriptionMatcher struct { 132 *android_bundle_proto.ApkDescription 133} 134 135func (m apkDescriptionMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool { 136 return m.ApkDescription == nil || (apkTargetingMatcher{m.Targeting}).matches(config, allAbisMustMatch) 137} 138 139type apkTargetingMatcher struct { 140 *android_bundle_proto.ApkTargeting 141} 142 143func (m apkTargetingMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool { 144 return m.ApkTargeting == nil || 145 (abiTargetingMatcher{m.AbiTargeting}.matches(config) && 146 languageTargetingMatcher{m.LanguageTargeting}.matches(config) && 147 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) && 148 sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) && 149 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config, allAbisMustMatch)) 150} 151 152type languageTargetingMatcher struct { 153 *android_bundle_proto.LanguageTargeting 154} 155 156func (m languageTargetingMatcher) matches(_ TargetConfig) bool { 157 if m.LanguageTargeting == nil { 158 return true 159 } 160 log.Fatal("language based entry selection is not implemented") 161 return false 162} 163 164type moduleMetadataMatcher struct { 165 *android_bundle_proto.ModuleMetadata 166} 167 168func (m moduleMetadataMatcher) matches(config TargetConfig) bool { 169 return m.ModuleMetadata == nil || 170 (m.GetDeliveryType() == android_bundle_proto.DeliveryType_INSTALL_TIME && 171 moduleTargetingMatcher{m.Targeting}.matches(config) && 172 !m.IsInstant) 173} 174 175type moduleTargetingMatcher struct { 176 *android_bundle_proto.ModuleTargeting 177} 178 179func (m moduleTargetingMatcher) matches(config TargetConfig) bool { 180 return m.ModuleTargeting == nil || 181 (sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) && 182 userCountriesTargetingMatcher{m.UserCountriesTargeting}.matches(config)) 183} 184 185// A higher number means a higher priority. 186// This order must be kept identical to bundletool's. 187var multiAbiPriorities = map[android_bundle_proto.Abi_AbiAlias]int{ 188 android_bundle_proto.Abi_ARMEABI: 1, 189 android_bundle_proto.Abi_ARMEABI_V7A: 2, 190 android_bundle_proto.Abi_ARM64_V8A: 3, 191 android_bundle_proto.Abi_X86: 4, 192 android_bundle_proto.Abi_X86_64: 5, 193 android_bundle_proto.Abi_MIPS: 6, 194 android_bundle_proto.Abi_MIPS64: 7, 195} 196 197type multiAbiTargetingMatcher struct { 198 *android_bundle_proto.MultiAbiTargeting 199} 200 201type multiAbiValue []*bundle_proto.Abi 202 203func (m multiAbiValue) compare(other multiAbiValue) int { 204 min := func(a, b int) int { 205 if a < b { 206 return a 207 } 208 return b 209 } 210 211 sortAbis := func(abiSlice multiAbiValue) func(i, j int) bool { 212 return func(i, j int) bool { 213 // sort priorities greatest to least 214 return multiAbiPriorities[abiSlice[i].Alias] > multiAbiPriorities[abiSlice[j].Alias] 215 } 216 } 217 218 sortedM := append(multiAbiValue{}, m...) 219 sort.Slice(sortedM, sortAbis(sortedM)) 220 sortedOther := append(multiAbiValue{}, other...) 221 sort.Slice(sortedOther, sortAbis(sortedOther)) 222 223 for i := 0; i < min(len(sortedM), len(sortedOther)); i++ { 224 if multiAbiPriorities[sortedM[i].Alias] > multiAbiPriorities[sortedOther[i].Alias] { 225 return 1 226 } 227 if multiAbiPriorities[sortedM[i].Alias] < multiAbiPriorities[sortedOther[i].Alias] { 228 return -1 229 } 230 } 231 232 return len(sortedM) - len(sortedOther) 233} 234 235// this logic should match the logic in bundletool at 236// https://github.com/google/bundletool/blob/ae0fc0162fd80d92ef8f4ef4527c066f0106942f/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java#L43 237// (note link is the commit at time of writing; but logic should always match the latest) 238func (t multiAbiTargetingMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool { 239 if t.MultiAbiTargeting == nil { 240 return true 241 } 242 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok { 243 return true 244 } 245 246 multiAbiIsValid := func(m multiAbiValue) bool { 247 numValid := 0 248 for _, abi := range m { 249 if _, ok := config.abis[abi.Alias]; ok { 250 numValid += 1 251 } 252 } 253 if numValid == 0 { 254 return false 255 } else if numValid > 0 && !allAbisMustMatch { 256 return true 257 } else { 258 return numValid == len(m) 259 } 260 } 261 262 // ensure that the current value is valid for our config 263 valueSetContainsViableAbi := false 264 multiAbiSet := t.GetValue() 265 for _, multiAbi := range multiAbiSet { 266 if multiAbiIsValid(multiAbi.GetAbi()) { 267 valueSetContainsViableAbi = true 268 break 269 } 270 } 271 272 if !valueSetContainsViableAbi { 273 return false 274 } 275 276 // See if there are any matching alternatives with a higher priority. 277 for _, altMultiAbi := range t.GetAlternatives() { 278 if !multiAbiIsValid(altMultiAbi.GetAbi()) { 279 continue 280 } 281 282 for _, multiAbi := range multiAbiSet { 283 valueAbis := multiAbiValue(multiAbi.GetAbi()) 284 altAbis := multiAbiValue(altMultiAbi.GetAbi()) 285 if valueAbis.compare(altAbis) < 0 { 286 // An alternative has a higher priority, don't use this one 287 return false 288 } 289 } 290 } 291 292 return true 293} 294 295type screenDensityTargetingMatcher struct { 296 *android_bundle_proto.ScreenDensityTargeting 297} 298 299func (m screenDensityTargetingMatcher) matches(config TargetConfig) bool { 300 if m.ScreenDensityTargeting == nil { 301 return true 302 } 303 if _, ok := config.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED]; ok { 304 return true 305 } 306 for _, v := range m.GetValue() { 307 switch x := v.GetDensityOneof().(type) { 308 case *android_bundle_proto.ScreenDensity_DensityAlias_: 309 if _, ok := config.screenDpi[x.DensityAlias]; ok { 310 return true 311 } 312 default: 313 log.Fatal("For screen density, only DPI name based entry selection (e.g. HDPI, XHDPI) is implemented") 314 } 315 } 316 return false 317} 318 319type sdkVersionTargetingMatcher struct { 320 *android_bundle_proto.SdkVersionTargeting 321} 322 323func (m sdkVersionTargetingMatcher) matches(config TargetConfig) bool { 324 const preReleaseVersion = 10000 325 if m.SdkVersionTargeting == nil { 326 return true 327 } 328 if len(m.Value) > 1 { 329 log.Fatal(fmt.Sprintf("sdk_version_targeting should not have multiple values:%#v", m.Value)) 330 } 331 // Inspect only sdkVersionTargeting.Value. 332 // Even though one of the SdkVersionTargeting.Alternatives values may be 333 // better matching, we will select all of them 334 return m.Value[0].Min == nil || 335 m.Value[0].Min.Value <= config.sdkVersion || 336 (config.allowPrereleased && m.Value[0].Min.Value == preReleaseVersion) 337} 338 339type textureCompressionFormatTargetingMatcher struct { 340 *android_bundle_proto.TextureCompressionFormatTargeting 341} 342 343func (m textureCompressionFormatTargetingMatcher) matches(_ TargetConfig) bool { 344 if m.TextureCompressionFormatTargeting == nil { 345 return true 346 } 347 log.Fatal("texture based entry selection is not implemented") 348 return false 349} 350 351type userCountriesTargetingMatcher struct { 352 *android_bundle_proto.UserCountriesTargeting 353} 354 355func (m userCountriesTargetingMatcher) matches(_ TargetConfig) bool { 356 if m.UserCountriesTargeting == nil { 357 return true 358 } 359 log.Fatal("country based entry selection is not implemented") 360 return false 361} 362 363type variantTargetingMatcher struct { 364 *android_bundle_proto.VariantTargeting 365} 366 367func (m variantTargetingMatcher) matches(config TargetConfig, allAbisMustMatch bool) bool { 368 if m.VariantTargeting == nil { 369 return true 370 } 371 return sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) && 372 abiTargetingMatcher{m.AbiTargeting}.matches(config) && 373 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config, allAbisMustMatch) && 374 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) && 375 textureCompressionFormatTargetingMatcher{m.TextureCompressionFormatTargeting}.matches(config) 376} 377 378type SelectionResult struct { 379 moduleName string 380 entries []string 381} 382 383// Return all entries matching target configuration 384func selectApks(toc Toc, targetConfig TargetConfig) SelectionResult { 385 checkMatching := func(allAbisMustMatch bool) SelectionResult { 386 var result SelectionResult 387 for _, variant := range (*toc).GetVariant() { 388 if !(variantTargetingMatcher{variant.GetTargeting()}.matches(targetConfig, allAbisMustMatch)) { 389 continue 390 } 391 for _, as := range variant.GetApkSet() { 392 if !(moduleMetadataMatcher{as.ModuleMetadata}.matches(targetConfig)) { 393 continue 394 } 395 for _, apkdesc := range as.GetApkDescription() { 396 if (apkDescriptionMatcher{apkdesc}).matches(targetConfig, allAbisMustMatch) { 397 result.entries = append(result.entries, apkdesc.GetPath()) 398 // TODO(asmundak): As it turns out, moduleName which we get from 399 // the ModuleMetadata matches the module names of the generated 400 // entry paths just by coincidence, only for the split APKs. We 401 // need to discuss this with bundletool folks. 402 result.moduleName = as.GetModuleMetadata().GetName() 403 } 404 } 405 // we allow only a single module, so bail out here if we found one 406 if result.moduleName != "" { 407 return result 408 } 409 } 410 } 411 return result 412 } 413 result := checkMatching(true) 414 if result.moduleName == "" { 415 // if there are no matches where all of the ABIs are available in the 416 // TargetConfig, then search again with a looser requirement of at 417 // least one matching ABI 418 // NOTE(b/260130686): this logic diverges from the logic in bundletool 419 // https://github.com/google/bundletool/blob/ae0fc0162fd80d92ef8f4ef4527c066f0106942f/src/main/java/com/android/tools/build/bundletool/device/MultiAbiMatcher.java#L43 420 result = checkMatching(false) 421 } 422 return result 423} 424 425type Zip2ZipWriter interface { 426 CopyFrom(file *zip.File, name string) error 427} 428 429// Writes out selected entries, renaming them as needed 430func (apkSet *ApkSet) writeApks(selected SelectionResult, config TargetConfig, 431 outFile io.Writer, zipWriter Zip2ZipWriter, partition string) ([]string, error) { 432 // Renaming rules: 433 // splits/MODULE-master.apk to STEM.apk 434 // else 435 // splits/MODULE-*.apk to STEM>-$1.apk 436 // TODO(asmundak): 437 // add more rules, for .apex files 438 renameRules := []struct { 439 rex *regexp.Regexp 440 repl string 441 }{ 442 { 443 regexp.MustCompile(`^.*/` + selected.moduleName + `-master\.apk$`), 444 config.stem + `.apk`, 445 }, 446 { 447 regexp.MustCompile(`^.*/` + selected.moduleName + `(-.*\.apk)$`), 448 config.stem + `$1`, 449 }, 450 { 451 regexp.MustCompile(`^universal\.apk$`), 452 config.stem + ".apk", 453 }, 454 } 455 renamer := func(path string) (string, bool) { 456 for _, rr := range renameRules { 457 if rr.rex.MatchString(path) { 458 return rr.rex.ReplaceAllString(path, rr.repl), true 459 } 460 } 461 return "", false 462 } 463 464 entryOrigin := make(map[string]string) // output entry to input entry 465 var apkcerts []string 466 for _, apk := range selected.entries { 467 apkFile, ok := apkSet.entries[apk] 468 if !ok { 469 return nil, fmt.Errorf("TOC refers to an entry %s which does not exist", apk) 470 } 471 inName := apkFile.Name 472 outName, ok := renamer(inName) 473 if !ok { 474 log.Fatalf("selected an entry with unexpected name %s", inName) 475 } 476 if origin, ok := entryOrigin[inName]; ok { 477 log.Fatalf("selected entries %s and %s will have the same output name %s", 478 origin, inName, outName) 479 } 480 entryOrigin[outName] = inName 481 if outName == config.stem+".apk" { 482 if err := writeZipEntryToFile(outFile, apkFile); err != nil { 483 return nil, err 484 } 485 } else { 486 if err := zipWriter.CopyFrom(apkFile, outName); err != nil { 487 return nil, err 488 } 489 } 490 if partition != "" { 491 apkcerts = append(apkcerts, fmt.Sprintf( 492 `name="%s" certificate="PRESIGNED" private_key="" partition="%s"`, outName, partition)) 493 } 494 } 495 sort.Strings(apkcerts) 496 return apkcerts, nil 497} 498 499func (apkSet *ApkSet) extractAndCopySingle(selected SelectionResult, outFile *os.File) error { 500 if len(selected.entries) != 1 { 501 return fmt.Errorf("Too many matching entries for extract-single:\n%v", selected.entries) 502 } 503 apk, ok := apkSet.entries[selected.entries[0]] 504 if !ok { 505 return fmt.Errorf("Couldn't find apk path %s", selected.entries[0]) 506 } 507 return writeZipEntryToFile(outFile, apk) 508} 509 510// Arguments parsing 511var ( 512 outputFile = flag.String("o", "", "output file for primary entry") 513 zipFile = flag.String("zip", "", "output file containing additional extracted entries") 514 targetConfig = TargetConfig{ 515 screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{}, 516 abis: map[android_bundle_proto.Abi_AbiAlias]int{}, 517 } 518 extractSingle = flag.Bool("extract-single", false, 519 "extract a single target and output it uncompressed. only available for standalone apks and apexes.") 520 apkcertsOutput = flag.String("apkcerts", "", 521 "optional apkcerts.txt output file containing signing info of all outputted apks") 522 partition = flag.String("partition", "", "partition string. required when -apkcerts is used.") 523) 524 525// Parse abi values 526type abiFlagValue struct { 527 targetConfig *TargetConfig 528} 529 530func (a abiFlagValue) String() string { 531 return "all" 532} 533 534func (a abiFlagValue) Set(abiList string) error { 535 for i, abi := range strings.Split(abiList, ",") { 536 v, ok := android_bundle_proto.Abi_AbiAlias_value[abi] 537 if !ok { 538 return fmt.Errorf("bad ABI value: %q", abi) 539 } 540 targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = i 541 } 542 return nil 543} 544 545// Parse screen density values 546type screenDensityFlagValue struct { 547 targetConfig *TargetConfig 548} 549 550func (s screenDensityFlagValue) String() string { 551 return "none" 552} 553 554func (s screenDensityFlagValue) Set(densityList string) error { 555 if densityList == "none" { 556 return nil 557 } 558 if densityList == "all" { 559 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true 560 return nil 561 } 562 for _, density := range strings.Split(densityList, ",") { 563 v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density] 564 if !found { 565 return fmt.Errorf("bad screen density value: %q", density) 566 } 567 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true 568 } 569 return nil 570} 571 572func processArgs() { 573 flag.Usage = func() { 574 fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-file> [-zip <output-zip-file>] `+ 575 `-sdk-version value -abis value `+ 576 `-screen-densities value {-stem value | -extract-single} [-allow-prereleased] `+ 577 `[-apkcerts <apkcerts output file> -partition <partition>] <APK set>`) 578 flag.PrintDefaults() 579 os.Exit(2) 580 } 581 version := flag.Uint("sdk-version", 0, "SDK version") 582 flag.Var(abiFlagValue{&targetConfig}, "abis", 583 "comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64") 584 flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities", 585 "'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)") 586 flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false, 587 "allow prereleased") 588 flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name in the output zip file") 589 flag.Parse() 590 if (*outputFile == "") || len(flag.Args()) != 1 || *version == 0 || 591 ((targetConfig.stem == "" || *zipFile == "") && !*extractSingle) || 592 (*apkcertsOutput != "" && *partition == "") { 593 flag.Usage() 594 } 595 targetConfig.sdkVersion = int32(*version) 596 597} 598 599func main() { 600 processArgs() 601 var toc Toc 602 apkSet, err := newApkSet(flag.Arg(0)) 603 if err == nil { 604 defer apkSet.close() 605 toc, err = apkSet.getToc() 606 } 607 if err != nil { 608 log.Fatal(err) 609 } 610 sel := selectApks(toc, targetConfig) 611 if len(sel.entries) == 0 { 612 log.Fatalf("there are no entries for the target configuration: %#v", targetConfig) 613 } 614 615 outFile, err := os.Create(*outputFile) 616 if err != nil { 617 log.Fatal(err) 618 } 619 defer outFile.Close() 620 621 if *extractSingle { 622 err = apkSet.extractAndCopySingle(sel, outFile) 623 } else { 624 zipOutputFile, err := os.Create(*zipFile) 625 if err != nil { 626 log.Fatal(err) 627 } 628 defer zipOutputFile.Close() 629 630 zipWriter := zip.NewWriter(zipOutputFile) 631 defer func() { 632 if err := zipWriter.Close(); err != nil { 633 log.Fatal(err) 634 } 635 }() 636 637 apkcerts, err := apkSet.writeApks(sel, targetConfig, outFile, zipWriter, *partition) 638 if err == nil && *apkcertsOutput != "" { 639 apkcertsFile, err := os.Create(*apkcertsOutput) 640 if err != nil { 641 log.Fatal(err) 642 } 643 defer apkcertsFile.Close() 644 for _, a := range apkcerts { 645 _, err = apkcertsFile.WriteString(a + "\n") 646 if err != nil { 647 log.Fatal(err) 648 } 649 } 650 } 651 } 652 if err != nil { 653 log.Fatal(err) 654 } 655} 656 657func writeZipEntryToFile(outFile io.Writer, zipEntry *zip.File) error { 658 reader, err := zipEntry.Open() 659 if err != nil { 660 return err 661 } 662 defer reader.Close() 663 _, err = io.Copy(outFile, reader) 664 return err 665} 666