From ae37e0be530a73377346d3aedb784b52956ab07a Mon Sep 17 00:00:00 2001 From: ericek111 Date: Sat, 24 Oct 2026 01:07:47 +0000 Subject: [PATCH] Write an update archive with only the files changed since the last publish Co-Authored-By: Claude Sonnet 5.5 --- publish-update.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++ publish.sh | 34 ++++++++++++++++----- 2 files changed, 104 insertions(+), 7 deletions(-) create mode 100755 publish-update.py diff --git a/publish-update.py b/publish-update.py new file mode 100755 index 0000000..2739850 --- /dev/null +++ b/publish-update.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Archives the files of a publish folder that differ from the previous archive. + +usage: publish-update.py + +The previous archive is the full one the last publish left behind (.zip or +.tar.gz). A file goes into the output when it is new or its content changed; +files the new publish no longer has are listed but cannot be removed by +unpacking over the old install. The archive keeps the folder name as its top +directory, like the full one. Prints nothing and writes nothing when no file +changed. +""" +import hashlib +import os +import sys +import tarfile +import zipfile + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def read_previous(path): + """Maps each file's path inside the archive to a hash of its content.""" + hashes = {} + if path.endswith(".zip"): + with zipfile.ZipFile(path) as archive: + for info in archive.infolist(): + if not info.is_dir(): + hashes[info.filename] = digest(archive.read(info)) + else: + with tarfile.open(path) as archive: + for member in archive.getmembers(): + if member.isfile(): + hashes[member.name] = digest(archive.extractfile(member).read()) + return hashes + + +def main(previous, folder, output): + root = os.path.basename(os.path.normpath(folder)) + old = read_previous(previous) + changed = [] + current = set() + for directory, _, names in os.walk(folder): + for name in sorted(names): + full = os.path.join(directory, name) + inside = root + "/" + os.path.relpath(full, folder).replace(os.sep, "/") + current.add(inside) + with open(full, "rb") as file: + if old.get(inside) != digest(file.read()): + changed.append((full, inside)) + removed = sorted(set(old) - current) + + if os.path.exists(output): + os.remove(output) + if not changed: + print("no file changed since the previous archive") + return + changed.sort(key=lambda pair: pair[1]) + if output.endswith(".zip"): + with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive: + for full, inside in changed: + archive.write(full, inside) + else: + with tarfile.open(output, "w:gz") as archive: + for full, inside in changed: + archive.add(full, inside, recursive=False) + print(f"{output}: {len(changed)} changed of {len(current)} files") + for gone in removed: + print(f" no longer in the publish: {gone}") + + +if __name__ == "__main__": + if len(sys.argv) != 4: + sys.exit(__doc__) + main(*sys.argv[1:]) diff --git a/publish.sh b/publish.sh index a85d084..e581d08 100755 --- a/publish.sh +++ b/publish.sh @@ -5,7 +5,9 @@ # files, so the second publish only adds what the first lacks. The target # machine needs no .NET install. Use publish-windows.sh or publish-linux.sh, or # pass the runtime id, for example win-arm64 (the bridge and XMMT.ocx are 32 -# bit x86 either way). +# bit x86 either way). When an archive from an earlier run exists, a second +# archive, dist/nonemm--update, holds only the files that are new or +# changed since then, for unpacking over the old install. rid="${1:?usage: publish.sh }" cd "$(dirname "$0")" || exit 1 name="nonemm-$rid" @@ -13,20 +15,38 @@ out="dist/$name" mkdir -p dist rm -rf "$out" +case "$rid" in + win-*) extension=zip ;; + *) extension=tar.gz ;; +esac +archive="dist/$name.$extension" +update="dist/$name-update.$extension" + +# the full archive is replaced below, so keep the old one to compare against +previous="" +if [ -f "$archive" ]; then + previous="dist/$name.previous.$extension" + cp "$archive" "$previous" || exit 1 +fi + for project in Nonemm.App Nonemm.Recorder.App; do ./build.sh publish "src/$project" -c Release -r "$rid" --self-contained -o "$out" || exit 1 done rm -f "$out"/*.pdb -case "$rid" in - win-*) - rm -f "dist/$name.zip" +case "$extension" in + zip) python3 -c 'import shutil, sys; shutil.make_archive(sys.argv[1], "zip", "dist", sys.argv[2])' \ "$out" "$name" || exit 1 - echo "dist/$name.zip" ;; *) - tar -C dist -czf "dist/$name.tar.gz" "$name" || exit 1 - echo "dist/$name.tar.gz" + tar -C dist -czf "$archive" "$name" || exit 1 ;; esac +echo "$archive" + +rm -f "$update" +if [ -n "$previous" ]; then + python3 "$(dirname "$0")/publish-update.py" "$previous" "$out" "$update" || exit 1 + rm -f "$previous" +fi