release: add portable zipapp and installer
This commit is contained in:
parent
e053d31a2f
commit
9a3590ce48
5
.gitignore
vendored
5
.gitignore
vendored
@ -3,4 +3,7 @@ __pycache__/
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/*
|
||||||
|
!dist/
|
||||||
|
!dist/ictlogin.pyz
|
||||||
|
!dist/ictlogin.pyz.sha256
|
||||||
|
|||||||
25
README.md
25
README.md
@ -14,6 +14,31 @@ python3 -m pip install --user --no-deps --no-build-isolation .
|
|||||||
|
|
||||||
如果不想安装,也可以直接运行 `python3 run_ictlogin.py ...`。
|
如果不想安装,也可以直接运行 `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:
|
先检查当前 IP:
|
||||||
|
|||||||
BIN
dist/ictlogin.pyz
vendored
Executable file
BIN
dist/ictlogin.pyz
vendored
Executable file
Binary file not shown.
1
dist/ictlogin.pyz.sha256
vendored
Normal file
1
dist/ictlogin.pyz.sha256
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
b5ea158db1982149800d823482391e6c77c857b6c34a14a9b0f002ed0a8d07fa ictlogin.pyz
|
||||||
164
install.sh
Executable file
164
install.sh
Executable file
@ -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
|
||||||
77
scripts/build_release.py
Executable file
77
scripts/build_release.py
Executable file
@ -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()
|
||||||
Loading…
Reference in New Issue
Block a user