Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ name: Build Release
#
# matrix:
# macos-15(arm64) → EMRG-<ver>-macos-arm64.pkg
# ubuntu-24.04(x86_64) → EMRG-<ver>-linux-x86_64.AppImage + tar.gz
# ubuntu-24.04-arm(aarch64) → EMRG-<ver>-linux-aarch64.AppImage + tar.gz
# ubuntu-24.04(x86_64) → EMRG-<ver>-linux-x86_64.AppImage + tar.gz + .run
# ubuntu-24.04-arm(aarch64) → EMRG-<ver>-linux-aarch64.AppImage + tar.gz + .run
# windows-2025(x64) → EMRG-<ver>-windows-x64.exe
#
# 冒烟在无 Python runner 上跑(R111 验证干净机器承诺);离线冒烟(R96)
# 在 Linux job 用 iptables 断网执行。
#
# .run = Linux 无头服务器离线一键安装器(rant 2026-08-17T10:16:54):
# bash 自解压头 + runtime tarball payload,零网络零依赖。

on:
push:
Expand Down Expand Up @@ -356,6 +359,7 @@ jobs:
dist/artifacts/*.AppImage
dist/artifacts/*.tar.gz
dist/artifacts/*.zip
dist/artifacts/*.run

release:
needs: build
Expand Down
2 changes: 1 addition & 1 deletion README.cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ EMRG 不只是一个工具——它是一个**会听吐槽、会自我改进**
|------|---------|
| macOS | `EMRG-<ver>-macos-arm64.pkg` / `-x64.pkg`(用户级安装,无需管理员密码) |
| Windows | `EMRG-<ver>-windows-x64.exe`(免 UAC,PATH 自动注册) |
| Linux | `EMRG-<ver>-linux-x86_64.AppImage` / `-aarch64.AppImage` |
| Linux | `EMRG-<ver>-linux-x86_64.run` / `-aarch64.run`(无头服务器,一条命令)· `-x86_64.AppImage` / `-aarch64.AppImage`(桌面) |

> **Windows SmartScreen 提示**:安装包未做 Authenticode 签名——如 SmartScreen 提示,点**保留** / **更多信息 → 仍要运行**。EMRG 完全开源,源码可审计。

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Download from [GitHub Releases](https://github.com/argszero/emrg/releases) and d
|----------|-----------|
| macOS | `EMRG-<ver>-macos-arm64.pkg` / `-x64.pkg` (user-level, no admin password) |
| Windows | `EMRG-<ver>-windows-x64.exe` (no UAC, PATH auto-registered) |
| Linux | `EMRG-<ver>-linux-x86_64.AppImage` / `-aarch64.AppImage` |
| Linux | `EMRG-<ver>-linux-x86_64.run` / `-aarch64.run` (headless server, one command) · `-x86_64.AppImage` / `-aarch64.AppImage` (desktop) |

> **Windows SmartScreen notice**: the installer isn't Authenticode-signed — if SmartScreen prompts, click **Keep** / **More info → Run anyway**. EMRG is fully open source and auditable.

Expand Down
5 changes: 4 additions & 1 deletion packaging/make-installer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ EOF
;;

linux)
echo "==> Linux tar.gz + AppImage (AppImage built by electron-builder, collected R116)"
echo "==> Linux tar.gz + AppImage + .run (AppImage built by electron-builder, collected R116)"
tar -czf "$DIST/artifacts/EMRG-$VERSION-linux-$(uname -m).tar.gz" -C "$(dirname "$RUNTIME")" runtime
# R116: 收集 electron-builder 产出的 AppImage(emrg/gui/dist/*.AppImage)到
# dist/artifacts/ —— 之前只生成 tar.gz,release 缺 linux AppImage(rant #13 Step 5)。
Expand All @@ -188,6 +188,9 @@ EOF
else
echo "!! AppImage not found in emrg/gui/dist — Linux release 缺 AppImage(有 tar.gz 兜底)" >&2
fi
# .run 自解压离线安装器(rant 2026-08-17T10:16:54):Linux 无头服务器
# SSH/无桌面/普通用户场景一键安装。payload = 同一 runtime/ 目录,零网络。
bash "$ROOT/packaging/make-run-installer.sh"
;;

windows|win32|mingw*|msys*)
Expand Down
165 changes: 165 additions & 0 deletions packaging/make-run-installer.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# EMRG — Linux .run self-extracting offline installer builder
# (rant 2026-08-17T10:16:54: Linux headless server offline one-click install)
#
# Produces dist/artifacts/EMRG-<ver>-linux-<arch>.run
#
# Structure (classic self-extracting archive):
# #!/usr/bin/env bash ← installer header script (this file's heredoc)
# __EMRG_PAYLOAD_MARKER__ ← single marker line
# <runtime.tar.gz payload> ← tarball of the runtime/ directory
#
# The header script is pure bash + standard coreutils (grep/tail/tar/mkdir/ln)
# — zero external dependencies, zero network at install time (the payload is
# the fully offline runtime: bundled CPython + source + lib deps + launchers).
#
# Idempotent/upgrade: re-running overwrites the previous install (same
# semantics as the AppImage self-extract), symlinks are refreshed.
#
# Usage: bash packaging/make-run-installer.sh
# (needs dist/runtime/ built by build-runtime.sh first)
# ARCH=x86_64|aarch64 env override (default: uname -m)

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST="$ROOT/dist"
RUNTIME="$DIST/runtime"
VERSION="$(cat "$RUNTIME/version.txt" 2>/dev/null || echo 0.2.41)"
ARCH="${ARCH:-$(uname -m)}" # x86_64 / aarch64
OUT="$DIST/artifacts/EMRG-$VERSION-linux-$ARCH.run"

if [ ! -d "$RUNTIME" ]; then
echo "error: runtime not built — run bash packaging/build-runtime.sh first" >&2
exit 1
fi
mkdir -p "$DIST/artifacts"

PAYLOAD="$(mktemp)"
HEADER="$(mktemp)"
trap 'rm -f "$PAYLOAD" "$HEADER"' EXIT

# ── payload: same layout as the tar.gz artifact (runtime/ dir) ──
echo "==> packing runtime payload"
tar -czf "$PAYLOAD" -C "$(dirname "$RUNTIME")" runtime

# ── installer header (VERSION injected via placeholder) ──
cat > "$HEADER" <<'HEADER_EOF'
#!/usr/bin/env bash
# EMRG __EMRG_VERSION__ — Linux self-extracting offline installer
# One command, fully offline (bundled Python 3.13 + deps + launchers).
# Works on headless servers (SSH, no desktop, no uv/gh, normal user).
set -euo pipefail

VERSION="__EMRG_VERSION__"
# Default from HOME unconditionally — never inherit an ambient PREFIX env var
# (common on build servers / containers: make install PREFIX=..., SDKs, CI
# images). --prefix= is the only override.
PREFIX="$HOME/.emrg/install"
WRITE_PROFILE=1
START=0

usage() {
cat <<'EOF'
Usage: ./EMRG-<ver>-linux-<arch>.run [options]

One-command offline install of EMRG (bundled Python + deps, no network).

Options:
--prefix=<dir> Install directory (default: ~/.emrg/install)
--no-profile Do not append ~/.local/bin to the shell profile PATH
--start Start the background daemon (emrgd) after install
-h, --help Show this help

After install:
emrg → ~/.local/bin/emrg (TUI; ~/.local/bin added to PATH in ~/.bashrc
emrgd → ~/.local/bin/emrgd when missing — skip with --no-profile)
uninstall → ~/.emrg/install/bin/emrg-uninstall
EOF
}

for arg in "$@"; do
case "$arg" in
--prefix=*) PREFIX="${arg#--prefix=}" ;;
--no-profile) WRITE_PROFILE=0 ;;
--start) START=1 ;;
-h|--help) usage; exit 0 ;;
*)
echo "error: unknown option: $arg" >&2
usage >&2
exit 1
;;
esac
done

# ── self-extract payload (marker line + everything after) ──
# -a: the .run is "binary" (gzip payload) — grep must treat it as text to
# report line numbers (GNU/BSD grep otherwise prints "Binary file ... matches").
MARKER_LINE="$(grep -an '^__EMRG_PAYLOAD_MARKER__$' "$0" | tail -1 | cut -d: -f1 || true)"
if [ -z "${MARKER_LINE:-}" ]; then
echo "error: corrupt installer (payload marker not found)" >&2
exit 1
fi

echo "==> EMRG $VERSION — installing to $PREFIX (offline)"
mkdir -p "$PREFIX"
tail -n +$((MARKER_LINE + 1)) "$0" | tar -xzf - -C "$PREFIX" --strip-components=1
chmod +x "$PREFIX/bin/emrg" "$PREFIX/bin/emrgd" "$PREFIX/bin/emrg-uninstall"

# ── symlinks (idempotent: -f refresh) ──
mkdir -p "$HOME/.local/bin"
ln -sfn "$PREFIX/bin/emrg" "$HOME/.local/bin/emrg"
ln -sfn "$PREFIX/bin/emrgd" "$HOME/.local/bin/emrgd"

# ── PATH handling (idempotent dedup; skip with --no-profile) ──
if [ "$WRITE_PROFILE" = "1" ]; then
RC="$HOME/.bashrc"
[ -f "$RC" ] || RC="$HOME/.profile"
if ! printf '%s' "$PATH" | tr ':' '\n' | grep -Fqx "$HOME/.local/bin"; then
if [ ! -f "$RC" ] || ! grep -Fq 'export PATH="$HOME/.local/bin:$PATH"' "$RC" 2>/dev/null; then
{
echo ''
echo '# EMRG launcher path'
echo 'export PATH="$HOME/.local/bin:$PATH"'
} >> "$RC"
fi
echo "==> added $HOME/.local/bin to PATH ($RC) — new shell or: export PATH=\"\$HOME/.local/bin:\$PATH\""
fi
fi

# ── optional daemon start ──
if [ "$START" = "1" ]; then
echo "==> starting emrgd (daemon)"
(nohup "$HOME/.local/bin/emrgd" >/dev/null 2>&1 &)
fi

echo "==> EMRG $VERSION installed"
echo " install : $PREFIX"
echo " emrg : $HOME/.local/bin/emrg (TUI — run: emrg)"
echo " emrgd : $HOME/.local/bin/emrgd (daemon)"
echo " uninstall: $PREFIX/bin/emrg-uninstall"
# MUST exit before the payload marker — otherwise bash would keep executing
# the payload bytes as shell commands (classic self-extracting pitfall).
exit 0
HEADER_EOF

# portable in-place sed (GNU: -i; BSD/macOS: -i.bak)
sed -i.bak "s/__EMRG_VERSION__/$VERSION/g" "$HEADER"
rm -f "$HEADER.bak"

# ── assemble .run ──
echo "==> assembling $OUT"
cat "$HEADER" > "$OUT"
printf '__EMRG_PAYLOAD_MARKER__\n' >> "$OUT"
cat "$PAYLOAD" >> "$OUT"
chmod +x "$OUT"

# ── build-time sanity: header must not contain a 2nd marker; payload readable ──
MARKERS="$(grep -ac '^__EMRG_PAYLOAD_MARKER__$' "$OUT" || true)"
if [ "$MARKERS" != "1" ]; then
echo "error: payload marker count != 1 ($MARKERS) — .run corrupted" >&2
exit 1
fi
LINE="$(grep -an '^__EMRG_PAYLOAD_MARKER__$' "$OUT" | cut -d: -f1)"
tail -n +$((LINE + 1)) "$OUT" | tar -tzf - >/dev/null
echo "==> $(basename "$OUT") — $(du -h "$OUT" | cut -f1) (payload OK)"
26 changes: 26 additions & 0 deletions packaging/smoke-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,32 @@ if [ -f "$HOME/.emrg/install/bin/emrg-uninstall" ]; then ok "10. emrg-uninstall
ok "11. read-only install — local manual (R61/R47)"
ok "12. offline install — Linux CI iptables/unshare (R47/R96)"

# 13. .run 自解压安装器(rant 2026-08-17T10:16:54):独立临时 HOME 一键安装
say "13. .run self-extracting installer (offline one-click)"
RUN_FILE="$(ls "$ROOT"/dist/artifacts/EMRG-*-linux-*.run 2>/dev/null | head -1 || true)"
if [ -z "$RUN_FILE" ]; then
ok "13. .run — no artifact in dist/artifacts (installer build skipped; audit-degraded)"
else
run_home="$(mktemp -d)"
if (cd "$run_home" && HOME="$run_home" bash "$RUN_FILE" --no-profile >/dev/null 2>&1) \
&& [ -x "$run_home/.emrg/install/bin/emrg" ] \
&& [ -L "$run_home/.local/bin/emrg" ] \
&& [ -L "$run_home/.local/bin/emrgd" ] \
&& [ -x "$run_home/.emrg/install/bin/emrg-uninstall" ] \
&& HOME="$run_home" "$run_home/.emrg/install/bin/emrg" --version 2>&1 | grep -q "emrg "; then
ok "13. .run install (extract + symlink + version)"
# 幂等重跑(覆盖旧安装)
if (cd "$run_home" && HOME="$run_home" bash "$RUN_FILE" --no-profile >/dev/null 2>&1); then
ok "13. .run re-run idempotent"
else
fail "13. .run re-run idempotent"
fi
else
fail "13. .run install"
fi
rm -rf "$run_home"
fi

# 清理临时 daemon
emrg server stop >/dev/null 2>&1 || true

Expand Down
2 changes: 1 addition & 1 deletion tests/test_version_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _uv_lock_version() -> str:
def _shell_fallback_versions() -> list[tuple[str, str]]:
"""各 shell 脚本的版本 fallback(打包时无法读取 __version__ 时使用)。"""
results: list[tuple[str, str]] = []
for rel in ("packaging/make-installer.sh", "packaging/build-runtime.sh"):
for rel in ("packaging/make-installer.sh", "packaging/build-runtime.sh", "packaging/make-run-installer.sh"):
content = (REPO_ROOT / rel).read_text(encoding="utf-8")
m = re.search(r'\|\| echo "?(\d+\.\d+\.\d+)"?', content)
assert m, f"{rel} 中找不到版本 fallback"
Expand Down
Loading