109 lines
4.1 KiB
Python
Executable file
109 lines
4.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Publish a release: copy the artifacts in, write the manifest, commit.
|
|
|
|
Reads the version and build number out of amber-adult's pubspec so the two
|
|
cannot drift — a manifest advertising a build number the APK does not carry is
|
|
an update the device installs and is then offered again on every launch.
|
|
"""
|
|
import hashlib
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
|
|
HERE = pathlib.Path(__file__).resolve().parent
|
|
PUBSPEC = HERE.parent / "amber-adult" / "pubspec.yaml"
|
|
BASE_URL = "https://media-updates.petruzalekr.cz"
|
|
|
|
|
|
def pubspec_version() -> tuple[str, int]:
|
|
m = re.search(r"^version:\s*([0-9.]+)\+([0-9]+)\s*$",
|
|
PUBSPEC.read_text(encoding="utf-8"), re.M)
|
|
if not m:
|
|
sys.exit(f"no version line in {PUBSPEC}")
|
|
return m.group(1), int(m.group(2))
|
|
|
|
|
|
def main() -> None:
|
|
args = sys.argv[1:]
|
|
# Linux is optional and off by default. It is another ~34MB in the
|
|
# repository per release, and the desktop build has no self-updater to feed
|
|
# — it exists so somebody can check a source without walking to the
|
|
# television, and is published when that is worth doing.
|
|
linux_bundle = None
|
|
if "--linux" in args:
|
|
i = args.index("--linux")
|
|
linux_bundle = pathlib.Path(args[i + 1]).resolve()
|
|
del args[i:i + 2]
|
|
if not args:
|
|
sys.exit("usage: release.py <apk> [notes] [--linux <bundle-dir>]")
|
|
apk = pathlib.Path(args[0]).resolve()
|
|
notes = args[1] if len(args) > 1 else ""
|
|
if not apk.is_file():
|
|
sys.exit(f"no such APK: {apk}")
|
|
if linux_bundle is not None and not linux_bundle.is_dir():
|
|
sys.exit(f"no such bundle directory: {linux_bundle}")
|
|
|
|
version, build = pubspec_version()
|
|
public = HERE / "public"
|
|
public.mkdir(exist_ok=True)
|
|
name = f"media-{build}.apk"
|
|
dest = public / name
|
|
shutil.copy2(apk, dest)
|
|
|
|
digest = hashlib.sha256(dest.read_bytes()).hexdigest()
|
|
manifest = {
|
|
"version": version,
|
|
"buildNumber": build,
|
|
"notes": notes,
|
|
"sha256": digest,
|
|
"size": dest.stat().st_size,
|
|
"url": f"{BASE_URL}/{name}",
|
|
}
|
|
# ensure_ascii=False: the notes are Czech and get read by a person as
|
|
# often as by the app.
|
|
(public / "manifest.json").write_text(
|
|
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
|
encoding="utf-8")
|
|
|
|
# A stable name, so "where do I download it" has one permanent answer
|
|
# instead of one that has to be looked up in the manifest each time. A
|
|
# symlink rather than a copy: git stores it as a path, so it costs bytes
|
|
# rather than another 34MB per release.
|
|
latest = public / "latest.apk"
|
|
latest.unlink(missing_ok=True)
|
|
latest.symlink_to(name)
|
|
|
|
if linux_bundle is not None:
|
|
tar_name = f"media-linux-{build}.tar.gz"
|
|
# Renamed on the way in, so unpacking gives a directory somebody can
|
|
# read rather than "bundle".
|
|
packaging = HERE.parent / "amber-adult" / "linux" / "packaging"
|
|
with tarfile.open(public / tar_name, "w:gz") as tar:
|
|
tar.add(linux_bundle, arcname="amber-adult")
|
|
# The desktop entry, its icons and the installer ride along, so the
|
|
# tarball can put itself in the launcher. Without them the app can
|
|
# only be started by path, and on Wayland it has no icon at all —
|
|
# the compositor resolves that from an installed .desktop, never
|
|
# from the running process.
|
|
if packaging.is_dir():
|
|
tar.add(packaging, arcname="amber-adult/packaging")
|
|
tar_latest = public / "latest-linux.tar.gz"
|
|
tar_latest.unlink(missing_ok=True)
|
|
tar_latest.symlink_to(tar_name)
|
|
print(f"staged {tar_name}")
|
|
|
|
subprocess.run(["git", "add", "-A"], cwd=HERE, check=True)
|
|
subprocess.run(
|
|
["git", "commit", "-m", f"Release {version}+{build}"],
|
|
cwd=HERE, check=True)
|
|
print(f"staged {name} ({dest.stat().st_size // 1024 // 1024} MB), "
|
|
f"version {version}+{build}")
|
|
print("push to deploy")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|