Initial commit

This commit is contained in:
2026-02-02 04:50:13 +01:00
commit 5b11698731
22592 changed files with 7677434 additions and 0 deletions

View File

@@ -0,0 +1,529 @@
#!/usr/bin/env python3
#
# Copyright 2021 Google LLC
#
# 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.
"""Script to add Asset Packs to an already built Android App Bundle."""
import argparse
import fnmatch
import os
import platform
import shutil
import subprocess
import sys
import tempfile
import typing
import xml.dom.minidom
import zipfile
import config_pb2
import google.protobuf.json_format as json_format
FASTFOLLOW = "dist:fast-follow"
ONDEMAND = "dist:on-demand"
UPFRONT = "dist:install-time"
def parse_args() -> argparse.Namespace:
"""Parse input arguments."""
parser = argparse.ArgumentParser(
description="Augments an Android App Bundle with given asset packs.",
add_help=True)
parser.add_argument(
"--androidsdk", required=True, help="Android SDK location")
parser.add_argument("--sdkver", required=True, help="Android SDK version")
parser.add_argument(
"--buildtoolsver", required=True, help="Android Build Tools version")
parser.add_argument(
"--bundletool", required=True, help="Path to Bundletool jar file")
parser.add_argument(
"--inputbundle", required=True, help="App Bundle to augment")
parser.add_argument(
"--packdir", required=True, help="Folder to read assets packs from")
parser.add_argument(
"--packnames",
required=True,
help="Comma separated list of asset pack files")
parser.add_argument("--output", required=True, help="Output App Bundle")
parser.add_argument(
"--overwrite",
required=False,
action="store_true",
help="Overwrite existing files")
parser.add_argument(
"--striptcfsuffixes",
required=False,
action="store_true",
help="Enable removal of #tcf_xxx suffixes in asset pack folder names")
parser.add_argument(
"--compressinstalltimeassets",
required=False,
action="store_true",
help=("Compress assets within install time asset packs."
"This will not apply to on demand or fast follow asset packs"
"Setting is overridden for files matched in the uncompressed glob"))
return parser.parse_args()
def abs_expand_all(path: str) -> str:
return os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
def get_aapt2_bin_path(args: argparse.Namespace) -> str:
"""Retrieve the path for the aapt2 binary."""
android_sdk_path = abs_expand_all(args.androidsdk)
build_tools_version = args.buildtoolsver
aapt2_bin_path: str
if platform.system() == "Windows":
aapt2_bin_path = os.path.join(android_sdk_path, "build-tools",
build_tools_version, "aapt2.exe")
else:
aapt2_bin_path = os.path.join(android_sdk_path, "build-tools",
build_tools_version, "aapt2")
if not os.path.exists(aapt2_bin_path):
print(
"Cannot find AAPT2 at {aapt2_bin_path}".format(
aapt2_bin_path=aapt2_bin_path),
file=sys.stderr)
sys.exit(-1)
return aapt2_bin_path
def get_sdk_jar_path(args: argparse.Namespace) -> str:
"""Retrieve the path for the android SDK JAR file."""
android_sdk_path = abs_expand_all(args.androidsdk)
sdk_ver = args.sdkver
sdk_jar_path = os.path.join(android_sdk_path, "platforms",
"android-" + sdk_ver, "android.jar")
if not os.path.exists(sdk_jar_path):
print(
"Cannot find android.jar at {sdk_jar_path}".format(
sdk_jar_path=sdk_jar_path),
file=sys.stderr)
sys.exit(-1)
return sdk_jar_path
def get_bundletool_path(args: argparse.Namespace) -> str:
"""Retrieve the path for the BundleTool JAR file."""
bundletool_path = abs_expand_all(args.bundletool)
if not os.path.exists(bundletool_path):
print(
"Cannot find Bundletool at {bundletool_path}".format(
bundletool_path=bundletool_path),
file=sys.stderr)
sys.exit(-1)
return bundletool_path
def get_packs(args: argparse.Namespace) -> (str, typing.List[str]):
"""Retrieve the pack directory and pack names from the input flags."""
pack_dir = abs_expand_all(args.packdir)
if not os.path.exists(pack_dir):
print(
"Cannot find asset pack directory at {pack_dir}".format(
pack_dir=pack_dir),
file=sys.stderr)
sys.exit(-1)
pack_names = args.packnames.split(",")
for pack_name in pack_names:
pack_path = os.path.join(pack_dir, pack_name)
if not os.path.exists(pack_path):
print(
"Cannot find asset pack {pack_name} at {pack_path}".format(
pack_name=pack_name, pack_path=pack_path),
file=sys.stderr)
sys.exit(-1)
return (pack_dir, pack_names)
def get_input_bundle_path(args: argparse.Namespace) -> str:
bundle_path = abs_expand_all(args.inputbundle)
if not os.path.exists(bundle_path):
print(
"Cannot find input app bundle_path {bundle_path}".format(
bundle_path=bundle_path),
file=sys.stderr)
sys.exit(-1)
return bundle_path
def get_output_path(args: argparse.Namespace) -> str:
"""Retrieve the output file name."""
output_path = abs_expand_all(args.output)
if os.path.exists(output_path):
if os.path.isdir(output_path):
print(
"Output location {output_path} is a directory. Specify a file path instead."
.format(output_path=output_path))
sys.exit(-1)
if not args.overwrite:
print(
"Output file {output_path} exists. Specify --overwrite to bypass. Exiting."
.format(output_path=output_path))
sys.exit(-1)
else:
os.remove(output_path)
return output_path
def purge_files(directory: str, pattern: str) -> None:
"""Purge the files inside a directory that match the given pattern."""
for root_dir, _, filenames in os.walk(directory):
for filename in fnmatch.filter(filenames, pattern):
try:
os.remove(os.path.join(root_dir, filename))
except OSError as e:
print(
"Error while deleting {filename}: {e}".format(
filename=filename, e=e),
file=sys.stderr)
def purge_subdirs(directory: str, pattern: str) -> None:
"""Purge the subdirectories inside a directory that match the given pattern."""
for root_dir, subdirs, _ in os.walk(directory):
for filename in fnmatch.filter(subdirs, pattern):
try:
shutil.rmtree(os.path.join(root_dir, filename))
except FileNotFoundError as e:
print(
"Error while deleting {filename}: {e}".format(
filename=filename, e=e),
file=sys.stderr)
def parse_bundle_metadata(bundle_folder: str) -> typing.List[str]:
"""Parse the Bundle Metadata from the given bundle directory."""
metadata = []
metadata_folder = os.path.join(bundle_folder, "BUNDLE-METADATA")
if not os.path.isdir(metadata_folder):
return
for folder in os.listdir(metadata_folder):
inner_directory = os.path.join(metadata_folder, folder)
if not os.path.isdir(inner_directory):
continue
for file in os.listdir(inner_directory):
entry = "{path_in_bundle}:{physical_file_path}".format(
path_in_bundle=os.path.join(folder, file),
physical_file_path=os.path.join(inner_directory, file))
metadata.append(entry)
return metadata
def get_min_sdk_version(bundle_path: str, bundletool: str) -> int:
"""Get the minimum supported SDK version from an App Bundle file."""
bundletool_cmd = [
"java", "-jar", bundletool, "dump", "manifest", "--bundle", bundle_path,
"--xpath", "/manifest/uses-sdk/@android:minSdkVersion"
]
print("Running {bundletool_cmd}".format(bundletool_cmd=bundletool_cmd))
min_sdk = subprocess.check_output(bundletool_cmd)
return int(min_sdk.decode("utf-8").rstrip())
def get_strip_tcf_suffixes(args: argparse.Namespace) -> bool:
return args.striptcfsuffixes
def get_compress_install_time_assets(args: argparse.Namespace) -> bool:
return args.compressinstalltimeassets
def get_asset_pack_type(path: str) -> str:
"""Retrieve the Asset Pack delivery type from an AndroidManifest.xml file."""
xmldoc = xml.dom.minidom.parse(path)
tags = xmldoc.getElementsByTagName(ONDEMAND)
if tags.length:
return ONDEMAND
tags = xmldoc.getElementsByTagName(FASTFOLLOW)
if tags.length:
return FASTFOLLOW
tags = xmldoc.getElementsByTagName(UPFRONT)
if tags.length:
return UPFRONT
return None
def extract_bundle_config(bundle_folder: str, add_standalone_config: bool,
strip_tcf_suffixes: bool,
compress_install_time_assets: bool) -> str:
"""Extract the BundleConfig contents and optionally add standalone_config."""
bundle_config = config_pb2.BundleConfig()
with open(os.path.join(bundle_folder, "BundleConfig.pb"), mode="rb") as f:
content = f.read()
bundle_config.ParseFromString(content)
if compress_install_time_assets:
json_format.ParseDict(
{
"compression": {
"install_time_asset_module_default_compression": "COMPRESSED"
}
}, bundle_config)
if add_standalone_config:
json_format.ParseDict(
{
"optimizations": {
"standalone_config": {
"split_dimension": [{
"value": "ABI",
"negate": True
}, {
"value": "TEXTURE_COMPRESSION_FORMAT",
"negate": True
}, {
"value": "LANGUAGE",
"negate": True
}, {
"value": "SCREEN_DENSITY",
"negate": True
}],
"strip_64_bit_libraries": True
}
}
}, bundle_config)
# Check if game already defines any split_config dimensions
dimensions = []
try:
dimensions = list(json_format.MessageToDict(bundle_config)["optimizations"]
["splitsConfig"]["splitDimension"])
except KeyError:
print("No existing split dimensions")
tcf_split_dimension = {
"value": "TEXTURE_COMPRESSION_FORMAT",
"negate": False,
"suffix_stripping": {
"enabled": True,
"default_suffix": ""
}
}
# Add the TCF split dimension, if needed
if strip_tcf_suffixes:
dimensions.append(tcf_split_dimension)
if strip_tcf_suffixes:
json_format.ParseDict(
{
"optimizations": {
"splits_config": {
"split_dimension": dimensions
}
}
}, bundle_config)
output_path = os.path.join(bundle_folder, "BundleConfig.pb.json")
with open(output_path, mode="w") as f:
print(json_format.MessageToJson(bundle_config), file=f)
return output_path
def aapt_link(input_manifest_path: str, output_manifest_folder: str,
aapt2_bin_path: str, sdk_jar_path: str):
"""Run aapt link to convert the manifest to proto format."""
aapt_cmd = [
aapt2_bin_path, "link", "--proto-format", "--output-to-dir", "-o",
output_manifest_folder, "--manifest", input_manifest_path, "-I",
sdk_jar_path
]
print(" Running {aapt_cmd}".format(aapt_cmd=aapt_cmd))
exit_code = subprocess.call(aapt_cmd)
if exit_code != 0:
print(
"Error executing {aapt_cmd}".format(aapt_cmd=aapt_cmd), file=sys.stderr)
sys.exit(-1)
def process_packs(packs_folder: str, bundle_folder: str,
pack_names: typing.List[str], aapt2_bin_path: str,
sdk_jar_path: str) -> bool:
"""Repackage all packs into modules."""
print("Processing packs...")
has_upfront_pack = False
for pack_name in pack_names:
print(" Pack {pack_name}".format(pack_name=pack_name))
pack_basename = os.path.splitext(pack_name)[0]
pack_folder = os.path.join(bundle_folder, pack_basename)
os.makedirs(pack_folder)
print(" Extracting pack {pack_name} to {pack_folder}.".format(
pack_name=pack_name, pack_folder=pack_folder))
pack_zip_path = zipfile.ZipFile(
os.path.join(packs_folder, pack_name), "r")
pack_zip_path.extractall(path=pack_folder)
pack_zip_path.close()
print(" Processing manifest.")
manifest_folder = os.path.join(pack_folder, "manifest")
original_manifest_path = os.path.join(manifest_folder,
"AndroidManifest.xml")
has_upfront_pack = has_upfront_pack or (
get_asset_pack_type(original_manifest_path) == UPFRONT)
tmp_manifest_path = os.path.join(bundle_folder, "manifest.xml")
shutil.move(original_manifest_path, tmp_manifest_path)
aapt_link(tmp_manifest_path, manifest_folder,
aapt2_bin_path, sdk_jar_path)
print(" Cleaning up\n")
os.remove(os.path.join(manifest_folder, "resources.pb"))
os.remove(tmp_manifest_path)
return has_upfront_pack
def clear_autogenerated_bundle_files(bundle_folder: str):
print("Removing old META_INF and BundleConfig.pb")
shutil.rmtree(os.path.join(bundle_folder, "META-INF"), ignore_errors=True)
os.remove(os.path.join(bundle_folder, "BundleConfig.pb"))
print("Removing old __MACOSX folders")
purge_subdirs(bundle_folder, "__MACOSX")
print("Removing old .DS_Store files")
purge_files(bundle_folder, ".DS_Store")
def zip_module(module_folder: str, bundle_folder: str) -> str:
print(" Module {module_folder}".format(module_folder=module_folder))
basename = os.path.join(bundle_folder, module_folder)
module_zip = shutil.make_archive(basename, "zip", root_dir=basename)
return module_zip
def build_bundle(module_zip_files: typing.List[str], output_path: str,
bundle_config_path: str, metadata: typing.List[str],
bundletool_path: str):
"""Build the bundle using bundletool build-bundle."""
bundletool_cmd = [
"java", "-jar", bundletool_path, "build-bundle", "--modules",
",".join(module_zip_files), "--output", output_path, "--config",
bundle_config_path
]
for entry in metadata:
bundletool_cmd.append("--metadata-file")
bundletool_cmd.append(entry)
print("Running {bundletool_cmd}".format(bundletool_cmd=bundletool_cmd))
exit_code = subprocess.call(bundletool_cmd)
if exit_code != 0:
print(
"Error executing {bundletool_cmd}".format(
bundletool_cmd=bundletool_cmd),
file=sys.stderr)
sys.exit(-1)
def main() -> None:
args = parse_args()
bundle_path = get_input_bundle_path(args)
output_path = get_output_path(args)
aapt2_bin_path = get_aapt2_bin_path(args)
sdk_jar_path = get_sdk_jar_path(args)
bundletool_path = get_bundletool_path(args)
(pack_dir, pack_names) = get_packs(args)
strip_tcf_suffixes = get_strip_tcf_suffixes(args)
compress_install_time_assets = get_compress_install_time_assets(args)
with tempfile.TemporaryDirectory() as bundle_folder:
print("Extracting input app bundle to {bundle_folder}".format(
bundle_folder=bundle_folder))
bundle_zip_path = zipfile.ZipFile(bundle_path, "r")
bundle_zip_path.extractall(path=bundle_folder)
bundle_zip_path.close()
has_upfront_pack = process_packs(pack_dir, bundle_folder, pack_names,
aapt2_bin_path, sdk_jar_path)
uses_upfront_pre_l = has_upfront_pack and get_min_sdk_version(
bundle_path, bundletool_path) < 21
bundle_config_path = extract_bundle_config(bundle_folder,
uses_upfront_pre_l,
strip_tcf_suffixes,
compress_install_time_assets)
clear_autogenerated_bundle_files(bundle_folder)
print("Parsing bundle metadata...")
metadata = parse_bundle_metadata(bundle_folder)
print("Zipping module folders...")
metadata = []
module_folders = (
module_folder for module_folder in os.listdir(bundle_folder)
if module_folder != "BUNDLE-METADATA" and
os.path.isdir(os.path.join(bundle_folder, module_folder)))
module_zip_files = [
zip_module(module_folder, bundle_folder)
for module_folder in module_folders
]
bundletool_cmd = [
"java", "-jar", bundletool_path, "build-bundle", "--modules",
",".join(module_zip_files), "--output", output_path, "--config",
bundle_config_path
]
for entry in metadata:
bundletool_cmd.append("--metadata-file")
bundletool_cmd.append(entry)
print("Running {bundletool_cmd}".format(bundletool_cmd=bundletool_cmd))
exit_code = subprocess.call(bundletool_cmd)
if exit_code != 0:
print(
"Error executing {bundletool_cmd}".format(
bundletool_cmd=bundletool_cmd),
file=sys.stderr)
sys.exit(-1)
print("Augmented app bundle is ready at {output_path}".format(
output_path=output_path))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,197 @@
syntax = "proto3";
package android.bundle;
option java_package = "com.android.bundle";
message BundleConfig {
Bundletool bundletool = 1;
Optimizations optimizations = 2;
Compression compression = 3;
// Resources to be always kept in the master split.
MasterResources master_resources = 4;
ApexConfig apex_config = 5;
// APKs to be signed with the same key as generated APKs.
repeated UnsignedEmbeddedApkConfig unsigned_embedded_apk_config = 6;
AssetModulesConfig asset_modules_config = 7;
enum BundleType {
REGULAR = 0;
APEX = 1;
ASSET_ONLY = 2;
}
BundleType type = 8;
}
message Bundletool {
reserved 1;
// Version of BundleTool used to build the Bundle.
string version = 2;
}
message Compression {
// Glob matching the list of files to leave uncompressed in the APKs.
// The matching is done against the path of files in the APK, thus excluding
// the name of the modules, and using forward slash ("/") as a name separator.
// Examples: "res/raw/**", "assets/**/*.uncompressed", etc.
repeated string uncompressed_glob = 1;
enum AssetModuleCompression {
UNSPECIFIED = 0;
// Assets are left uncompressed in the generated asset module.
UNCOMPRESSED = 1;
// Assets are compressed in the generated asset module.
// This option can be overridden at a finer granularity by specifying
// files or folders to keep uncompressed in `uncompressed_glob`.
// This option should only be used if the app is able to handle compressed
// asset module content at runtime (some runtime APIs may misbehave).
COMPRESSED = 2;
}
// Default compression strategy for install-time asset modules.
// If the compression strategy indicates to compress a file and the same file
// matches one of the `uncompressed_glob` values, the `uncompressed_glob`
// takes precedence (the file is left uncompressed in the generated APK).
//
// If unspecified, asset module content is left uncompressed in the
// generated asset modules.
//
// Note: this flag only configures the compression strategy for install-time
// asset modules; the content of on-demand and fast-follow asset modules is
// always kept uncompressed.
AssetModuleCompression install_time_asset_module_default_compression = 2;
}
// Resources to keep in the master split.
message MasterResources {
// Resource IDs to be kept in master split.
repeated int32 resource_ids = 1;
// Resource names to be kept in master split.
repeated string resource_names = 2;
}
message Optimizations {
SplitsConfig splits_config = 1;
// This is for uncompressing native libraries on M+ devices (L+ devices on
// instant apps).
UncompressNativeLibraries uncompress_native_libraries = 2;
// This is for uncompressing dex files on P+ devices.
UncompressDexFiles uncompress_dex_files = 3;
// Configuration for the generation of standalone APKs.
// If no StandaloneConfig is set, the configuration is inherited from
// splits_config.
StandaloneConfig standalone_config = 4;
}
message UncompressNativeLibraries {
bool enabled = 1;
}
message UncompressDexFiles {
bool enabled = 1;
}
// Optimization configuration used to generate Split APKs.
message SplitsConfig {
repeated SplitDimension split_dimension = 1;
}
// Optimization configuration used to generate Standalone APKs.
message StandaloneConfig {
// Device targeting dimensions to shard.
repeated SplitDimension split_dimension = 1;
// Whether 64 bit libraries should be stripped from Standalone APKs.
bool strip_64_bit_libraries = 2;
// Dex merging strategy that should be applied to produce Standalone APKs.
DexMergingStrategy dex_merging_strategy = 3;
enum DexMergingStrategy {
// Strategy that does dex merging for applications that have minimum SDK
// below 21 to ensure dex files from all modules are merged into one or
// mainDexList is applied when merging into one dex is not possible. For
// applications with minSdk >= 21 dex files from all modules are copied into
// standalone APK as is because Android supports multiple dex files natively
// starting from Android 5.0.
MERGE_IF_NEEDED = 0;
// Requires to copy dex files from all modules into standalone APK as is.
// If an application supports SDKs below 21 this strategy puts
// responsibility of providing dex files compatible with legacy multidex on
// application developers.
NEVER_MERGE = 1;
}
}
message SplitDimension {
enum Value {
UNSPECIFIED_VALUE = 0;
ABI = 1;
SCREEN_DENSITY = 2;
LANGUAGE = 3;
TEXTURE_COMPRESSION_FORMAT = 4;
DEVICE_TIER = 6;
}
Value value = 1;
// If set to 'true', indicates that APKs should *not* be split by this
// dimension.
bool negate = 2;
// Optional transformation to be applied to asset directories where
// the targeting is encoded in the directory name (e.g: assets/foo#tcf_etc1)
SuffixStripping suffix_stripping = 3;
}
message SuffixStripping {
// If set to 'true', indicates that the targeting suffix should be removed
// from assets paths for this dimension when splits (e.g: "asset packs") or
// standalone/universal APKs are generated.
// This only applies to assets.
// For example a folder with path "assets/level1_textures#tcf_etc1"
// would be outputted to "assets/level1_textures". File contents are
// unchanged.
bool enabled = 1;
// The default suffix to be used for the cases where separate slices can't
// be generated for this dimension - typically for standalone or universal
// APKs.
// This default suffix defines the directories to retain. The others are
// discarded: standalone/universal APKs will contain only directories
// targeted at this value for the dimension.
//
// If not set or empty, the fallback directory in each directory group will be
// used (for example, if both "assets/level1_textures#tcf_etc1" and
// "assets/level1_textures" are present and the default suffix is empty,
// then only "assets/level1_textures" will be used).
string default_suffix = 2;
}
// Configuration for processing APEX bundles.
// https://source.android.com/devices/tech/ota/apex
message ApexConfig {
// Configuration for processing of APKs embedded in an APEX image.
repeated ApexEmbeddedApkConfig apex_embedded_apk_config = 1;
}
message ApexEmbeddedApkConfig {
// Android package name of the APK.
string package_name = 1;
// Path to the APK within the APEX system image.
string path = 2;
}
message UnsignedEmbeddedApkConfig {
// Path to the APK inside the module (e.g. if the path inside the bundle
// is split/assets/example.apk, this will be assets/example.apk).
string path = 1;
}
message AssetModulesConfig {
// App versionCodes that will be updated with these asset modules.
// Only relevant for asset-only bundles.
repeated int64 app_version = 1;
// Version tag for the asset upload.
// Only relevant for asset-only bundles.
string asset_version_tag = 2;
}

