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