blob: db54ffbafb4a67f2de34512a0ca7fd86d5c87059 [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"
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -070027 "sort"
Sasha Smundak7a894a62020-05-06 21:23:08 -070028 "strings"
29
30 "github.com/golang/protobuf/proto"
31
32 "android/soong/cmd/extract_apks/bundle_proto"
33 "android/soong/third_party/zip"
34)
35
36type TargetConfig struct {
Jaewoong Jungfa00c062020-05-14 14:15:24 -070037 sdkVersion int32
38 screenDpi map[android_bundle_proto.ScreenDensity_DensityAlias]bool
39 // Map holding <ABI alias>:<its sequence number in the flag> info.
40 abis map[android_bundle_proto.Abi_AbiAlias]int
Sasha Smundak7a894a62020-05-06 21:23:08 -070041 allowPrereleased bool
42 stem string
43}
44
45// An APK set is a zip archive. An entry 'toc.pb' describes its contents.
46// It is a protobuf message BuildApkResult.
47type Toc *android_bundle_proto.BuildApksResult
48
49type ApkSet struct {
50 path string
51 reader *zip.ReadCloser
52 entries map[string]*zip.File
53}
54
55func newApkSet(path string) (*ApkSet, error) {
56 apkSet := &ApkSet{path: path, entries: make(map[string]*zip.File)}
57 var err error
58 if apkSet.reader, err = zip.OpenReader(apkSet.path); err != nil {
59 return nil, err
60 }
61 for _, f := range apkSet.reader.File {
62 apkSet.entries[f.Name] = f
63 }
64 return apkSet, nil
65}
66
67func (apkSet *ApkSet) getToc() (Toc, error) {
68 var err error
69 tocFile, ok := apkSet.entries["toc.pb"]
70 if !ok {
71 return nil, fmt.Errorf("%s: APK set should have toc.pb entry", apkSet.path)
72 }
73 rc, err := tocFile.Open()
74 if err != nil {
75 return nil, err
76 }
77 bytes := make([]byte, tocFile.FileHeader.UncompressedSize64)
78 if _, err := rc.Read(bytes); err != io.EOF {
79 return nil, err
80 }
81 rc.Close()
82 buildApksResult := new(android_bundle_proto.BuildApksResult)
83 if err = proto.Unmarshal(bytes, buildApksResult); err != nil {
84 return nil, err
85 }
86 return buildApksResult, nil
87}
88
89func (apkSet *ApkSet) close() {
90 apkSet.reader.Close()
91}
92
93// Matchers for selection criteria
Jaewoong Jungfa00c062020-05-14 14:15:24 -070094
Sasha Smundak7a894a62020-05-06 21:23:08 -070095type abiTargetingMatcher struct {
96 *android_bundle_proto.AbiTargeting
97}
98
99func (m abiTargetingMatcher) matches(config TargetConfig) bool {
100 if m.AbiTargeting == nil {
101 return true
102 }
103 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
104 return true
105 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700106 // Find the one that appears first in the abis flags.
107 abiIdx := math.MaxInt32
Sasha Smundak7a894a62020-05-06 21:23:08 -0700108 for _, v := range m.GetValue() {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700109 if i, ok := config.abis[v.Alias]; ok {
110 if i < abiIdx {
111 abiIdx = i
112 }
Sasha Smundak7a894a62020-05-06 21:23:08 -0700113 }
114 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700115 if abiIdx == math.MaxInt32 {
116 return false
117 }
118 // See if any alternatives appear before the above one.
119 for _, a := range m.GetAlternatives() {
120 if i, ok := config.abis[a.Alias]; ok {
121 if i < abiIdx {
122 // There is a better alternative. Skip this one.
123 return false
124 }
125 }
126 }
127 return true
Sasha Smundak7a894a62020-05-06 21:23:08 -0700128}
129
130type apkDescriptionMatcher struct {
131 *android_bundle_proto.ApkDescription
132}
133
134func (m apkDescriptionMatcher) matches(config TargetConfig) bool {
135 return m.ApkDescription == nil || (apkTargetingMatcher{m.Targeting}).matches(config)
136}
137
138type apkTargetingMatcher struct {
139 *android_bundle_proto.ApkTargeting
140}
141
142func (m apkTargetingMatcher) matches(config TargetConfig) bool {
143 return m.ApkTargeting == nil ||
144 (abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
145 languageTargetingMatcher{m.LanguageTargeting}.matches(config) &&
146 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
147 sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
148 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config))
149}
150
151type languageTargetingMatcher struct {
152 *android_bundle_proto.LanguageTargeting
153}
154
155func (m languageTargetingMatcher) matches(_ TargetConfig) bool {
156 if m.LanguageTargeting == nil {
157 return true
158 }
159 log.Fatal("language based entry selection is not implemented")
160 return false
161}
162
163type moduleMetadataMatcher struct {
164 *android_bundle_proto.ModuleMetadata
165}
166
167func (m moduleMetadataMatcher) matches(config TargetConfig) bool {
168 return m.ModuleMetadata == nil ||
169 (m.GetDeliveryType() == android_bundle_proto.DeliveryType_INSTALL_TIME &&
170 moduleTargetingMatcher{m.Targeting}.matches(config) &&
171 !m.IsInstant)
172}
173
174type moduleTargetingMatcher struct {
175 *android_bundle_proto.ModuleTargeting
176}
177
178func (m moduleTargetingMatcher) matches(config TargetConfig) bool {
179 return m.ModuleTargeting == nil ||
180 (sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
181 userCountriesTargetingMatcher{m.UserCountriesTargeting}.matches(config))
182}
183
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700184// A higher number means a higher priority.
185// This order must be kept identical to bundletool's.
186var multiAbiPriorities = map[android_bundle_proto.Abi_AbiAlias]int{
187 android_bundle_proto.Abi_ARMEABI: 1,
188 android_bundle_proto.Abi_ARMEABI_V7A: 2,
189 android_bundle_proto.Abi_ARM64_V8A: 3,
190 android_bundle_proto.Abi_X86: 4,
191 android_bundle_proto.Abi_X86_64: 5,
192 android_bundle_proto.Abi_MIPS: 6,
193 android_bundle_proto.Abi_MIPS64: 7,
194}
195
Sasha Smundak7a894a62020-05-06 21:23:08 -0700196type multiAbiTargetingMatcher struct {
197 *android_bundle_proto.MultiAbiTargeting
198}
199
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700200func (t multiAbiTargetingMatcher) matches(config TargetConfig) bool {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700201 if t.MultiAbiTargeting == nil {
202 return true
203 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700204 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
205 return true
206 }
207 // Find the one with the highest priority.
208 highestPriority := 0
209 for _, v := range t.GetValue() {
210 for _, a := range v.GetAbi() {
211 if _, ok := config.abis[a.Alias]; ok {
212 if highestPriority < multiAbiPriorities[a.Alias] {
213 highestPriority = multiAbiPriorities[a.Alias]
214 }
215 }
216 }
217 }
218 if highestPriority == 0 {
219 return false
220 }
221 // See if there are any matching alternatives with a higher priority.
222 for _, v := range t.GetAlternatives() {
223 for _, a := range v.GetAbi() {
224 if _, ok := config.abis[a.Alias]; ok {
225 if highestPriority < multiAbiPriorities[a.Alias] {
226 // There's a better one. Skip this one.
227 return false
228 }
229 }
230 }
231 }
232 return true
Sasha Smundak7a894a62020-05-06 21:23:08 -0700233}
234
235type screenDensityTargetingMatcher struct {
236 *android_bundle_proto.ScreenDensityTargeting
237}
238
239func (m screenDensityTargetingMatcher) matches(config TargetConfig) bool {
240 if m.ScreenDensityTargeting == nil {
241 return true
242 }
243 if _, ok := config.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED]; ok {
244 return true
245 }
246 for _, v := range m.GetValue() {
247 switch x := v.GetDensityOneof().(type) {
248 case *android_bundle_proto.ScreenDensity_DensityAlias_:
249 if _, ok := config.screenDpi[x.DensityAlias]; ok {
250 return true
251 }
252 default:
253 log.Fatal("For screen density, only DPI name based entry selection (e.g. HDPI, XHDPI) is implemented")
254 }
255 }
256 return false
257}
258
259type sdkVersionTargetingMatcher struct {
260 *android_bundle_proto.SdkVersionTargeting
261}
262
263func (m sdkVersionTargetingMatcher) matches(config TargetConfig) bool {
264 const preReleaseVersion = 10000
265 if m.SdkVersionTargeting == nil {
266 return true
267 }
268 if len(m.Value) > 1 {
269 log.Fatal(fmt.Sprintf("sdk_version_targeting should not have multiple values:%#v", m.Value))
270 }
271 // Inspect only sdkVersionTargeting.Value.
272 // Even though one of the SdkVersionTargeting.Alternatives values may be
273 // better matching, we will select all of them
274 return m.Value[0].Min == nil ||
275 m.Value[0].Min.Value <= config.sdkVersion ||
276 (config.allowPrereleased && m.Value[0].Min.Value == preReleaseVersion)
277}
278
279type textureCompressionFormatTargetingMatcher struct {
280 *android_bundle_proto.TextureCompressionFormatTargeting
281}
282
283func (m textureCompressionFormatTargetingMatcher) matches(_ TargetConfig) bool {
284 if m.TextureCompressionFormatTargeting == nil {
285 return true
286 }
287 log.Fatal("texture based entry selection is not implemented")
288 return false
289}
290
291type userCountriesTargetingMatcher struct {
292 *android_bundle_proto.UserCountriesTargeting
293}
294
295func (m userCountriesTargetingMatcher) matches(_ TargetConfig) bool {
296 if m.UserCountriesTargeting == nil {
297 return true
298 }
299 log.Fatal("country based entry selection is not implemented")
300 return false
301}
302
303type variantTargetingMatcher struct {
304 *android_bundle_proto.VariantTargeting
305}
306
307func (m variantTargetingMatcher) matches(config TargetConfig) bool {
308 if m.VariantTargeting == nil {
309 return true
310 }
311 return sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
312 abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
313 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config) &&
314 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
315 textureCompressionFormatTargetingMatcher{m.TextureCompressionFormatTargeting}.matches(config)
316}
317
318type SelectionResult struct {
319 moduleName string
320 entries []string
321}
322
323// Return all entries matching target configuration
324func selectApks(toc Toc, targetConfig TargetConfig) SelectionResult {
325 var result SelectionResult
326 for _, variant := range (*toc).GetVariant() {
327 if !(variantTargetingMatcher{variant.GetTargeting()}.matches(targetConfig)) {
328 continue
329 }
330 for _, as := range variant.GetApkSet() {
331 if !(moduleMetadataMatcher{as.ModuleMetadata}.matches(targetConfig)) {
332 continue
333 }
334 for _, apkdesc := range as.GetApkDescription() {
335 if (apkDescriptionMatcher{apkdesc}).matches(targetConfig) {
336 result.entries = append(result.entries, apkdesc.GetPath())
337 // TODO(asmundak): As it turns out, moduleName which we get from
338 // the ModuleMetadata matches the module names of the generated
339 // entry paths just by coincidence, only for the split APKs. We
340 // need to discuss this with bundletool folks.
341 result.moduleName = as.GetModuleMetadata().GetName()
342 }
343 }
344 // we allow only a single module, so bail out here if we found one
345 if result.moduleName != "" {
346 return result
347 }
348 }
349 }
350 return result
351}
352
353type Zip2ZipWriter interface {
354 CopyFrom(file *zip.File, name string) error
355}
356
357// Writes out selected entries, renaming them as needed
358func (apkSet *ApkSet) writeApks(selected SelectionResult, config TargetConfig,
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700359 writer Zip2ZipWriter, partition string) ([]string, error) {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700360 // Renaming rules:
361 // splits/MODULE-master.apk to STEM.apk
362 // else
363 // splits/MODULE-*.apk to STEM>-$1.apk
364 // TODO(asmundak):
365 // add more rules, for .apex files
366 renameRules := []struct {
367 rex *regexp.Regexp
368 repl string
369 }{
370 {
371 regexp.MustCompile(`^.*/` + selected.moduleName + `-master\.apk$`),
372 config.stem + `.apk`,
373 },
374 {
375 regexp.MustCompile(`^.*/` + selected.moduleName + `(-.*\.apk)$`),
376 config.stem + `$1`,
377 },
Sasha Smundak827c55f2020-05-20 13:10:59 -0700378 {
379 regexp.MustCompile(`^universal\.apk$`),
380 config.stem + ".apk",
381 },
Sasha Smundak7a894a62020-05-06 21:23:08 -0700382 }
383 renamer := func(path string) (string, bool) {
384 for _, rr := range renameRules {
385 if rr.rex.MatchString(path) {
386 return rr.rex.ReplaceAllString(path, rr.repl), true
387 }
388 }
389 return "", false
390 }
391
392 entryOrigin := make(map[string]string) // output entry to input entry
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700393 var apkcerts []string
Sasha Smundak7a894a62020-05-06 21:23:08 -0700394 for _, apk := range selected.entries {
395 apkFile, ok := apkSet.entries[apk]
396 if !ok {
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700397 return nil, fmt.Errorf("TOC refers to an entry %s which does not exist", apk)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700398 }
399 inName := apkFile.Name
400 outName, ok := renamer(inName)
401 if !ok {
402 log.Fatalf("selected an entry with unexpected name %s", inName)
403 }
404 if origin, ok := entryOrigin[inName]; ok {
405 log.Fatalf("selected entries %s and %s will have the same output name %s",
406 origin, inName, outName)
407 }
408 entryOrigin[outName] = inName
409 if err := writer.CopyFrom(apkFile, outName); err != nil {
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700410 return nil, err
411 }
412 if partition != "" {
413 apkcerts = append(apkcerts, fmt.Sprintf(
414 `name="%s" certificate="PRESIGNED" private_key="" partition="%s"`, outName, partition))
Sasha Smundak7a894a62020-05-06 21:23:08 -0700415 }
416 }
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700417 sort.Strings(apkcerts)
418 return apkcerts, nil
Sasha Smundak7a894a62020-05-06 21:23:08 -0700419}
420
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700421func (apkSet *ApkSet) extractAndCopySingle(selected SelectionResult, outFile *os.File) error {
422 if len(selected.entries) != 1 {
423 return fmt.Errorf("Too many matching entries for extract-single:\n%v", selected.entries)
424 }
425 apk, ok := apkSet.entries[selected.entries[0]]
426 if !ok {
427 return fmt.Errorf("Couldn't find apk path %s", selected.entries[0])
428 }
429 inputReader, _ := apk.Open()
430 _, err := io.Copy(outFile, inputReader)
431 return err
432}
433
Sasha Smundak7a894a62020-05-06 21:23:08 -0700434// Arguments parsing
435var (
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700436 outputFile = flag.String("o", "", "output file containing extracted entries")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700437 targetConfig = TargetConfig{
438 screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{},
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700439 abis: map[android_bundle_proto.Abi_AbiAlias]int{},
Sasha Smundak7a894a62020-05-06 21:23:08 -0700440 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700441 extractSingle = flag.Bool("extract-single", false,
442 "extract a single target and output it uncompressed. only available for standalone apks and apexes.")
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700443 apkcertsOutput = flag.String("apkcerts", "",
444 "optional apkcerts.txt output file containing signing info of all outputted apks")
445 partition = flag.String("partition", "", "partition string. required when -apkcerts is used.")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700446)
447
448// Parse abi values
449type abiFlagValue struct {
450 targetConfig *TargetConfig
451}
452
453func (a abiFlagValue) String() string {
454 return "all"
455}
456
457func (a abiFlagValue) Set(abiList string) error {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700458 for i, abi := range strings.Split(abiList, ",") {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700459 v, ok := android_bundle_proto.Abi_AbiAlias_value[abi]
460 if !ok {
461 return fmt.Errorf("bad ABI value: %q", abi)
462 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700463 targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = i
Sasha Smundak7a894a62020-05-06 21:23:08 -0700464 }
465 return nil
466}
467
468// Parse screen density values
469type screenDensityFlagValue struct {
470 targetConfig *TargetConfig
471}
472
473func (s screenDensityFlagValue) String() string {
474 return "none"
475}
476
477func (s screenDensityFlagValue) Set(densityList string) error {
478 if densityList == "none" {
479 return nil
480 }
481 if densityList == "all" {
482 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true
483 return nil
484 }
485 for _, density := range strings.Split(densityList, ",") {
486 v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density]
487 if !found {
488 return fmt.Errorf("bad screen density value: %q", density)
489 }
490 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true
491 }
492 return nil
493}
494
495func processArgs() {
496 flag.Usage = func() {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700497 fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-file> -sdk-version value -abis value `+
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700498 `-screen-densities value {-stem value | -extract-single} [-allow-prereleased] `+
499 `[-apkcerts <apkcerts output file> -partition <partition>] <APK set>`)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700500 flag.PrintDefaults()
501 os.Exit(2)
502 }
503 version := flag.Uint("sdk-version", 0, "SDK version")
504 flag.Var(abiFlagValue{&targetConfig}, "abis",
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700505 "comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700506 flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities",
507 "'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)")
508 flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false,
509 "allow prereleased")
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700510 flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name in the output zip file")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700511 flag.Parse()
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700512 if (*outputFile == "") || len(flag.Args()) != 1 || *version == 0 ||
513 (targetConfig.stem == "" && !*extractSingle) || (*apkcertsOutput != "" && *partition == "") {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700514 flag.Usage()
515 }
516 targetConfig.sdkVersion = int32(*version)
517
518}
519
520func main() {
521 processArgs()
522 var toc Toc
523 apkSet, err := newApkSet(flag.Arg(0))
524 if err == nil {
525 defer apkSet.close()
526 toc, err = apkSet.getToc()
527 }
528 if err != nil {
529 log.Fatal(err)
530 }
531 sel := selectApks(toc, targetConfig)
532 if len(sel.entries) == 0 {
533 log.Fatalf("there are no entries for the target configuration: %#v", targetConfig)
534 }
535
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700536 outFile, err := os.Create(*outputFile)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700537 if err != nil {
538 log.Fatal(err)
539 }
540 defer outFile.Close()
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700541
542 if *extractSingle {
543 err = apkSet.extractAndCopySingle(sel, outFile)
544 } else {
545 writer := zip.NewWriter(outFile)
546 defer func() {
547 if err := writer.Close(); err != nil {
548 log.Fatal(err)
549 }
550 }()
Jaewoong Jung11c1e0f2020-06-29 19:18:44 -0700551 apkcerts, err := apkSet.writeApks(sel, targetConfig, writer, *partition)
552 if err == nil && *apkcertsOutput != "" {
553 apkcertsFile, err := os.Create(*apkcertsOutput)
554 if err != nil {
555 log.Fatal(err)
556 }
557 defer apkcertsFile.Close()
558 for _, a := range apkcerts {
559 _, err = apkcertsFile.WriteString(a + "\n")
560 if err != nil {
561 log.Fatal(err)
562 }
563 }
564 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700565 }
566 if err != nil {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700567 log.Fatal(err)
568 }
569}