fix: ship updatable 1.1.1 source archives
Package a clean shallow main checkout with credential-free Gitea remote, localize the China launcher, and set the new-install Responses endpoint.
This commit is contained in:
1 parent
bfcd828fa8
commit
8522fa3ca3
22 files changed
+445
-140
No files matched your search
+111
-11
@@ -1,10 +1,13 @@
|
||||
"""Create a portable source ZIP without environments, builds, or local credentials."""
|
||||
"""Create a clean, shallow Git checkout ZIP that can fast-forward on launch."""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
from urllib.parse import urlsplit
|
||||
import zipfile
|
||||
|
||||
|
||||
@@ -83,6 +86,9 @@ EXCLUDED_SUFFIXES = {
|
||||
".swo",
|
||||
}
|
||||
|
||||
GIT_METADATA_FILES = {"HEAD", "config", "index", "shallow", "packed-refs"}
|
||||
GIT_METADATA_DIRS = {"objects", "refs"}
|
||||
|
||||
|
||||
def excluded(path: Path) -> bool:
|
||||
parts = tuple(part.lower() for part in path.parts)
|
||||
@@ -116,22 +122,116 @@ def source_files(root: Path):
|
||||
yield path
|
||||
|
||||
|
||||
def git(root: Path, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=90,
|
||||
)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"git {args[0]} failed (exit {result.returncode})")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def git_metadata_files(root: Path):
|
||||
metadata = root / ".git"
|
||||
for name in sorted(GIT_METADATA_FILES):
|
||||
path = metadata / name
|
||||
if path.is_file():
|
||||
yield path
|
||||
for name in sorted(GIT_METADATA_DIRS):
|
||||
directory = metadata / name
|
||||
if directory.exists():
|
||||
yield from source_files_without_filter(directory)
|
||||
for name in ("exclude", "sparse-checkout"):
|
||||
path = metadata / "info" / name
|
||||
if path.is_file():
|
||||
yield path
|
||||
|
||||
|
||||
def source_files_without_filter(root: Path):
|
||||
for directory, dirs, files in os.walk(root, followlinks=False):
|
||||
current = Path(directory)
|
||||
dirs[:] = sorted(name for name in dirs if not (current / name).is_symlink())
|
||||
for name in sorted(files):
|
||||
path = current / name
|
||||
if not path.is_symlink():
|
||||
yield path
|
||||
|
||||
|
||||
def release_remote(root: Path) -> tuple[str, str]:
|
||||
if not (root / ".git").exists():
|
||||
raise ValueError("Source packaging requires a Git checkout")
|
||||
if git(root, "status", "--porcelain", "--untracked-files=no"):
|
||||
raise ValueError("Commit tracked changes before packaging a release")
|
||||
branch = git(root, "symbolic-ref", "--quiet", "--short", "HEAD")
|
||||
remote = git(root, "config", "--get", f"branch.{branch}.remote")
|
||||
merge_ref = git(root, "config", "--get", f"branch.{branch}.merge")
|
||||
if remote == "." or merge_ref != f"refs/heads/{branch}":
|
||||
raise ValueError("The current branch must track the same branch on a remote")
|
||||
url = git(root, "remote", "get-url", remote)
|
||||
parsed = urlsplit(url)
|
||||
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password:
|
||||
raise ValueError("The release upstream must be a credential-free HTTPS URL")
|
||||
if parsed.query or parsed.fragment:
|
||||
raise ValueError("The release upstream must not contain a query or fragment")
|
||||
return branch, url
|
||||
|
||||
|
||||
def package_source(root: Path, output: Path) -> tuple[int, str]:
|
||||
root = root.resolve()
|
||||
output = output.resolve()
|
||||
branch, remote_url = release_remote(root)
|
||||
revision = git(root, "rev-parse", "HEAD")
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = output.with_suffix(".zip.tmp")
|
||||
files = [p for p in source_files(root) if p.resolve() not in {output, temporary}]
|
||||
try:
|
||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for path in files:
|
||||
archive.write(path, Path("artemis") / path.relative_to(root))
|
||||
with zipfile.ZipFile(temporary) as archive:
|
||||
bad_file = archive.testzip()
|
||||
if bad_file:
|
||||
raise RuntimeError(f"ZIP integrity check failed: {bad_file}")
|
||||
if any(excluded(Path(name).relative_to("artemis")) for name in archive.namelist()):
|
||||
raise RuntimeError("An excluded file was found in the source archive")
|
||||
with tempfile.TemporaryDirectory(prefix="artemis-release-") as work:
|
||||
checkout = Path(work) / "artemis"
|
||||
git(
|
||||
root,
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--single-branch",
|
||||
"--branch",
|
||||
branch,
|
||||
root.as_uri(),
|
||||
str(checkout),
|
||||
)
|
||||
git(checkout, "remote", "set-url", "origin", remote_url)
|
||||
# Generated assets are tracked upstream but must not appear in the
|
||||
# unpacked source. Sparse checkout keeps the Git index clean.
|
||||
omitted = [
|
||||
name for name in git(checkout, "ls-files").splitlines() if excluded(Path(name))
|
||||
]
|
||||
patterns = ["/*", *(f"!/{name}" for name in omitted)]
|
||||
git(checkout, "sparse-checkout", "set", "--no-cone", *patterns)
|
||||
# Rebuilt assets in sparse paths must not look like user edits.
|
||||
info_exclude = checkout / ".git" / "info" / "exclude"
|
||||
with info_exclude.open("a", encoding="utf-8") as stream:
|
||||
stream.write("\n".join(["", *(f"/{name}" for name in omitted), ""]))
|
||||
if git(checkout, "rev-parse", "HEAD") != revision or git(
|
||||
checkout, "status", "--porcelain"
|
||||
):
|
||||
raise RuntimeError("The packaged checkout is not clean at the requested revision")
|
||||
files = [*source_files(checkout), *git_metadata_files(checkout)]
|
||||
names = {
|
||||
str(Path("artemis") / path.relative_to(checkout)).replace("\\", "/")
|
||||
for path in files
|
||||
}
|
||||
with zipfile.ZipFile(temporary, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as archive:
|
||||
for path in files:
|
||||
archive.write(path, Path("artemis") / path.relative_to(checkout))
|
||||
with zipfile.ZipFile(temporary) as archive:
|
||||
bad_file = archive.testzip()
|
||||
if bad_file or set(archive.namelist()) != names:
|
||||
raise RuntimeError(
|
||||
f"ZIP integrity check failed: {bad_file or 'file list mismatch'}"
|
||||
)
|
||||
temporary.replace(output)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
Reference in new issue
Block a user