From 9a3590ce48157d1bc3cde13cd68447653c2c1e39 Mon Sep 17 00:00:00 2001 From: roy1994 Date: Sun, 23 Aug 2026 05:25:05 +0800 Subject: [PATCH] release: add portable zipapp and installer --- .gitignore | 5 +- README.md | 25 ++++++ dist/ictlogin.pyz | Bin 0 -> 11914 bytes dist/ictlogin.pyz.sha256 | 1 + install.sh | 164 +++++++++++++++++++++++++++++++++++++++ scripts/build_release.py | 77 ++++++++++++++++++ 6 files changed, 271 insertions(+), 1 deletion(-) create mode 100755 dist/ictlogin.pyz create mode 100644 dist/ictlogin.pyz.sha256 create mode 100755 install.sh create mode 100755 scripts/build_release.py diff --git a/.gitignore b/.gitignore index ec27f1c..f3f9a02 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ __pycache__/ .ruff_cache/ *.egg-info/ build/ -dist/ +dist/* +!dist/ +!dist/ictlogin.pyz +!dist/ictlogin.pyz.sha256 diff --git a/README.md b/README.md index d04da36..1a92e2b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,31 @@ python3 -m pip install --user --no-deps --no-build-isolation . 如果不想安装,也可以直接运行 `python3 run_ictlogin.py ...`。 +## 单文件发布 + +构建可直接复制到其他 Linux 服务器的 zipapp: + +```bash +python3 scripts/build_release.py +./dist/ictlogin.pyz --version +(cd dist && sha256sum -c ictlogin.pyz.sha256) +``` + +在包含 `install.sh` 和 `dist/ictlogin.pyz` 的目录中执行: + +```bash +./install.sh +``` + +安装脚本默认写入 `~/.local/bin/ictlogin`,会自动校验同目录的 `.sha256` 文件。也可以从指定文件或 URL 安装: + +```bash +./install.sh --source /path/to/ictlogin.pyz --prefix "$HOME/.local/bin" +ICTLOGIN_PYZ_URL="https://example.invalid/ictlogin.pyz" ./install.sh +``` + +当前 Gitea 仓库是私有仓库;在没有仓库访问凭据的服务器上,请先通过 `scp` 复制 `install.sh`、`dist/ictlogin.pyz` 和 `dist/ictlogin.pyz.sha256`,再运行安装脚本。 + ## 使用 先检查当前 IP: diff --git a/dist/ictlogin.pyz b/dist/ictlogin.pyz new file mode 100755 index 0000000000000000000000000000000000000000..ac072a07c40f685b4b6c4a277001a98972851651 GIT binary patch literal 11914 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 ?znM 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 HAfW#T=Q~le literal 0 HcmV?d00001 diff --git a/dist/ictlogin.pyz.sha256 b/dist/ictlogin.pyz.sha256 new file mode 100644 index 0000000..610f35f --- /dev/null +++ b/dist/ictlogin.pyz.sha256 @@ -0,0 +1 @@ +b5ea158db1982149800d823482391e6c77c857b6c34a14a9b0f002ed0a8d07fa ictlogin.pyz diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b97175b --- /dev/null +++ b/install.sh @@ -0,0 +1,164 @@ +#!/bin/sh +# Install the portable ICT login zipapp for the current user. + +set -eu + +usage() { + cat <<'EOF' +Usage: install.sh [options] + +Options: + -s, --source PATH|URL install this .pyz file + -p, --prefix DIR install directory (default: $HOME/.local/bin) + --sha256 HEX expected SHA-256 of the .pyz + -h, --help show this help + +Environment overrides: + ICTLOGIN_PYZ local .pyz path or URL + ICTLOGIN_PYZ_URL download URL when no local artifact is present + ICTLOGIN_PYZ_SHA256 expected SHA-256 + ICTLOGIN_PREFIX install directory + ICTLOGIN_PYTHON Python 3 executable +EOF +} + +SOURCE="${ICTLOGIN_PYZ:-}" +URL="${ICTLOGIN_PYZ_URL:-}" +EXPECTED_SHA256="${ICTLOGIN_PYZ_SHA256:-}" +PREFIX="${ICTLOGIN_PREFIX:-${HOME}/.local/bin}" + +while [ "$#" -gt 0 ]; do + case "$1" in + -s|--source) + [ "$#" -ge 2 ] || { echo "--source requires a value" >&2; exit 2; } + SOURCE=$2 + shift 2 + ;; + -p|--prefix) + [ "$#" -ge 2 ] || { echo "--prefix requires a value" >&2; exit 2; } + PREFIX=$2 + shift 2 + ;; + --sha256) + [ "$#" -ge 2 ] || { echo "--sha256 requires a value" >&2; exit 2; } + EXPECTED_SHA256=$2 + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +PYTHON="${ICTLOGIN_PYTHON:-}" +if [ -z "$PYTHON" ]; then + PYTHON=$(command -v python3 2>/dev/null || true) +fi +if [ -z "$PYTHON" ]; then + PYTHON=$(command -v python 2>/dev/null || true) +fi +if [ -z "$PYTHON" ]; then + echo "Python 3 is required" >&2 + exit 1 +fi + +if ! "$PYTHON" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 8) else 1)' 2>/dev/null; then + echo "Python 3.8 or newer is required: $PYTHON" >&2 + exit 1 +fi + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +if [ -z "$SOURCE" ] && [ -z "$URL" ]; then + if [ -f "$SCRIPT_DIR/dist/ictlogin.pyz" ]; then + SOURCE=$SCRIPT_DIR/dist/ictlogin.pyz + elif [ -f "$SCRIPT_DIR/ictlogin.pyz" ]; then + SOURCE=$SCRIPT_DIR/ictlogin.pyz + else + URL="https://gitx.roy1994.top/mancitrus/ictlogin/raw/branch/main/dist/ictlogin.pyz" + fi +fi + +SOURCE_PATH="" +case "$SOURCE" in + http://*|https://*) URL=$SOURCE; SOURCE="" ;; + "") ;; + *) + SOURCE_PATH=$SOURCE + [ -f "$SOURCE_PATH" ] || { echo "file not found: $SOURCE_PATH" >&2; exit 1; } + ;; +esac + +TMP_FILE=$(mktemp "${TMPDIR:-/tmp}/ictlogin.pyz.XXXXXX") +cleanup() { + rm -f "$TMP_FILE" +} +trap cleanup EXIT HUP INT TERM + +if [ -n "$URL" ]; then + if command -v curl >/dev/null 2>&1; then + curl --fail --location --silent --show-error -o "$TMP_FILE" "$URL" + elif command -v wget >/dev/null 2>&1; then + wget --quiet --output-document="$TMP_FILE" "$URL" + else + echo "curl or wget is required to download $URL" >&2 + exit 1 + fi +else + cp "$SOURCE_PATH" "$TMP_FILE" +fi + +if [ -z "$EXPECTED_SHA256" ] && [ -n "$SOURCE_PATH" ] && [ -f "${SOURCE_PATH}.sha256" ]; then + EXPECTED_SHA256=$(awk 'NF { print $1; exit }' "${SOURCE_PATH}.sha256") +fi + +sha256_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + "$PYTHON" - "$1" <<'PY' +import hashlib +import sys + +digest = hashlib.sha256() +with open(sys.argv[1], "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) +print(digest.hexdigest()) +PY + fi +} + +if [ -n "$EXPECTED_SHA256" ]; then + ACTUAL_SHA256=$(sha256_file "$TMP_FILE") + if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then + echo "SHA-256 verification failed" >&2 + echo "expected: $EXPECTED_SHA256" >&2 + echo "actual: $ACTUAL_SHA256" >&2 + exit 1 + fi +fi + +"$PYTHON" "$TMP_FILE" --version >/dev/null +mkdir -p "$PREFIX" +TARGET="$PREFIX/ictlogin" +if command -v install >/dev/null 2>&1; then + install -m 0755 "$TMP_FILE" "$TARGET" +else + cp "$TMP_FILE" "$TARGET" + chmod 0755 "$TARGET" +fi + +echo "installed $TARGET" +case ":${PATH:-}:" in + *":$PREFIX:"*) ;; + *) echo "add to PATH: export PATH=\"$PREFIX:\$PATH\"" ;; +esac +"$TARGET" --version diff --git a/scripts/build_release.py b/scripts/build_release.py new file mode 100755 index 0000000..86ec703 --- /dev/null +++ b/scripts/build_release.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Build the dependency-free ICT login zipapp and its checksum.""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import shutil +import tempfile +from pathlib import Path +from zipapp import create_archive + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_OUTPUT = ROOT / "dist" / "ictlogin.pyz" + + +def _copy_source(source: Path, target: Path) -> None: + target.mkdir(parents=True, exist_ok=True) + shutil.copytree( + source, + target / "ictlogin", + ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo", "*.egg-info"), + ) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def build(output: Path, checksum_path: Path) -> None: + output.parent.mkdir(parents=True, exist_ok=True) + staging_root = Path(tempfile.mkdtemp(prefix="ictlogin-build-")) + temporary_output = output.with_name(output.name + ".tmp") + try: + _copy_source(ROOT / "src" / "ictlogin", staging_root) + create_archive( + staging_root, + temporary_output, + interpreter="/usr/bin/env python3", + main="ictlogin.cli:main", + compressed=True, + ) + os.chmod(temporary_output, 0o755) + os.replace(temporary_output, output) + digest = _sha256(output) + checksum_path.parent.mkdir(parents=True, exist_ok=True) + checksum_path.write_text(f"{digest} {output.name}\n", encoding="ascii") + os.chmod(checksum_path, 0o644) + finally: + if temporary_output.exists(): + temporary_output.unlink() + shutil.rmtree(staging_root, ignore_errors=True) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--output", type=Path, default=DEFAULT_OUTPUT, help="output .pyz path" + ) + parser.add_argument( + "--checksum", type=Path, help="checksum path (defaults to OUTPUT.sha256)" + ) + args = parser.parse_args() + checksum_path = args.checksum or Path(str(args.output) + ".sha256") + build(args.output.resolve(), checksum_path.resolve()) + print(f"built {args.output}") + print(f"sha256 {checksum_path}") + + +if __name__ == "__main__": + main()