Implement extract_apks
Bug: 152319766 Test: manual and builtin Change-Id: Ia15d66e86c7bcfd52f5b776173ca1665b68ff438
This commit is contained in:
467
cmd/extract_apks/main.go
Normal file
467
cmd/extract_apks/main.go
Normal file
@@ -0,0 +1,467 @@
|
||||
// Copyright 2020 Google Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Copies all the entries (APKs/APEXes) matching the target configuration from the given
|
||||
// APK set into a zip file. Run it without arguments to see usage details.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
|
||||
"android/soong/cmd/extract_apks/bundle_proto"
|
||||
"android/soong/third_party/zip"
|
||||
)
|
||||
|
||||
type TargetConfig struct {
|
||||
sdkVersion int32
|
||||
screenDpi map[android_bundle_proto.ScreenDensity_DensityAlias]bool
|
||||
abis map[android_bundle_proto.Abi_AbiAlias]bool
|
||||
allowPrereleased bool
|
||||
stem string
|
||||
}
|
||||
|
||||
// An APK set is a zip archive. An entry 'toc.pb' describes its contents.
|
||||
// It is a protobuf message BuildApkResult.
|
||||
type Toc *android_bundle_proto.BuildApksResult
|
||||
|
||||
type ApkSet struct {
|
||||
path string
|
||||
reader *zip.ReadCloser
|
||||
entries map[string]*zip.File
|
||||
}
|
||||
|
||||
func newApkSet(path string) (*ApkSet, error) {
|
||||
apkSet := &ApkSet{path: path, entries: make(map[string]*zip.File)}
|
||||
var err error
|
||||
if apkSet.reader, err = zip.OpenReader(apkSet.path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range apkSet.reader.File {
|
||||
apkSet.entries[f.Name] = f
|
||||
}
|
||||
return apkSet, nil
|
||||
}
|
||||
|
||||
func (apkSet *ApkSet) getToc() (Toc, error) {
|
||||
var err error
|
||||
tocFile, ok := apkSet.entries["toc.pb"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: APK set should have toc.pb entry", apkSet.path)
|
||||
}
|
||||
rc, err := tocFile.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bytes := make([]byte, tocFile.FileHeader.UncompressedSize64)
|
||||
if _, err := rc.Read(bytes); err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
rc.Close()
|
||||
buildApksResult := new(android_bundle_proto.BuildApksResult)
|
||||
if err = proto.Unmarshal(bytes, buildApksResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildApksResult, nil
|
||||
}
|
||||
|
||||
func (apkSet *ApkSet) close() {
|
||||
apkSet.reader.Close()
|
||||
}
|
||||
|
||||
// Matchers for selection criteria
|
||||
type abiTargetingMatcher struct {
|
||||
*android_bundle_proto.AbiTargeting
|
||||
}
|
||||
|
||||
func (m abiTargetingMatcher) matches(config TargetConfig) bool {
|
||||
if m.AbiTargeting == nil {
|
||||
return true
|
||||
}
|
||||
if _, ok := config.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE]; ok {
|
||||
return true
|
||||
}
|
||||
for _, v := range m.GetValue() {
|
||||
if _, ok := config.abis[v.Alias]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type apkDescriptionMatcher struct {
|
||||
*android_bundle_proto.ApkDescription
|
||||
}
|
||||
|
||||
func (m apkDescriptionMatcher) matches(config TargetConfig) bool {
|
||||
return m.ApkDescription == nil || (apkTargetingMatcher{m.Targeting}).matches(config)
|
||||
}
|
||||
|
||||
type apkTargetingMatcher struct {
|
||||
*android_bundle_proto.ApkTargeting
|
||||
}
|
||||
|
||||
func (m apkTargetingMatcher) matches(config TargetConfig) bool {
|
||||
return m.ApkTargeting == nil ||
|
||||
(abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
|
||||
languageTargetingMatcher{m.LanguageTargeting}.matches(config) &&
|
||||
screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
|
||||
sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
|
||||
multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config))
|
||||
}
|
||||
|
||||
type languageTargetingMatcher struct {
|
||||
*android_bundle_proto.LanguageTargeting
|
||||
}
|
||||
|
||||
func (m languageTargetingMatcher) matches(_ TargetConfig) bool {
|
||||
if m.LanguageTargeting == nil {
|
||||
return true
|
||||
}
|
||||
log.Fatal("language based entry selection is not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
type moduleMetadataMatcher struct {
|
||||
*android_bundle_proto.ModuleMetadata
|
||||
}
|
||||
|
||||
func (m moduleMetadataMatcher) matches(config TargetConfig) bool {
|
||||
return m.ModuleMetadata == nil ||
|
||||
(m.GetDeliveryType() == android_bundle_proto.DeliveryType_INSTALL_TIME &&
|
||||
moduleTargetingMatcher{m.Targeting}.matches(config) &&
|
||||
!m.IsInstant)
|
||||
}
|
||||
|
||||
type moduleTargetingMatcher struct {
|
||||
*android_bundle_proto.ModuleTargeting
|
||||
}
|
||||
|
||||
func (m moduleTargetingMatcher) matches(config TargetConfig) bool {
|
||||
return m.ModuleTargeting == nil ||
|
||||
(sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
|
||||
userCountriesTargetingMatcher{m.UserCountriesTargeting}.matches(config))
|
||||
}
|
||||
|
||||
type multiAbiTargetingMatcher struct {
|
||||
*android_bundle_proto.MultiAbiTargeting
|
||||
}
|
||||
|
||||
func (t multiAbiTargetingMatcher) matches(_ TargetConfig) bool {
|
||||
if t.MultiAbiTargeting == nil {
|
||||
return true
|
||||
}
|
||||
log.Fatal("multiABI based selection is not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
type screenDensityTargetingMatcher struct {
|
||||
*android_bundle_proto.ScreenDensityTargeting
|
||||
}
|
||||
|
||||
func (m screenDensityTargetingMatcher) matches(config TargetConfig) bool {
|
||||
if m.ScreenDensityTargeting == nil {
|
||||
return true
|
||||
}
|
||||
if _, ok := config.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED]; ok {
|
||||
return true
|
||||
}
|
||||
for _, v := range m.GetValue() {
|
||||
switch x := v.GetDensityOneof().(type) {
|
||||
case *android_bundle_proto.ScreenDensity_DensityAlias_:
|
||||
if _, ok := config.screenDpi[x.DensityAlias]; ok {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
log.Fatal("For screen density, only DPI name based entry selection (e.g. HDPI, XHDPI) is implemented")
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type sdkVersionTargetingMatcher struct {
|
||||
*android_bundle_proto.SdkVersionTargeting
|
||||
}
|
||||
|
||||
func (m sdkVersionTargetingMatcher) matches(config TargetConfig) bool {
|
||||
const preReleaseVersion = 10000
|
||||
if m.SdkVersionTargeting == nil {
|
||||
return true
|
||||
}
|
||||
if len(m.Value) > 1 {
|
||||
log.Fatal(fmt.Sprintf("sdk_version_targeting should not have multiple values:%#v", m.Value))
|
||||
}
|
||||
// Inspect only sdkVersionTargeting.Value.
|
||||
// Even though one of the SdkVersionTargeting.Alternatives values may be
|
||||
// better matching, we will select all of them
|
||||
return m.Value[0].Min == nil ||
|
||||
m.Value[0].Min.Value <= config.sdkVersion ||
|
||||
(config.allowPrereleased && m.Value[0].Min.Value == preReleaseVersion)
|
||||
}
|
||||
|
||||
type textureCompressionFormatTargetingMatcher struct {
|
||||
*android_bundle_proto.TextureCompressionFormatTargeting
|
||||
}
|
||||
|
||||
func (m textureCompressionFormatTargetingMatcher) matches(_ TargetConfig) bool {
|
||||
if m.TextureCompressionFormatTargeting == nil {
|
||||
return true
|
||||
}
|
||||
log.Fatal("texture based entry selection is not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
type userCountriesTargetingMatcher struct {
|
||||
*android_bundle_proto.UserCountriesTargeting
|
||||
}
|
||||
|
||||
func (m userCountriesTargetingMatcher) matches(_ TargetConfig) bool {
|
||||
if m.UserCountriesTargeting == nil {
|
||||
return true
|
||||
}
|
||||
log.Fatal("country based entry selection is not implemented")
|
||||
return false
|
||||
}
|
||||
|
||||
type variantTargetingMatcher struct {
|
||||
*android_bundle_proto.VariantTargeting
|
||||
}
|
||||
|
||||
func (m variantTargetingMatcher) matches(config TargetConfig) bool {
|
||||
if m.VariantTargeting == nil {
|
||||
return true
|
||||
}
|
||||
return sdkVersionTargetingMatcher{m.SdkVersionTargeting}.matches(config) &&
|
||||
abiTargetingMatcher{m.AbiTargeting}.matches(config) &&
|
||||
multiAbiTargetingMatcher{m.MultiAbiTargeting}.matches(config) &&
|
||||
screenDensityTargetingMatcher{m.ScreenDensityTargeting}.matches(config) &&
|
||||
textureCompressionFormatTargetingMatcher{m.TextureCompressionFormatTargeting}.matches(config)
|
||||
}
|
||||
|
||||
type SelectionResult struct {
|
||||
moduleName string
|
||||
entries []string
|
||||
}
|
||||
|
||||
// Return all entries matching target configuration
|
||||
func selectApks(toc Toc, targetConfig TargetConfig) SelectionResult {
|
||||
var result SelectionResult
|
||||
for _, variant := range (*toc).GetVariant() {
|
||||
if !(variantTargetingMatcher{variant.GetTargeting()}.matches(targetConfig)) {
|
||||
continue
|
||||
}
|
||||
for _, as := range variant.GetApkSet() {
|
||||
if !(moduleMetadataMatcher{as.ModuleMetadata}.matches(targetConfig)) {
|
||||
continue
|
||||
}
|
||||
for _, apkdesc := range as.GetApkDescription() {
|
||||
if (apkDescriptionMatcher{apkdesc}).matches(targetConfig) {
|
||||
result.entries = append(result.entries, apkdesc.GetPath())
|
||||
// TODO(asmundak): As it turns out, moduleName which we get from
|
||||
// the ModuleMetadata matches the module names of the generated
|
||||
// entry paths just by coincidence, only for the split APKs. We
|
||||
// need to discuss this with bundletool folks.
|
||||
result.moduleName = as.GetModuleMetadata().GetName()
|
||||
}
|
||||
}
|
||||
// we allow only a single module, so bail out here if we found one
|
||||
if result.moduleName != "" {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type Zip2ZipWriter interface {
|
||||
CopyFrom(file *zip.File, name string) error
|
||||
}
|
||||
|
||||
// Writes out selected entries, renaming them as needed
|
||||
func (apkSet *ApkSet) writeApks(selected SelectionResult, config TargetConfig,
|
||||
writer Zip2ZipWriter) error {
|
||||
// Renaming rules:
|
||||
// splits/MODULE-master.apk to STEM.apk
|
||||
// else
|
||||
// splits/MODULE-*.apk to STEM>-$1.apk
|
||||
// TODO(asmundak):
|
||||
// add more rules, for .apex files
|
||||
renameRules := []struct {
|
||||
rex *regexp.Regexp
|
||||
repl string
|
||||
}{
|
||||
{
|
||||
regexp.MustCompile(`^.*/` + selected.moduleName + `-master\.apk$`),
|
||||
config.stem + `.apk`,
|
||||
},
|
||||
{
|
||||
regexp.MustCompile(`^.*/` + selected.moduleName + `(-.*\.apk)$`),
|
||||
config.stem + `$1`,
|
||||
},
|
||||
}
|
||||
renamer := func(path string) (string, bool) {
|
||||
for _, rr := range renameRules {
|
||||
if rr.rex.MatchString(path) {
|
||||
return rr.rex.ReplaceAllString(path, rr.repl), true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
entryOrigin := make(map[string]string) // output entry to input entry
|
||||
for _, apk := range selected.entries {
|
||||
apkFile, ok := apkSet.entries[apk]
|
||||
if !ok {
|
||||
return fmt.Errorf("TOC refers to an entry %s which does not exist", apk)
|
||||
}
|
||||
inName := apkFile.Name
|
||||
outName, ok := renamer(inName)
|
||||
if !ok {
|
||||
log.Fatalf("selected an entry with unexpected name %s", inName)
|
||||
}
|
||||
if origin, ok := entryOrigin[inName]; ok {
|
||||
log.Fatalf("selected entries %s and %s will have the same output name %s",
|
||||
origin, inName, outName)
|
||||
}
|
||||
entryOrigin[outName] = inName
|
||||
if err := writer.CopyFrom(apkFile, outName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Arguments parsing
|
||||
var (
|
||||
outputZip = flag.String("o", "", "output zip containing extracted entries")
|
||||
targetConfig = TargetConfig{
|
||||
screenDpi: map[android_bundle_proto.ScreenDensity_DensityAlias]bool{},
|
||||
abis: map[android_bundle_proto.Abi_AbiAlias]bool{},
|
||||
}
|
||||
)
|
||||
|
||||
// Parse abi values
|
||||
type abiFlagValue struct {
|
||||
targetConfig *TargetConfig
|
||||
}
|
||||
|
||||
func (a abiFlagValue) String() string {
|
||||
return "all"
|
||||
}
|
||||
|
||||
func (a abiFlagValue) Set(abiList string) error {
|
||||
if abiList == "none" {
|
||||
return nil
|
||||
}
|
||||
if abiList == "all" {
|
||||
targetConfig.abis[android_bundle_proto.Abi_UNSPECIFIED_CPU_ARCHITECTURE] = true
|
||||
return nil
|
||||
}
|
||||
for _, abi := range strings.Split(abiList, ",") {
|
||||
v, ok := android_bundle_proto.Abi_AbiAlias_value[abi]
|
||||
if !ok {
|
||||
return fmt.Errorf("bad ABI value: %q", abi)
|
||||
}
|
||||
targetConfig.abis[android_bundle_proto.Abi_AbiAlias(v)] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse screen density values
|
||||
type screenDensityFlagValue struct {
|
||||
targetConfig *TargetConfig
|
||||
}
|
||||
|
||||
func (s screenDensityFlagValue) String() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
func (s screenDensityFlagValue) Set(densityList string) error {
|
||||
if densityList == "none" {
|
||||
return nil
|
||||
}
|
||||
if densityList == "all" {
|
||||
targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DENSITY_UNSPECIFIED] = true
|
||||
return nil
|
||||
}
|
||||
for _, density := range strings.Split(densityList, ",") {
|
||||
v, found := android_bundle_proto.ScreenDensity_DensityAlias_value[density]
|
||||
if !found {
|
||||
return fmt.Errorf("bad screen density value: %q", density)
|
||||
}
|
||||
targetConfig.screenDpi[android_bundle_proto.ScreenDensity_DensityAlias(v)] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func processArgs() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintln(os.Stderr, `usage: extract_apks -o <output-zip> -sdk-version value -abis value -screen-densities value <APK set>`)
|
||||
flag.PrintDefaults()
|
||||
os.Exit(2)
|
||||
}
|
||||
version := flag.Uint("sdk-version", 0, "SDK version")
|
||||
flag.Var(abiFlagValue{&targetConfig}, "abis",
|
||||
"'all' or comma-separated ABIs list of ARMEABI ARMEABI_V7A ARM64_V8A X86 X86_64 MIPS MIPS64")
|
||||
flag.Var(screenDensityFlagValue{&targetConfig}, "screen-densities",
|
||||
"'all' or comma-separated list of screen density names (NODPI LDPI MDPI TVDPI HDPI XHDPI XXHDPI XXXHDPI)")
|
||||
flag.BoolVar(&targetConfig.allowPrereleased, "allow-prereleased", false,
|
||||
"allow prereleased")
|
||||
flag.StringVar(&targetConfig.stem, "stem", "", "output entries base name")
|
||||
flag.Parse()
|
||||
if (*outputZip == "") || len(flag.Args()) != 1 || *version == 0 || targetConfig.stem == "" {
|
||||
flag.Usage()
|
||||
}
|
||||
targetConfig.sdkVersion = int32(*version)
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
processArgs()
|
||||
var toc Toc
|
||||
apkSet, err := newApkSet(flag.Arg(0))
|
||||
if err == nil {
|
||||
defer apkSet.close()
|
||||
toc, err = apkSet.getToc()
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
sel := selectApks(toc, targetConfig)
|
||||
if len(sel.entries) == 0 {
|
||||
log.Fatalf("there are no entries for the target configuration: %#v", targetConfig)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(*outputZip)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
writer := zip.NewWriter(outFile)
|
||||
defer func() {
|
||||
if err := writer.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
if err = apkSet.writeApks(sel, targetConfig, writer); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user