View File

@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: config.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onfig.proto\x12\x0e\x61ndroid.bundle\"\x91\x04\n\x0c\x42undleConfig\x12.\n\nbundletool\x18\x01 \x01(\x0b\x32\x1a.android.bundle.Bundletool\x12\x34\n\roptimizations\x18\x02 \x01(\x0b\x32\x1d.android.bundle.Optimizations\x12\x30\n\x0b\x63ompression\x18\x03 \x01(\x0b\x32\x1b.android.bundle.Compression\x12\x39\n\x10master_resources\x18\x04 \x01(\x0b\x32\x1f.android.bundle.MasterResources\x12/\n\x0b\x61pex_config\x18\x05 \x01(\x0b\x32\x1a.android.bundle.ApexConfig\x12O\n\x1cunsigned_embedded_apk_config\x18\x06 \x03(\x0b\x32).android.bundle.UnsignedEmbeddedApkConfig\x12@\n\x14\x61sset_modules_config\x18\x07 \x01(\x0b\x32\".android.bundle.AssetModulesConfig\x12\x35\n\x04type\x18\x08 \x01(\x0e\x32\'.android.bundle.BundleConfig.BundleType\"3\n\nBundleType\x12\x0b\n\x07REGULAR\x10\x00\x12\x08\n\x04\x41PEX\x10\x01\x12\x0e\n\nASSET_ONLY\x10\x02\"#\n\nBundletool\x12\x0f\n\x07version\x18\x02 \x01(\tJ\x04\x08\x01\x10\x02\"\xe0\x01\n\x0b\x43ompression\x12\x19\n\x11uncompressed_glob\x18\x01 \x03(\t\x12i\n-install_time_asset_module_default_compression\x18\x02 \x01(\x0e\x32\x32.android.bundle.Compression.AssetModuleCompression\"K\n\x16\x41ssetModuleCompression\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x10\n\x0cUNCOMPRESSED\x10\x01\x12\x0e\n\nCOMPRESSED\x10\x02\"?\n\x0fMasterResources\x12\x14\n\x0cresource_ids\x18\x01 \x03(\x05\x12\x16\n\x0eresource_names\x18\x02 \x03(\t\"\x93\x02\n\rOptimizations\x12\x33\n\rsplits_config\x18\x01 \x01(\x0b\x32\x1c.android.bundle.SplitsConfig\x12N\n\x1buncompress_native_libraries\x18\x02 \x01(\x0b\x32).android.bundle.UncompressNativeLibraries\x12@\n\x14uncompress_dex_files\x18\x03 \x01(\x0b\x32\".android.bundle.UncompressDexFiles\x12;\n\x11standalone_config\x18\x04 \x01(\x0b\x32 .android.bundle.StandaloneConfig\",\n\x19UncompressNativeLibraries\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\"%\n\x12UncompressDexFiles\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\"G\n\x0cSplitsConfig\x12\x37\n\x0fsplit_dimension\x18\x01 \x03(\x0b\x32\x1e.android.bundle.SplitDimension\"\xfa\x01\n\x10StandaloneConfig\x12\x37\n\x0fsplit_dimension\x18\x01 \x03(\x0b\x32\x1e.android.bundle.SplitDimension\x12\x1e\n\x16strip_64_bit_libraries\x18\x02 \x01(\x08\x12Q\n\x14\x64\x65x_merging_strategy\x18\x03 \x01(\x0e\x32\x33.android.bundle.StandaloneConfig.DexMergingStrategy\":\n\x12\x44\x65xMergingStrategy\x12\x13\n\x0fMERGE_IF_NEEDED\x10\x00\x12\x0f\n\x0bNEVER_MERGE\x10\x01\"\x8c\x02\n\x0eSplitDimension\x12\x33\n\x05value\x18\x01 \x01(\x0e\x32$.android.bundle.SplitDimension.Value\x12\x0e\n\x06negate\x18\x02 \x01(\x08\x12\x39\n\x10suffix_stripping\x18\x03 \x01(\x0b\x32\x1f.android.bundle.SuffixStripping\"z\n\x05Value\x12\x15\n\x11UNSPECIFIED_VALUE\x10\x00\x12\x07\n\x03\x41\x42I\x10\x01\x12\x12\n\x0eSCREEN_DENSITY\x10\x02\x12\x0c\n\x08LANGUAGE\x10\x03\x12\x1e\n\x1aTEXTURE_COMPRESSION_FORMAT\x10\x04\x12\x0f\n\x0b\x44\x45VICE_TIER\x10\x06\":\n\x0fSuffixStripping\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x12\x16\n\x0e\x64\x65\x66\x61ult_suffix\x18\x02 \x01(\t\"U\n\nApexConfig\x12G\n\x18\x61pex_embedded_apk_config\x18\x01 \x03(\x0b\x32%.android.bundle.ApexEmbeddedApkConfig\";\n\x15\x41pexEmbeddedApkConfig\x12\x14\n\x0cpackage_name\x18\x01 \x01(\t\x12\x0c\n\x04path\x18\x02 \x01(\t\")\n\x19UnsignedEmbeddedApkConfig\x12\x0c\n\x04path\x18\x01 \x01(\t\"D\n\x12\x41ssetModulesConfig\x12\x13\n\x0b\x61pp_version\x18\x01 \x03(\x03\x12\x19\n\x11\x61sset_version_tag\x18\x02 \x01(\tB\x14\n\x12\x63om.android.bundleb\x06proto3')
_BUNDLECONFIG = DESCRIPTOR.message_types_by_name['BundleConfig']
_BUNDLETOOL = DESCRIPTOR.message_types_by_name['Bundletool']
_COMPRESSION = DESCRIPTOR.message_types_by_name['Compression']
_MASTERRESOURCES = DESCRIPTOR.message_types_by_name['MasterResources']
_OPTIMIZATIONS = DESCRIPTOR.message_types_by_name['Optimizations']
_UNCOMPRESSNATIVELIBRARIES = DESCRIPTOR.message_types_by_name['UncompressNativeLibraries']
_UNCOMPRESSDEXFILES = DESCRIPTOR.message_types_by_name['UncompressDexFiles']
_SPLITSCONFIG = DESCRIPTOR.message_types_by_name['SplitsConfig']
_STANDALONECONFIG = DESCRIPTOR.message_types_by_name['StandaloneConfig']
_SPLITDIMENSION = DESCRIPTOR.message_types_by_name['SplitDimension']
_SUFFIXSTRIPPING = DESCRIPTOR.message_types_by_name['SuffixStripping']
_APEXCONFIG = DESCRIPTOR.message_types_by_name['ApexConfig']
_APEXEMBEDDEDAPKCONFIG = DESCRIPTOR.message_types_by_name['ApexEmbeddedApkConfig']
_UNSIGNEDEMBEDDEDAPKCONFIG = DESCRIPTOR.message_types_by_name['UnsignedEmbeddedApkConfig']
_ASSETMODULESCONFIG = DESCRIPTOR.message_types_by_name['AssetModulesConfig']
_BUNDLECONFIG_BUNDLETYPE = _BUNDLECONFIG.enum_types_by_name['BundleType']
_COMPRESSION_ASSETMODULECOMPRESSION = _COMPRESSION.enum_types_by_name['AssetModuleCompression']
_STANDALONECONFIG_DEXMERGINGSTRATEGY = _STANDALONECONFIG.enum_types_by_name['DexMergingStrategy']
_SPLITDIMENSION_VALUE = _SPLITDIMENSION.enum_types_by_name['Value']
BundleConfig = _reflection.GeneratedProtocolMessageType('BundleConfig', (_message.Message,), {
'DESCRIPTOR' : _BUNDLECONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.BundleConfig)
})
_sym_db.RegisterMessage(BundleConfig)
Bundletool = _reflection.GeneratedProtocolMessageType('Bundletool', (_message.Message,), {
'DESCRIPTOR' : _BUNDLETOOL,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.Bundletool)
})
_sym_db.RegisterMessage(Bundletool)
Compression = _reflection.GeneratedProtocolMessageType('Compression', (_message.Message,), {
'DESCRIPTOR' : _COMPRESSION,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.Compression)
})
_sym_db.RegisterMessage(Compression)
MasterResources = _reflection.GeneratedProtocolMessageType('MasterResources', (_message.Message,), {
'DESCRIPTOR' : _MASTERRESOURCES,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.MasterResources)
})
_sym_db.RegisterMessage(MasterResources)
Optimizations = _reflection.GeneratedProtocolMessageType('Optimizations', (_message.Message,), {
'DESCRIPTOR' : _OPTIMIZATIONS,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.Optimizations)
})
_sym_db.RegisterMessage(Optimizations)
UncompressNativeLibraries = _reflection.GeneratedProtocolMessageType('UncompressNativeLibraries', (_message.Message,), {
'DESCRIPTOR' : _UNCOMPRESSNATIVELIBRARIES,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.UncompressNativeLibraries)
})
_sym_db.RegisterMessage(UncompressNativeLibraries)
UncompressDexFiles = _reflection.GeneratedProtocolMessageType('UncompressDexFiles', (_message.Message,), {
'DESCRIPTOR' : _UNCOMPRESSDEXFILES,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.UncompressDexFiles)
})
_sym_db.RegisterMessage(UncompressDexFiles)
SplitsConfig = _reflection.GeneratedProtocolMessageType('SplitsConfig', (_message.Message,), {
'DESCRIPTOR' : _SPLITSCONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.SplitsConfig)
})
_sym_db.RegisterMessage(SplitsConfig)
StandaloneConfig = _reflection.GeneratedProtocolMessageType('StandaloneConfig', (_message.Message,), {
'DESCRIPTOR' : _STANDALONECONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.StandaloneConfig)
})
_sym_db.RegisterMessage(StandaloneConfig)
SplitDimension = _reflection.GeneratedProtocolMessageType('SplitDimension', (_message.Message,), {
'DESCRIPTOR' : _SPLITDIMENSION,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.SplitDimension)
})
_sym_db.RegisterMessage(SplitDimension)
SuffixStripping = _reflection.GeneratedProtocolMessageType('SuffixStripping', (_message.Message,), {
'DESCRIPTOR' : _SUFFIXSTRIPPING,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.SuffixStripping)
})
_sym_db.RegisterMessage(SuffixStripping)
ApexConfig = _reflection.GeneratedProtocolMessageType('ApexConfig', (_message.Message,), {
'DESCRIPTOR' : _APEXCONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.ApexConfig)
})
_sym_db.RegisterMessage(ApexConfig)
ApexEmbeddedApkConfig = _reflection.GeneratedProtocolMessageType('ApexEmbeddedApkConfig', (_message.Message,), {
'DESCRIPTOR' : _APEXEMBEDDEDAPKCONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.ApexEmbeddedApkConfig)
})
_sym_db.RegisterMessage(ApexEmbeddedApkConfig)
UnsignedEmbeddedApkConfig = _reflection.GeneratedProtocolMessageType('UnsignedEmbeddedApkConfig', (_message.Message,), {
'DESCRIPTOR' : _UNSIGNEDEMBEDDEDAPKCONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.UnsignedEmbeddedApkConfig)
})
_sym_db.RegisterMessage(UnsignedEmbeddedApkConfig)
AssetModulesConfig = _reflection.GeneratedProtocolMessageType('AssetModulesConfig', (_message.Message,), {
'DESCRIPTOR' : _ASSETMODULESCONFIG,
'__module__' : 'config_pb2'
# @@protoc_insertion_point(class_scope:android.bundle.AssetModulesConfig)
})
_sym_db.RegisterMessage(AssetModulesConfig)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\022com.android.bundle'
_BUNDLECONFIG._serialized_start=33
_BUNDLECONFIG._serialized_end=562
_BUNDLECONFIG_BUNDLETYPE._serialized_start=511
_BUNDLECONFIG_BUNDLETYPE._serialized_end=562
_BUNDLETOOL._serialized_start=564
_BUNDLETOOL._serialized_end=599
_COMPRESSION._serialized_start=602
_COMPRESSION._serialized_end=826
_COMPRESSION_ASSETMODULECOMPRESSION._serialized_start=751
_COMPRESSION_ASSETMODULECOMPRESSION._serialized_end=826
_MASTERRESOURCES._serialized_start=828
_MASTERRESOURCES._serialized_end=891
_OPTIMIZATIONS._serialized_start=894
_OPTIMIZATIONS._serialized_end=1169
_UNCOMPRESSNATIVELIBRARIES._serialized_start=1171
_UNCOMPRESSNATIVELIBRARIES._serialized_end=1215
_UNCOMPRESSDEXFILES._serialized_start=1217
_UNCOMPRESSDEXFILES._serialized_end=1254
_SPLITSCONFIG._serialized_start=1256
_SPLITSCONFIG._serialized_end=1327
_STANDALONECONFIG._serialized_start=1330
_STANDALONECONFIG._serialized_end=1580
_STANDALONECONFIG_DEXMERGINGSTRATEGY._serialized_start=1522
_STANDALONECONFIG_DEXMERGINGSTRATEGY._serialized_end=1580
_SPLITDIMENSION._serialized_start=1583
_SPLITDIMENSION._serialized_end=1851
_SPLITDIMENSION_VALUE._serialized_start=1729
_SPLITDIMENSION_VALUE._serialized_end=1851
_SUFFIXSTRIPPING._serialized_start=1853
_SUFFIXSTRIPPING._serialized_end=1911
_APEXCONFIG._serialized_start=1913
_APEXCONFIG._serialized_end=1998
_APEXEMBEDDEDAPKCONFIG._serialized_start=2000
_APEXEMBEDDEDAPKCONFIG._serialized_end=2059
_UNSIGNEDEMBEDDEDAPKCONFIG._serialized_start=2061
_UNSIGNEDEMBEDDEDAPKCONFIG._serialized_end=2102
_ASSETMODULESCONFIG._serialized_start=2104
_ASSETMODULESCONFIG._serialized_end=2172
# @@protoc_insertion_point(module_scope)

