blob: 4a146daf60dd81092e6dfe9ce257aa0e000ab1ba [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"
24 "os"
25 "regexp"
26 "strings"
27
28 "github.com/golang/protobuf/proto"
29
30 "android/soong/cmd/extract_apks/bundle_proto"
31 "android/soong/third_party/zip"
32)
33
34type TargetConfig struct {
35 sdkVersion int32
36 screenDpi map[android_bundle_proto.ScreenDensity_DensityAlias]bool
37 abis map[android_bundle_proto.Abi_AbiAlias]bool
38 allowPrereleased bool
39 stem string
40}
41
42// An APK set is a zip archive. An entry 'toc.pb' describes its contents.
43// It is a protobuf message BuildApkResult.
44type Toc *android_bundle_proto.BuildApksResult
45
46type ApkSet struct {
47 path string
48 reader *zip.ReadCloser
49 entries map[string]*zip.File
50}
51
52func newApkSet(path string) (*ApkSet, error) {
53 apkSet := &ApkSet{path: path, entries: make(map[string]*zip.File)}
54 var err error
55 if apkSet.reader, err = zip.OpenReader(apkSet.path); err != nil {
56 return nil, err
57 }
58 for _, f := range apkSet.reader.File {
59 apkSet.entries[f.Name] = f
60 }
61 return apkSet, nil
62}
63
64func (apkSet *ApkSet) getToc() (Toc, error) {
65 var err error
66 tocFile, ok := apkSet.entries["toc.pb"]
67 if !ok {
68 return nil, fmt.Errorf("%s: APK set should have toc.pb entry", apkSet.path)
69 }
70 rc, err := tocFile.Open()
71 if err != nil {
72 return nil, err
73 }
74 bytes := make([]byte, tocFile.FileHeader.UncompressedSize64)
75 if _, err := rc.Read(bytes); err != io.EOF {
76 return nil, err
77 }
78 rc.Close()
79 buildApksResult := new(android_bundle_proto.BuildApksResult)
80 if err = proto.Unmarshal(bytes, buildApksResult); err != nil {
81 return nil, err
82 }
83 return buildApksResult, nil
84}
85
86func (apkSet *ApkSet) close() {
87 apkSet.reader.Close()
88}
89
90// Matchers for selection criteria
91type abiTargetingMatcher struct {
92 *android_bundle_proto.AbiTargeting
93}
94
95func (m abiTargetingMatcher) matches(config TargetConfig) bool {
96 if m.AbiTargeting == nil {
97 return true
98 }
99 if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
100 return true
101 }
102 for _, v := range m.GetValue() {
103 if _, ok := config.abis[v.Alias]; ok {
104 return true
105 }
106 }
107 return false
108}
109
110type apkDescriptionMatcher struct {
111 *android_bundle_proto.ApkDescription
112}
113
114func (m apkDescriptionMatcher) matches(config TargetConfig) bool {
115 return m.ApkDescription == nil || (apkTargetingMatcher{m.Targeting}).matches(config)
116}
117
118type apkTargetingMatcher struct {
119 *android_bundle_proto.ApkTargeting
120}
121
122func (m apkTargetingMatcher) matches(config TargetConfig) bool {
123 return m.ApkTargeting == nil ||
124 (abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
125 languageTargetingMatcher{m.LanguageTargeting}.matches(config) &&
126 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
127 sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
128 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config))
129}
130
131type languageTargetingMatcher struct {
132 *android_bundle_proto.LanguageTargeting
133}
134
135func (m languageTargetingMatcher) matches(_ TargetConfig) bool {
136 if m.LanguageTargeting == nil {
137 return true
138 }
139 log.Fatal("language based entry selection is not implemented")
140 return false
141}
142
143type moduleMetadataMatcher struct {
144 *android_bundle_proto.ModuleMetadata
145}
146
147func (m moduleMetadataMatcher) matches(config TargetConfig) bool {
148 return m.ModuleMetadata == nil ||
149 (m.GetDeliveryType() == android_bundle_proto.DeliveryType_INSTALL_TIME &&
150 moduleTargetingMatcher{m.Targeting}.matches(config) &&
151 !m.IsInstant)
152}
153
154type moduleTargetingMatcher struct {
155 *android_bundle_proto.ModuleTargeting
156}
157
158func (m moduleTargetingMatcher) matches(config TargetConfig) bool {
159 return m.ModuleTargeting == nil ||
160 (sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
161 userCountriesTargetingMatcher{m.UserCountriesTargeting}.matches(config))
162}
163
164type multiAbiTargetingMatcher struct {
165 *android_bundle_proto.MultiAbiTargeting
166}
167
168func (t multiAbiTargetingMatcher) matches(_ TargetConfig) bool {
169 if t.MultiAbiTargeting == nil {
170 return true
171 }
172 log.Fatal("multiABI based selection is not implemented")
173 return false
174}
175
176type screenDensityTargetingMatcher struct {
177 *android_bundle_proto.ScreenDensityTargeting
178}
179
180func (m screenDensityTargetingMatcher) matches(config TargetConfig) bool {
181 if m.ScreenDensityTargeting == nil {
182 return true
183 }
184 if _, ok := config.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED]; ok {
185 return true
186 }
187 for _, v := range m.GetValue() {
188 switch x := v.GetDensityOneof().(type) {
189 case *android_bundle_proto.ScreenDensity_DensityAlias_:
190 if _, ok := config.screenDpi[x.DensityAlias]; ok {
191 return true
192 }
193 default:
194 log.Fatal("For screen density, only DPI name based entry selection (e.g. HDPI, XHDPI) is implemented")
195 }
196 }
197 return false
198}
199
200type sdkVersionTargetingMatcher struct {
201 *android_bundle_proto.SdkVersionTargeting
202}
203
204func (m sdkVersionTargetingMatcher) matches(config TargetConfig) bool {
205 const preReleaseVersion = 10000
206 if m.SdkVersionTargeting == nil {
207 return true
208 }
209 if len(m.Value) > 1 {
210 log.Fatal(fmt.Sprintf("sdk_version_targeting should not have multiple values:%#v", m.Value))
211 }
212 // Inspect only sdkVersionTargeting.Value.
213 // Even though one of the SdkVersionTargeting.Alternatives values may be
214 // better matching, we will select all of them
215 return m.Value[0].Min == nil ||
216 m.Value[0].Min.Value <= config.sdkVersion ||
217 (config.allowPrereleased && m.Value[0].Min.Value == preReleaseVersion)
218}
219
220type textureCompressionFormatTargetingMatcher struct {
221 *android_bundle_proto.TextureCompressionFormatTargeting
222}
223
224func (m textureCompressionFormatTargetingMatcher) matches(_ TargetConfig) bool {
225 if m.TextureCompressionFormatTargeting == nil {
226 return true
227 }
228 log.Fatal("texture based entry selection is not implemented")
229 return false
230}
231
232type userCountriesTargetingMatcher struct {
233 *android_bundle_proto.UserCountriesTargeting
234}
235
236func (m userCountriesTargetingMatcher) matches(_ TargetConfig) bool {
237 if m.UserCountriesTargeting == nil {
238 return true
239 }
240 log.Fatal("country based entry selection is not implemented")
241 return false
242}
243
244type variantTargetingMatcher struct {
245 *android_bundle_proto.VariantTargeting
246}
247
248func (m variantTargetingMatcher) matches(config TargetConfig) bool {
249 if m.VariantTargeting == nil {
250 return true
251 }
252 return sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
253 abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
254 multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config) &&
255 screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
256 textureCompressionFormatTargetingMatcher{m.TextureCompressionFormatTargeting}.matches(config)
257}
258
259type SelectionResult struct {
260 moduleName string
261 entries []string
262}
263
264// Return all entries matching target configuration
265func selectApks(toc Toc, targetConfig TargetConfig) SelectionResult {
266 var result SelectionResult
267 for _, variant := range (*toc).GetVariant() {
268 if !(variantTargetingMatcher{variant.GetTargeting()}.matches(targetConfig)) {
269 continue
270 }
271 for _, as := range variant.GetApkSet() {
272 if !(moduleMetadataMatcher{as.ModuleMetadata}.matches(targetConfig)) {
273 continue
274 }
275 for _, apkdesc := range as.GetApkDescription() {
276 if (apkDescriptionMatcher{apkdesc}).matches(targetConfig) {
277 result.entries = append(result.entries, apkdesc.GetPath())
278 // TODO(asmundak): As it turns out, moduleName which we get from
279 // the ModuleMetadata matches the module names of the generated
280 // entry paths just by coincidence, only for the split APKs. We
281 // need to discuss this with bundletool folks.
282 result.moduleName = as.GetModuleMetadata().GetName()
283 }
284 }
285 // we allow only a single module, so bail out here if we found one
286 if result.moduleName != "" {
287 return result
288 }
289 }
290 }
291 return result
292}
293
294type Zip2ZipWriter interface {
295 CopyFrom(file *zip.File, name string) error
296}
297
298// Writes out selected entries, renaming them as needed
299func (apkSet *ApkSet) writeApks(selected SelectionResult, config TargetConfig,
300 writer Zip2ZipWriter) error {
301 // Renaming rules:
302 // splits/MODULE-master.apk to STEM.apk
303 // else
304 // splits/MODULE-*.apk to STEM>-$1.apk
305 // TODO(asmundak):
306 // add more rules, for .apex files
307 renameRules := []struct {
308 rex *regexp.Regexp
309 repl string
310 }{
311 {
312 regexp.MustCompile(`^.*/` + selected.moduleName + `-master\.apk$`),
313 config.stem + `.apk`,
314 },
315 {
316 regexp.MustCompile(`^.*/` + selected.moduleName + `(-.*\.apk)$`),
317 config.stem + `$1`,
318 },
319 }
320 renamer := func(path string) (string, bool) {
321 for _, rr := range renameRules {
322 if rr.rex.MatchString(path) {
323 return rr.rex.ReplaceAllString(path, rr.repl), true
324 }
325 }
326 return "", false
327 }
328
329 entryOrigin := make(map[string]string) // output entry to input entry
330 for _, apk := range selected.entries {
331 apkFile, ok := apkSet.entries[apk]
332 if !ok {
333 return fmt.Errorf("TOC refers to an entry %s which does not exist", apk)
334 }
335 inName := apkFile.Name
336 outName, ok := renamer(inName)
337 if !ok {
338 log.Fatalf("selected an entry with unexpected name %s", inName)
339 }
340 if origin, ok := entryOrigin[inName]; ok {
341 log.Fatalf("selected entries %s and %s will have the same output name %s",
342 origin, inName, outName)
343 }
344 entryOrigin[outName] = inName
345 if err := writer.CopyFrom(apkFile, outName); err != nil {
346 return err
347 }
348 }
349 return nil
350}
351
352// Arguments parsing
353var (
354 outputZip = flag.String("o", "", "output zip containing extracted entries")
355 targetConfig = TargetConfig{
356 screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{},
357 abis: map[android_bundle_proto.Abi_AbiAlias]bool{},
358 }
359)
360
361// Parse abi values
362type abiFlagValue struct {
363 targetConfig *TargetConfig
364}
365
366func (a abiFlagValue) String() string {
367 return "all"
368}
369
370func (a abiFlagValue) Set(abiList string) error {
371 if abiList == "none" {
372 return nil
373 }
374 if abiList == "all" {
375 targetConfig.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE] = true
376 return nil
377 }
378 for _, abi := range strings.Split(abiList, ",") {
379 v, ok := android_bundle_proto.Abi_AbiAlias_value[abi]
380 if !ok {
381 return fmt.Errorf("bad ABI value: %q", abi)
382 }
383 targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = true
384 }
385 return nil
386}
387
388// Parse screen density values
389type screenDensityFlagValue struct {
390 targetConfig *TargetConfig
391}
392
393func (s screenDensityFlagValue) String() string {
394 return "none"
395}
396
397func (s screenDensityFlagValue) Set(densityList string) error {
398 if densityList == "none" {
399 return nil
400 }
401 if densityList == "all" {
402 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true
403 return nil
404 }
405 for _, density := range strings.Split(densityList, ",") {
406 v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density]
407 if !found {
408 return fmt.Errorf("bad screen density value: %q", density)
409 }
410 targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true
411 }
412 return nil
413}
414
415func processArgs() {
416 flag.Usage = func() {
417 fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-zip> -sdk-version value -abis value -screen-densities value <APK set>`)
418 flag.PrintDefaults()
419 os.Exit(2)
420 }
421 version := flag.Uint("sdk-version", 0, "SDK version")
422 flag.Var(abiFlagValue{&targetConfig}, "abis",
423 "'all' or comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64")
424 flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities",
425 "'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)")
426 flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false,
427 "allow prereleased")
428 flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name")
429 flag.Parse()
430 if (*outputZip == "") || len(flag.Args()) != 1 || *version == 0 || targetConfig.stem == "" {
431 flag.Usage()
432 }
433 targetConfig.sdkVersion = int32(*version)
434
435}
436
437func main() {
438 processArgs()
439 var toc Toc
440 apkSet, err := newApkSet(flag.Arg(0))
441 if err == nil {
442 defer apkSet.close()
443 toc, err = apkSet.getToc()
444 }
445 if err != nil {
446 log.Fatal(err)
447 }
448 sel := selectApks(toc, targetConfig)
449 if len(sel.entries) == 0 {
450 log.Fatalf("there are no entries for the target configuration: %#v", targetConfig)
451 }
452
453 outFile, err := os.Create(*outputZip)
454 if err != nil {
455 log.Fatal(err)
456 }
457 defer outFile.Close()
458 writer := zip.NewWriter(outFile)
459 defer func() {
460 if err := writer.Close(); err != nil {
461 log.Fatal(err)
462 }
463 }()
464 if err = apkSet.writeApks(sel, targetConfig, writer); err != nil {
465 log.Fatal(err)
466 }
467}