Init: base
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# prebuilt-app-maker config
|
||||
#
|
||||
# Run:
|
||||
# python prebuilt-app-maker.py # download all apps
|
||||
# python prebuilt-app-maker.py --app Cromite # single app
|
||||
# python prebuilt-app-maker.py --git-init # also commit on version branch
|
||||
# python prebuilt-app-maker.py --output /path/repos # custom base output dir
|
||||
#
|
||||
# Each app becomes its own repo (prebuilt_{Name}) with the version as the branch:
|
||||
# <project path="prebuilts/apps/Cromite"
|
||||
# name="PawletOS/prebuilt_Cromite"
|
||||
# revision="134.0.6998.170" />
|
||||
#
|
||||
# If an app has system_props, a {Name}.mk is generated alongside Android.bp.
|
||||
# Include it from your device tree or vendor common.mk:
|
||||
# $(call inherit-product-if-exists, $(LOCAL_PATH)/../../prebuilts/apps/Cromite/Cromite.mk)
|
||||
|
||||
defaults:
|
||||
product_specific: true
|
||||
privileged: false
|
||||
dex_preopt: false
|
||||
optional_uses_libs:
|
||||
- androidx.window.extensions
|
||||
- androidx.window.sidecar
|
||||
# system_props: {} # global props applied to every app (usually empty)
|
||||
# product_packages_extras: [] # extra PRODUCT_PACKAGES entries per app
|
||||
|
||||
apps:
|
||||
|
||||
# ── Cromite (browser + system WebView) ────────────────────────────────────
|
||||
# Cromite can serve as both a browser and the system WebView provider.
|
||||
# To activate as WebView, system_props generates Cromite.mk — include it
|
||||
# from your device tree or vendor common.mk:
|
||||
# $(call inherit-product-if-exists, $(LOCAL_PATH)/../../prebuilts/apps/Cromite/Cromite.mk)
|
||||
# - name: Cromite
|
||||
# source:
|
||||
# type: github
|
||||
# owner: uazo
|
||||
# repo: cromite
|
||||
# asset: "arm64_*.apk" # Pi 4/5 are arm64
|
||||
# system_props:
|
||||
# ro.webkit.package.name: org.cromite.cromite
|
||||
# android_bp:
|
||||
# optional_uses_libs: [] # Cromite manages its own deps
|
||||
|
||||
# ── Material Files (file manager) ─────────────────────────────────────────
|
||||
- name: MaterialFiles
|
||||
source:
|
||||
type: fdroid
|
||||
repo: https://www.f-droid.org/repo
|
||||
package_id: me.zhanghai.android.files
|
||||
|
||||
# ── Example: privileged system app with a prop ────────────────────────────
|
||||
# - name: MyPrivilegedApp
|
||||
# source:
|
||||
# type: github
|
||||
# owner: example
|
||||
# repo: myapp
|
||||
# asset: "app-release.apk"
|
||||
# android_bp:
|
||||
# privileged: true
|
||||
# system_props:
|
||||
# ro.myapp.enabled: "1"
|
||||
|
||||
# ── Example: app that adds extra PRODUCT_PACKAGES ─────────────────────────
|
||||
# - name: SomeApp
|
||||
# source:
|
||||
# type: github
|
||||
# owner: example
|
||||
# repo: someapp
|
||||
# asset: "app-arm64.apk"
|
||||
# product_packages_extras:
|
||||
# - SomeAppHelper
|
||||
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
prebuilt-app-maker — Download APKs and generate Android.bp prebuilt app repos.
|
||||
|
||||
Each app defined in config.yml becomes a directory containing the APK plus a
|
||||
generated Android.bp with an android_app_import module, ready to be pushed as
|
||||
a prebuilt app repo for AOSP local manifest inclusion:
|
||||
|
||||
<project path="prebuilts/apps/Cromite"
|
||||
name="PawletOS/prebuilt_Cromite"
|
||||
revision="134.0.6998.170" />
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_config(path: str) -> dict:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sources
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _fetch(url: str) -> requests.Response:
|
||||
r = requests.get(url, timeout=30)
|
||||
r.raise_for_status()
|
||||
return r
|
||||
|
||||
|
||||
def resolve_fdroid(source: dict) -> tuple:
|
||||
"""Return (version_name, version_code, download_url) from an F-Droid repo."""
|
||||
from xml.dom import minidom, pulldom
|
||||
|
||||
repo = source["repo"].rstrip("/")
|
||||
pkg = source["package_id"]
|
||||
index_url = f"{repo}/index.xml"
|
||||
|
||||
def text(el, tag):
|
||||
return el.getElementsByTagName(tag).item(0).firstChild.data
|
||||
|
||||
r = _fetch(index_url)
|
||||
doc = pulldom.parseString(r.text)
|
||||
for event, node in doc:
|
||||
if event == pulldom.START_ELEMENT and node.tagName == "application":
|
||||
if node.getAttribute("id") == pkg:
|
||||
doc.expandNode(node)
|
||||
vercode = text(node, "marketvercode")
|
||||
for p in node.getElementsByTagName("package"):
|
||||
if text(p, "versioncode") == vercode:
|
||||
return (
|
||||
text(p, "version"),
|
||||
int(vercode),
|
||||
f"{repo}/{text(p, 'apkname')}",
|
||||
)
|
||||
raise RuntimeError(f"Package {pkg} not found in {repo}")
|
||||
|
||||
|
||||
def resolve_github(source: dict) -> tuple:
|
||||
"""
|
||||
Return (version_name, version_code, download_url) from a GitHub release.
|
||||
|
||||
asset: glob pattern to pick the right APK (e.g. "arm64_*.apk").
|
||||
version_code is the GitHub release ID — monotonically increasing.
|
||||
"""
|
||||
owner = source["owner"]
|
||||
repo = source["repo"]
|
||||
asset = source.get("asset", "*.apk")
|
||||
|
||||
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
|
||||
data = _fetch(url).json()
|
||||
|
||||
matched = [a for a in data.get("assets", []) if fnmatch.fnmatch(a["name"], asset)]
|
||||
if not matched:
|
||||
raise RuntimeError(f"No asset matching '{asset}' in {owner}/{repo} latest release")
|
||||
|
||||
return (
|
||||
data["tag_name"].lstrip("v"),
|
||||
int(data["id"]),
|
||||
matched[0]["browser_download_url"],
|
||||
)
|
||||
|
||||
|
||||
def resolve_source(source: dict) -> tuple:
|
||||
kind = source["type"]
|
||||
if kind == "fdroid":
|
||||
return resolve_fdroid(source)
|
||||
if kind == "github":
|
||||
return resolve_github(source)
|
||||
raise ValueError(f"Unknown source type: {kind}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Android.bp generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate_android_bp(app: dict, apk_filename: str, defaults: dict) -> str:
|
||||
name = app["name"]
|
||||
bp_cfg = app.get("android_bp", {})
|
||||
privileged = bp_cfg.get("privileged", defaults.get("privileged", False))
|
||||
prod_spec = bp_cfg.get("product_specific", defaults.get("product_specific", True))
|
||||
dex_preopt = bp_cfg.get("dex_preopt", defaults.get("dex_preopt", False))
|
||||
uses_libs = bp_cfg.get("optional_uses_libs", defaults.get("optional_uses_libs", [
|
||||
"androidx.window.extensions",
|
||||
"androidx.window.sidecar",
|
||||
]))
|
||||
comment = bp_cfg.get("comment")
|
||||
|
||||
lines = []
|
||||
if comment:
|
||||
for c in comment.strip().splitlines():
|
||||
lines.append(f"// {c}")
|
||||
|
||||
lines.append("android_app_import {")
|
||||
lines.append(f' name: "{name}",')
|
||||
lines.append(f' apk: "{apk_filename}",')
|
||||
lines.append( ' presigned: true,')
|
||||
if privileged:
|
||||
lines.append(" privileged: true,")
|
||||
lines.append( ' dex_preopt: {')
|
||||
lines.append(f' enabled: {"true" if dex_preopt else "false"},')
|
||||
lines.append( ' },')
|
||||
if prod_spec:
|
||||
lines.append(" product_specific: true,")
|
||||
if uses_libs:
|
||||
lines.append(" optional_uses_libs: [")
|
||||
for lib in uses_libs:
|
||||
lines.append(f' "{lib}",')
|
||||
lines.append(" ],")
|
||||
lines.append("}")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# {Name}.mk generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def generate_mk(app: dict, defaults: dict) -> str | None:
|
||||
"""Return makefile content if system_props or product_packages_extras are set, else None."""
|
||||
props = {**defaults.get("system_props", {}), **app.get("system_props", {})}
|
||||
pkgs = defaults.get("product_packages_extras", []) + app.get("product_packages_extras", [])
|
||||
|
||||
if not props and not pkgs:
|
||||
return None
|
||||
|
||||
lines = [f"# Generated by prebuilt-app-maker — do not edit manually"]
|
||||
if props:
|
||||
pairs = [f" {k}={v}" for k, v in props.items()]
|
||||
lines.append("PRODUCT_SYSTEM_PROPERTIES += \\")
|
||||
lines.append(" \\\n".join(pairs))
|
||||
if pkgs:
|
||||
lines.append("PRODUCT_PACKAGES += \\")
|
||||
lines.append(" \\\n".join(f" {p}" for p in pkgs))
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Git
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _git(cwd: Path, *args: str) -> str:
|
||||
r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git {args[0]}: {r.stderr.strip()}")
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
def git_commit(output_dir: Path, app_name: str, version: str) -> None:
|
||||
if not (output_dir / ".git").exists():
|
||||
_git(output_dir, "init")
|
||||
|
||||
branches = _git(output_dir, "branch", "--list", version)
|
||||
if branches:
|
||||
_git(output_dir, "checkout", version)
|
||||
else:
|
||||
_git(output_dir, "checkout", "-b", version)
|
||||
|
||||
_git(output_dir, "add", ".")
|
||||
status = _git(output_dir, "status", "--porcelain")
|
||||
if status:
|
||||
_git(output_dir, "commit", "-m", f"{app_name} {version}")
|
||||
print(f" committed on branch '{version}'")
|
||||
else:
|
||||
print(f" branch '{version}' already up to date")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Core
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def process_app(app: dict, defaults: dict, git_init: bool, base_output: Path) -> None:
|
||||
name = app["name"]
|
||||
output_dir = Path(app.get("output_dir", base_output / name))
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"\n[{name}]")
|
||||
|
||||
version_name, version_code, download_url = resolve_source(app["source"])
|
||||
print(f" version: {version_name}")
|
||||
|
||||
apk_filename = f"{name}.apk"
|
||||
apk_path = output_dir / apk_filename
|
||||
print(f" downloading {download_url}")
|
||||
urllib.request.urlretrieve(download_url, apk_path)
|
||||
print(f" saved {apk_filename}")
|
||||
|
||||
bp = generate_android_bp(app, apk_filename, defaults)
|
||||
(output_dir / "Android.bp").write_text(bp, encoding="utf-8")
|
||||
print(f" wrote Android.bp")
|
||||
|
||||
mk = generate_mk(app, defaults)
|
||||
if mk:
|
||||
mk_path = output_dir / f"{name}.mk"
|
||||
mk_path.write_text(mk, encoding="utf-8")
|
||||
print(f" wrote {name}.mk (include from device tree to apply system props)")
|
||||
|
||||
if git_init:
|
||||
git_commit(output_dir, name, version_name)
|
||||
|
||||
print(f" output → {output_dir}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Download APKs and generate AOSP prebuilt app repos.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__,
|
||||
)
|
||||
parser.add_argument("--config", default="config.yml", help="Config file (default: config.yml)")
|
||||
parser.add_argument("--app", action="append", metavar="NAME", help="Process only this app (repeatable)")
|
||||
parser.add_argument("--git-init", action="store_true", help="Init git repo and commit on a version-named branch")
|
||||
parser.add_argument("--output", default="./output", help="Base output directory (default: ./output)")
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config(args.config)
|
||||
defaults = config.get("defaults", {})
|
||||
apps = config.get("apps", [])
|
||||
base_output = Path(args.output)
|
||||
filter_apps = set(args.app) if args.app else None
|
||||
|
||||
failed = []
|
||||
for app in apps:
|
||||
if filter_apps and app["name"] not in filter_apps:
|
||||
continue
|
||||
try:
|
||||
process_app(app, defaults, args.git_init, base_output)
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}", file=sys.stderr)
|
||||
failed.append(app["name"])
|
||||
|
||||
print()
|
||||
if failed:
|
||||
print(f"Failed: {', '.join(failed)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,2 @@
|
||||
requests>=2.28.0
|
||||
PyYAML>=6.0
|
||||
Reference in New Issue
Block a user