View File

@@ -0,0 +1,176 @@
#!/usr/bin/env python3
#
# Copyright 2021 Google LLC
#
# 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.
"""Script to generate a valid asset pack from a given assets folder.
Instant delivery is not supported.
"""
import argparse
import distutils.dir_util
import os
import shutil
import sys
import tempfile
manifest_template = """<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:dist="http://schemas.android.com/apk/distribution" package="{package_name}" split="{asset_pack_name}">
<dist:module dist:type="asset-pack">
<dist:delivery>
<dist:{delivery_mode}/>
</dist:delivery>
<dist:fusing dist:include="true"/>
</dist:module>
</manifest>
"""
def parse_args() -> argparse.Namespace:
"""Parse input arguments."""
parser = argparse.ArgumentParser(
description="Generates a valid asset pack from a given assets folder",
add_help=True)
parser.add_argument(
"--packagename", required=True, help="Package name of the app")
parser.add_argument(
"--assetpackname", required=True, help="Name of the asset pack module")
parser.add_argument(
"--deliverymode",
required=True,
choices=["install-time", "fast-follow", "on-demand"],
help="Delivery mode of the asset pack module")
parser.add_argument(
"--assetsdir", required=True, help="Folder to read assets from")
parser.add_argument("--outdir", required=True, help="Output folder")
parser.add_argument(
"--overwrite",
required=False,
action="store_true",
help="Overwrite existing files")
return parser.parse_args()
def abs_expand_all(path: str) -> str:
return os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
def get_assets_dir(args: argparse.Namespace) -> str:
assets_dir = abs_expand_all(args.assetsdir)
if (not (os.path.isdir(assets_dir) and os.access(assets_dir, os.X_OK) and
os.access(assets_dir, os.R_OK))):
print(
"Assets folder ({assets_dir}) is not accessible. Check permissions."
.format(assets_dir=assets_dir),
file=sys.stderr)
sys.exit(-1)
return assets_dir
def create_output_dir(args: argparse.Namespace) -> str:
"""Get the output directory."""
output_dir = abs_expand_all(args.outdir)
if not os.path.isdir(output_dir):
try:
os.makedirs(output_dir)
except OSError as e:
print(e, file=sys.stderr)
sys.exit(-1)
if (not (os.path.isdir(output_dir) and os.access(output_dir, os.X_OK) and
os.access(output_dir, os.W_OK))):
print(
"Output folder ({output_dir}) is not accessible. Check permissions."
.format(output_dir=output_dir),
file=sys.stderr)
sys.exit(-1)
return output_dir
def get_output_file_name(output_dir: str, args: argparse.Namespace) -> str:
output_file_name = os.path.join(output_dir, args.assetpackname)
if os.path.exists(output_file_name) and not args.overwrite:
print(
"Output file {output_file_name} exists. Specify --overwrite to bypass. Exiting."
.format(output_file_name=output_file_name))
sys.exit(-1)
return output_file_name
def make_manifest(package_name: str, asset_pack_name: str, delivery_mode: str,
pack_directory: str) -> None:
"""Generate the Android Manifest file for the pack."""
manifest = manifest_template.format(
package_name=package_name,
asset_pack_name=asset_pack_name,
delivery_mode=delivery_mode)
manifest_folder = os.path.join(pack_directory, "manifest")
try:
os.makedirs(manifest_folder)
except OSError as e:
print("Cannot create manifest folder. {e}".format(
e=e), file=sys.stderr)
sys.exit(-1)
manifest_file_name = os.path.join(manifest_folder, "AndroidManifest.xml")
manifest_file = open(manifest_file_name, "w")
print(manifest, file=manifest_file)
manifest_file.close()
print("Generated {manifest}".format(manifest=manifest_file_name))
def copy_assets(src: str, dest: str) -> None:
"""Copy assets from one folder to another."""
assets_folder = os.path.join(dest, "assets")
try:
os.makedirs(assets_folder)
except OSError as e:
print("Cannot create assets folder. {e}".format(e=e), file=sys.stderr)
sys.exit(-1)
try:
distutils.dir_util.copy_tree(src, assets_folder)
except FileNotFoundError as e:
print(
"Cannot copy assets folder into temporary folder. {e}".format(e=e),
file=sys.stderr)
sys.exit(-1)
print(
"Copied assets into {assets_folder}".format(assets_folder=assets_folder))
def main():
args = parse_args()
assets_dir = get_assets_dir(args)
output_dir = create_output_dir(args)
output_file_name = get_output_file_name(output_dir, args)
with tempfile.TemporaryDirectory(dir=output_dir) as pack_dir:
print("Created temporary working folder: {pack_dir}".format(
pack_dir=pack_dir))
make_manifest(args.packagename, args.assetpackname, args.deliverymode,
pack_dir)
copy_assets(assets_dir, pack_dir)
output_pack_path = shutil.make_archive(
os.path.join(output_dir, output_file_name), "zip", pack_dir)
print("Asset pack is generated at {output_pack_path}.\nDone.".format(
output_pack_path=output_pack_path))
if __name__ == "__main__":
main()