Initial commit
This commit is contained in:
62
devtools/packaging/dlc-games.json
Normal file
62
devtools/packaging/dlc-games.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"beneath-a-steel-sky": {
|
||||
"name": "Beneath a Steel Sky - Freeware CD Version",
|
||||
"packname": "beneath_a_steel_sky",
|
||||
"description": "A science-fiction thriller set in a bleak post-apocalyptic vision of the future, Beneath a Steel Sky revolves around \"Union City\", where selfishness, rivalry, and corruption by its citizens seems to be all too common, those who can afford it live underground, away from the pollution and social problems which are plaguing the city. \n\nYou take on the role of Robert Foster, an outcast of sorts from the city since a boy who was raised in a remote environment outside of Union City simply termed \"the gap\". Robert's mother took him away from Union City as a child on their way to \"Hobart\" but the helicopter crashed on its way, unfortunately Robert's mother dies, but he survives and is left to be raised by a local tribe from the gap. \n\nYears later, Union City security drops by and abducts Robert, killing his tribe in the process; upon reaching the city the helicopter taking him there crashes with him escaping, high upon a tower block in the middle of the city he sets out to discover the truth about his past, and to seek vengeance for the killing of his tribe. \n\nNote that this package only contains game-data. The game engine is provided by ScummVM.",
|
||||
"shortDescription": "Adventure sci-fi thriller game for ScummVM",
|
||||
"supportedLanguages": {
|
||||
"interface": ["english"],
|
||||
"audio": ["english"],
|
||||
"subtitles": ["english"]
|
||||
},
|
||||
"genres": ["Adventure", "Free to Play", "Point & Click", "Cyberpunk"],
|
||||
"storeCategories": ["Games"],
|
||||
"supportContactInfo": {
|
||||
"websiteURL": "https://www.scummvm.org/"
|
||||
},
|
||||
"graphicalAssets": {
|
||||
"screenshots": [
|
||||
"https://www.scummvm.org/data/screenshots/sky/sky/sky_dos_en_1_1.jpg",
|
||||
"https://www.scummvm.org/data/screenshots/sky/sky/sky_dos_en_1_2.jpg"
|
||||
],
|
||||
"StoreLogo": "https://raw.githubusercontent.com/scummvm/scummvm-icons/master/icons/sky-sky.png"
|
||||
},
|
||||
"scummVMConfigData": {
|
||||
"description": "Beneath a Steel Sky (v0.0372 CD/DOS)",
|
||||
"engineid": "sky",
|
||||
"extra": "v0.0372 CD",
|
||||
"gameid": "sky",
|
||||
"guioptions": "lang_English (GB) lang_German lang_French lang_English (US) lang_Swedish lang_Italian lang_Portuguese (Brazil) lang_Spanish",
|
||||
"language": "",
|
||||
"platform": "pc"
|
||||
},
|
||||
"size": "69377781",
|
||||
"url": "https://downloads.scummvm.org/frs/extras/Beneath%20a%20Steel%20Sky/bass-cd-1.2.zip"
|
||||
},
|
||||
"broken-sword-2.5": {
|
||||
"name": "broken-sword-2.5",
|
||||
"packname": "broken_sword_25",
|
||||
"description": "Broken Sword 2.5: The Return of the Templars (original title: Baphomets Fluch 2.5) is a fan game of the Broken Sword series, which was given clearance to be developed, and certain art assets were granted to the team, by Revolution Software. It takes place between the first and second games. George travels to France after he receives a letter stating Nico had died. His relief to find her alive is short-lived, as he soon finds out that she has been accused of murdering the mayor of Paris. The two must clear Nico's name, and discover who is actually behind the murder. \n\nNote that this package only contains game-data. The game engine is provided by ScummVM.",
|
||||
"shortDescription": "Fan game of the Broken Sword series for ScummVM",
|
||||
"supportedLanguages": {
|
||||
"interface": ["english"],
|
||||
"audio": ["english"],
|
||||
"subtitles": ["english"]
|
||||
},
|
||||
"genres": ["Adventure", "Free to Play", "Point & Click"],
|
||||
"storeCategories": ["Games"],
|
||||
"supportContactInfo": {
|
||||
"websiteURL": "https://www.scummvm.org/"
|
||||
},
|
||||
"graphicalAssets": {
|
||||
"screenshots": [
|
||||
"https://www.scummvm.org/data/screenshots/sword25/sword25/sword25_win_de_1_10.jpg",
|
||||
"https://www.scummvm.org/data/screenshots/sword25/sword25/sword25_win_de_1_11.jpg",
|
||||
"https://www.scummvm.org/data/screenshots/sword25/sword25/sword25_win_de_1_12.jpg",
|
||||
"https://www.scummvm.org/data/screenshots/sword25/sword25/sword25_win_de_1_13.jpg",
|
||||
"https://www.scummvm.org/data/screenshots/sword25/sword25/sword25_win_de_1_14.jpg"
|
||||
],
|
||||
"StoreLogo": "https://raw.githubusercontent.com/scummvm/scummvm-icons/master/icons/sword25-sword25.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
109
devtools/packaging/dlc-metadata-schema.json
Normal file
109
devtools/packaging/dlc-metadata-schema.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DLC Metadata Schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"description": { "type": "string" },
|
||||
"shortDescription": { "type": "string" },
|
||||
"installPath": { "type": "string" },
|
||||
"developer": { "type": "string" },
|
||||
"publisher": { "type": "string" },
|
||||
"websiteURL": { "type": "string", "format": "uri" },
|
||||
"forumURL": { "type": "string", "format": "uri" },
|
||||
"statsURL": { "type": "string", "format": "uri" },
|
||||
"manualURL": { "type": "string", "format": "uri" },
|
||||
"metacriticEntry": { "type": "string" },
|
||||
"searchKeywords": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"supportedLanguages": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"interface": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"audio": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"subtitles": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemRequirements": { "type": "string" },
|
||||
"genres": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"storeCategories": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"players": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"supportedFeatures": { "type": "string" },
|
||||
"thirdPartyDRM": { "type": "string" },
|
||||
"license": { "type": "string" },
|
||||
"supportContactInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"websiteURL": { "type": "string", "format": "uri" },
|
||||
"email": { "type": "string", "format": "email" },
|
||||
"phone": { "type": "string" }
|
||||
},
|
||||
"anyOf": [
|
||||
{ "required": ["websiteURL"] },
|
||||
{ "required": ["email"] },
|
||||
{ "required": ["phone"] }
|
||||
]
|
||||
},
|
||||
"graphicalAssets": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"screenshots": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "format": "uri" }
|
||||
},
|
||||
"headerImage": { "type": "string", "format": "uri" },
|
||||
"smallCapsuleImage": { "type": "string", "format": "uri" },
|
||||
"largeCapsuleImage": { "type": "string", "format": "uri" },
|
||||
"trailer": { "type": "string", "format": "uri" },
|
||||
"StoreLogo": { "type": "string", "format": "uri" },
|
||||
"Square44x44Logo": { "type": "string", "format": "uri" },
|
||||
"Square150x150Logo": { "type": "string", "format": "uri" },
|
||||
"BackgroundColor": { "type": "string", "format": "uri" }
|
||||
}
|
||||
},
|
||||
"scummVMConfigData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": { "type": "string" },
|
||||
"engineid": { "type": "string" },
|
||||
"extra": { "type": "string" },
|
||||
"gameid": { "type": "string" },
|
||||
"guioptions": { "type": "string" },
|
||||
"language": { "type": "string" },
|
||||
"platform": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"size": { "type": "string" },
|
||||
"url": { "type" : "string" }
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"shortDescription",
|
||||
"supportedLanguages",
|
||||
"genres",
|
||||
"storeCategories",
|
||||
"supportContactInfo",
|
||||
"graphicalAssets"
|
||||
]
|
||||
}
|
||||
36
devtools/packaging/export-platforms.json
Normal file
36
devtools/packaging/export-platforms.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"android": {
|
||||
"target": "android",
|
||||
"prepare": {
|
||||
"type": "python-script",
|
||||
"location": "android/generate_asset_pack.py",
|
||||
"options": {
|
||||
"--packagename": "org.scummvm.scummvm",
|
||||
"--assetpackname": "$$packname",
|
||||
"--deliverymode": "on-demand",
|
||||
"--assetsdir": "$game_location",
|
||||
"--outdir": "output/android"
|
||||
},
|
||||
"successMessage": "Asset pack required for bundling stage is successfully created!"
|
||||
},
|
||||
"bundle": {
|
||||
"type": "python-script",
|
||||
"location": "android/add_packs.py",
|
||||
"options": {
|
||||
"--androidsdk": "$$$ANDROID_SDK_ROOT",
|
||||
"--sdkver": "33",
|
||||
"--buildtoolsver": "33.0.1",
|
||||
"--bundletool": "scripts/android/bundletool-all-1.15.1.jar",
|
||||
"--inputbundle": "$binary_location",
|
||||
"--packdir": "output/android",
|
||||
"--packnames": "$$packname",
|
||||
"--output": "output/android/scummvm-bundled.aab"
|
||||
},
|
||||
"successMessage": "Android App Bundle is successfully created in output/android. You can now manually upload this file on Google Play Console."
|
||||
},
|
||||
"upload": {
|
||||
"type": "none"
|
||||
}
|
||||
},
|
||||
"steam": {}
|
||||
}
|
||||
92
devtools/packaging/package.py
Normal file
92
devtools/packaging/package.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def prepare(ex, game, arg_dict):
|
||||
if ex["prepare"]["type"] == "python-script":
|
||||
cmd = sys.executable + " scripts/" + ex["prepare"]["location"]
|
||||
for key in ex["prepare"]["options"].keys():
|
||||
cmd += " " + key + " "
|
||||
value = ex["prepare"]["options"][key]
|
||||
# $ -> get from here (package.py), $$ -> get from games.json, $$$ -> get from environment variables
|
||||
if len(value) >= 3 and value[2] == '$':
|
||||
cmd += value[2:]
|
||||
elif len(value) >= 2 and value[1] == '$':
|
||||
# check in game's metadata
|
||||
cmd += game[value[2:]]
|
||||
elif len(value) >= 1 and value[0] == '$':
|
||||
cmd += arg_dict[value[1:]]
|
||||
else:
|
||||
cmd += value
|
||||
|
||||
subprocess.run(cmd, shell=True)
|
||||
print(ex["prepare"]["successMessage"])
|
||||
|
||||
|
||||
def bundle(ex, game, arg_dict):
|
||||
if ex["bundle"]["type"] == "python-script":
|
||||
cmd = sys.executable + " scripts/" + ex["bundle"]["location"]
|
||||
for key in ex["bundle"]["options"].keys():
|
||||
cmd += " " + key + " "
|
||||
value = ex["bundle"]["options"][key]
|
||||
# $ -> get from here (package.py), $$ -> get from games.json, $$$ -> get from environment variables
|
||||
if len(value) >= 3 and value[2] == '$':
|
||||
cmd += value[2:]
|
||||
elif len(value) >= 2 and value[1] == '$':
|
||||
# check in game's metadata
|
||||
cmd += game[value[2:]]
|
||||
if value == "$$packname":
|
||||
cmd += ".zip" # temporary hack
|
||||
elif len(value) >= 1 and value[0] == '$':
|
||||
cmd += arg_dict[value[1:]]
|
||||
else:
|
||||
cmd += value
|
||||
|
||||
subprocess.run(cmd, shell=True)
|
||||
print(ex["bundle"]["successMessage"])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--export-platform', required=True,
|
||||
help='Export platform name as per export-platforms.json')
|
||||
parser.add_argument('--game', required=True,
|
||||
help='Game name as per dlc-games.json')
|
||||
parser.add_argument('--game-location', required=True,
|
||||
help='Location of game\'s datafiles')
|
||||
parser.add_argument('--binary-location', required=False,
|
||||
help='Location of ScummVM executable (Required for Android)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
arg_dict = {"game_location": args.game_location,
|
||||
"binary_location": args.binary_location}
|
||||
|
||||
f = open('export-platforms.json')
|
||||
export_platforms = json.load(f)
|
||||
|
||||
f = open('dlc-games.json')
|
||||
games = json.load(f)
|
||||
|
||||
if args.export_platform in export_platforms:
|
||||
if args.game in games:
|
||||
print()
|
||||
prepare(export_platforms[args.export_platform],
|
||||
games[args.game], arg_dict)
|
||||
|
||||
bundle(export_platforms[args.export_platform],
|
||||
games[args.game], arg_dict)
|
||||
|
||||
else:
|
||||
print(
|
||||
"Game's metadata not found! Make sure the provided game is in dlc-games.json")
|
||||
else:
|
||||
print("Export platform not found! Make sure the provided export-platform is in export-platforms.json")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
529
devtools/packaging/scripts/android/add_packs.py
Normal file
529
devtools/packaging/scripts/android/add_packs.py
Normal 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()
|
||||
BIN
devtools/packaging/scripts/android/bundletool-all-1.15.1.jar
Normal file
BIN
devtools/packaging/scripts/android/bundletool-all-1.15.1.jar
Normal file
Binary file not shown.
197
devtools/packaging/scripts/android/config.proto
Normal file
197
devtools/packaging/scripts/android/config.proto
Normal 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;
|
||||
}
|
||||
187
devtools/packaging/scripts/android/config_pb2.py
Normal file
187
devtools/packaging/scripts/android/config_pb2.py
Normal 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)
|
||||
176
devtools/packaging/scripts/android/generate_asset_pack.py
Normal file
176
devtools/packaging/scripts/android/generate_asset_pack.py
Normal 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()
|
||||
22
devtools/packaging/scripts/json-validator.py
Normal file
22
devtools/packaging/scripts/json-validator.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from jsonschema import validate
|
||||
import json
|
||||
|
||||
|
||||
def main():
|
||||
f = open('../dlc-metadata-schema.json')
|
||||
schema = json.load(f)
|
||||
|
||||
f = open('../dlc-games.json')
|
||||
games = json.load(f)
|
||||
|
||||
for key in games.keys():
|
||||
game = games[key]
|
||||
validate(game, schema) # we get error if not valid
|
||||
|
||||
print("All games' metadata in dlc-games.json is valid")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user