blob: a638db2a8a9929bb04f5632b4a480181e7f63465 [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 },
377 }
378 renamer := func(path string) (string, bool) {
379 for _, rr := range renameRules {
380 if rr.rex.MatchString(path) {
381 return rr.rex.ReplaceAllString(path, rr.repl), true
382 }
383 }
384 return "", false
385 }
386
387 entryOrigin := make(map[string]string) // output entry to input entry
388 for _, apk := range selected.entries {
389 apkFile, ok := apkSet.entries[apk]
390 if !ok {
391 return fmt.Errorf("TOC refers to an entry %s which does not exist", apk)
392 }
393 inName := apkFile.Name
394 outName, ok := renamer(inName)
395 if !ok {
396 log.Fatalf("selected an entry with unexpected name %s", inName)
397 }
398 if origin, ok := entryOrigin[inName]; ok {
399 log.Fatalf("selected entries %s and %s will have the same output name %s",
400 origin, inName, outName)
401 }
402 entryOrigin[outName] = inName
403 if err := writer.CopyFrom(apkFile, outName); err != nil {
404 return err
405 }
406 }
407 return nil
408}
409
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700410func (apkSet *ApkSet) extractAndCopySingle(selected SelectionResult, outFile *os.File) error {
411 if len(selected.entries) != 1 {
412 return fmt.Errorf("Too many matching entries for extract-single:\n%v", selected.entries)
413 }
414 apk, ok := apkSet.entries[selected.entries[0]]
415 if !ok {
416 return fmt.Errorf("Couldn't find apk path %s", selected.entries[0])
417 }
418 inputReader, _ := apk.Open()
419 _, err := io.Copy(outFile, inputReader)
420 return err
421}
422
Sasha Smundak7a894a62020-05-06 21:23:08 -0700423// Arguments parsing
424var (
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700425 outputFile = flag.String("o", "", "output file containing extracted entries")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700426 targetConfig = TargetConfig{
427 screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{},
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700428 abis: map[android_bundle_proto.Abi_AbiAlias]int{},
Sasha Smundak7a894a62020-05-06 21:23:08 -0700429 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700430 extractSingle = flag.Bool("extract-single", false,
431 "extract a single target and output it uncompressed. only available for standalone apks and apexes.")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700432)
433
434// Parse abi values
435type abiFlagValue struct {
436 targetConfig *TargetConfig
437}
438
439func (a abiFlagValue) String() string {
440 return "all"
441}
442
443func (a abiFlagValue) Set(abiList string) error {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700444 for i, abi := range strings.Split(abiList, ",") {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700445 v, ok := android_bundle_proto.Abi_AbiAlias_value[abi]
446 if !ok {
447 return fmt.Errorf("bad ABI value: %q", abi)
448 }
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700449 targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = i
Sasha Smundak7a894a62020-05-06 21:23:08 -0700450 }
451 return nil
452}
453
454// Parse screen density values
455type screenDensityFlagValue struct {
456 targetConfig *TargetConfig
457}
458
459func (s screenDensityFlagValue) String() string {
460 return "none"
461}
462
463func (s screenDensityFlagValue) Set(densityList string) error {
464 if densityList == "none" {
465 return nil
466 }
467 if densityList == "all" {
468 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true
469 return nil
470 }
471 for _, density := range strings.Split(densityList, ",") {
472 v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density]
473 if !found {
474 return fmt.Errorf("bad screen density value: %q", density)
475 }
476 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true
477 }
478 return nil
479}
480
481func processArgs() {
482 flag.Usage = func() {
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700483 fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-file> -sdk-version value -abis value `+
484 `-screen-densities value {-stem value | -extract-single} [-allow-prereleased] <APK set>`)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700485 flag.PrintDefaults()
486 os.Exit(2)
487 }
488 version := flag.Uint("sdk-version", 0, "SDK version")
489 flag.Var(abiFlagValue{&targetConfig}, "abis",
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700490 "comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700491 flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities",
492 "'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)")
493 flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false,
494 "allow prereleased")
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700495 flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name in the output zip file")
Sasha Smundak7a894a62020-05-06 21:23:08 -0700496 flag.Parse()
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700497 if (*outputFile == "") || len(flag.Args()) != 1 || *version == 0 || (targetConfig.stem == "" && !*extractSingle) {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700498 flag.Usage()
499 }
500 targetConfig.sdkVersion = int32(*version)
501
502}
503
504func main() {
505 processArgs()
506 var toc Toc
507 apkSet, err := newApkSet(flag.Arg(0))
508 if err == nil {
509 defer apkSet.close()
510 toc, err = apkSet.getToc()
511 }
512 if err != nil {
513 log.Fatal(err)
514 }
515 sel := selectApks(toc, targetConfig)
516 if len(sel.entries) == 0 {
517 log.Fatalf("there are no entries for the target configuration: %#v", targetConfig)
518 }
519
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700520 outFile, err := os.Create(*outputFile)
Sasha Smundak7a894a62020-05-06 21:23:08 -0700521 if err != nil {
522 log.Fatal(err)
523 }
524 defer outFile.Close()
Jaewoong Jungfa00c062020-05-14 14:15:24 -0700525
526 if *extractSingle {
527 err = apkSet.extractAndCopySingle(sel, outFile)
528 } else {
529 writer := zip.NewWriter(outFile)
530 defer func() {
531 if err := writer.Close(); err != nil {
532 log.Fatal(err)
533 }
534 }()
535 err = apkSet.writeApks(sel, targetConfig, writer)
536 }
537 if err != nil {
Sasha Smundak7a894a62020-05-06 21:23:08 -0700538 log.Fatal(err)
539 }
540}