Files
IronStorage/.gitea/scripts/android_notices.py
T
hugo bf83a0ba41
Android ARM verification / rust (push) Failing after 49m41s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Failing after 22s
Prepare Android release artifacts and listing (#112)
Add Rust distribution auditing, pinned post-build AAB signing, complete offline third-party notices, an Android privacy policy and reviewable Play listing assets. Preserve the application ID and audit both ARM ABIs, native symbols, baseline profiles and R8 mapping.

Fix dialog focus ownership, navigation-triggered cleanup loops and stale authentication errors after first-run setup, with native and real-activity regressions.

Google Play publication is deferred by the user. Issue #112 remains open for production signing, Console declarations and actual Play install/update verification.
2026-10-15 16:16:01 +02:00

104 lines
5.4 KiB
Python

#!/usr/bin/env python3
"""Build-time license text; never reads app data or a password-store repository."""
import json
from pathlib import Path
import platform
import re
import subprocess
import sys
import tarfile
import zipfile
from android_ci import ROOT, CARGO, download
def rust_notices(data, expected):
covered = set()
sections = []
for license in data["licenses"]:
if not license["text"].strip():
raise RuntimeError("Empty Rust license text")
users = []
for used in license["used_by"]:
crate = used["crate"]
key = (crate["name"], crate["version"])
covered.add(key)
users.append(f"{key[0]} {key[1]} — {', '.join(crate['authors'])}\n"
f"https://crates.io/crates/{key[0]}/{key[1]} (source archive)\n"
f"{crate.get('repository') or ''}")
sections.append(license["name"] + "\n" + "\n".join(sorted(users)) + "\n\n" + license["text"])
if expected - covered:
raise RuntimeError("Rust normal dependency is missing a license notice: " + repr(sorted(expected - covered)))
return "\n\n".join(sorted(set(sections)))
def android_notices(rows, policy):
common = (ROOT / "android/licenses/Apache-2.0.txt").read_text()
extras = {
"androidx.camera:camera-core:1.6.2": ("Apache-2.0 AND BSD-3-Clause", ["libyuv-LICENSE.txt", "libyuv-PATENTS.txt"]),
"net.java.dev.jna:jna:5.19.1": ("Apache-2.0 OR LGPL-2.1-or-later", ["jna-LICENSE.txt", "libffi.txt"]),
"org.checkerframework:checker-qual:3.43.0": ("MIT", ["checker-qual.txt"]),
"rustls:rustls-platform-verifier:0.1.1": ("MIT OR Apache-2.0", []),
}
sections = ["Apache License 2.0 (selected where a dual license permits it)\n\n" + common]
for row in sorted(rows, key=lambda r: r["component"]):
component = row["component"]
license = policy.get(component)
if license is None or (license != "Apache-2.0" and extras.get(component, (None,))[0] != license):
raise RuntimeError("Android notice needs review: " + component)
notices = []
with zipfile.ZipFile(row["path"]) as archive:
for name in sorted(archive.namelist()):
if re.search(r"(?i)(?:^|/)(?:licen[cs]e|notice|copying|copyright|al2\.0)(?:[._-].*)?$", name):
entry = archive.getinfo(name)
if entry.is_dir() or entry.file_size > 1024 * 1024:
raise RuntimeError("Unexpected Android notice archive entry")
notices.append(archive.read(name).decode("utf-8"))
for name in extras.get(component, (None, []))[1]:
notices.append((ROOT / "android/licenses" / name).read_text())
sections.append(component + "\nLicense: " + license + "\n\n" + "\n\n".join(sorted(set(notices))))
return "\n\n".join(dict.fromkeys(sections))
def generate(inventory, output):
if platform.machine() not in ("arm64", "aarch64"):
raise RuntimeError("License tools require the supported native ARM build host")
scratch = ROOT / "android/build/license-tools"
scratch.mkdir(parents=True, exist_ok=True)
archive_path = scratch / "cargo-about.tar.gz"
spec = json.loads((ROOT / "android/license-tool.json").read_text())[platform.system()]
download(spec, archive_path)
with tarfile.open(archive_path) as archive:
members = [m for m in archive.getmembers() if Path(m.name).name == "cargo-about" and m.isfile()]
if len(members) != 1 or members[0].size > 100 * 1024 * 1024:
raise RuntimeError("Unexpected cargo-about package")
binary = scratch / "cargo-about"
binary.write_bytes(archive.extractfile(members[0]).read())
binary.chmod(0o755)
data = json.loads(subprocess.check_output([str(binary), "generate", "--locked", "--fail", "--manifest-path",
str(ROOT / "crates/android/Cargo.toml"), "--config",
str(ROOT / "android/about.toml"), "--format", "json"]))
expected = set()
workspace = json.loads(subprocess.check_output([CARGO, "metadata", "--locked", "--no-deps", "--format-version", "1"]))
local_names = {package["name"] for package in workspace["packages"] if package["source"] is None}
for triple in ("aarch64-linux-android", "armv7-linux-androideabi"):
tree = subprocess.check_output([CARGO, "tree", "--locked", "-p", "ironstorage-android", "--target", triple,
"--edges", "normal", "--prefix", "none", "--format", "{p}"], text=True)
for line in tree.splitlines():
match = re.match(r"([^ ]+) v([^ ]+)", line)
if match and match[1] not in local_names:
expected.add((match[1], match[2]))
rows = json.loads(inventory.read_text())
if not rows:
raise RuntimeError("Android release notice inventory is empty")
text = "IronStorage — Third-party notices\n\nAndroid release libraries\n\n"
text += android_notices(rows, json.loads((ROOT / "android/dependency-policy.json").read_text())["android"])
text += "\n\nRust ARM libraries\n\n" + rust_notices(data, expected)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(text, encoding="utf-8")
print(f"Packaged full notices for {len({r['component'] for r in rows})} Android and {len(expected)} Rust components")
if __name__ == "__main__":
generate(Path(sys.argv[1]), Path(sys.argv[2]))