diff --git a/.github/workflows/verify-split-files.yml b/.github/workflows/verify-split-files.yml index b2280a1..361c586 100644 --- a/.github/workflows/verify-split-files.yml +++ b/.github/workflows/verify-split-files.yml @@ -72,6 +72,7 @@ on: - "tests/db-helper-context.sh" - "tests/cs-clear.sh" - "tests/forge-appversion-scope.sh" + - "tests/forge-singlefile-compressed.sh" - "tests/motd-os-version.sh" - "tests/gpg-key-reason.sh" - "tests/forge-branch-clean-install.sh" @@ -140,6 +141,7 @@ on: - "tests/db-helper-context.sh" - "tests/cs-clear.sh" - "tests/forge-appversion-scope.sh" + - "tests/forge-singlefile-compressed.sh" - "tests/motd-os-version.sh" - "tests/gpg-key-reason.sh" - "tests/forge-branch-clean-install.sh" @@ -299,6 +301,8 @@ jobs: run: bash tests/cs-clear.sh - name: App version pin and .deb stamp tests run: bash tests/forge-appversion-scope.sh + - name: Compressed single binary tests + run: bash tests/forge-singlefile-compressed.sh - name: GPG key failure reason tests run: bash tests/gpg-key-reason.sh - name: Clean install from a git branch tests diff --git a/lib/forge.func b/lib/forge.func index f884325..6f6a67e 100644 --- a/lib/forge.func +++ b/lib/forge.func @@ -2093,7 +2093,8 @@ fetch_and_deploy_codeberg_release() { # - source: Alias for tarball (same behavior) # - binary: .deb package install (arch-dependent) # - prebuild: Prebuilt .tar.gz archive (e.g. Go binaries) -# - singlefile: Standalone binary (no archive, direct chmod +x install) +# - singlefile: Standalone binary (no archive, direct chmod +x install; +# a .gz/.bz2/.xz/.zst single binary is unpacked first) # - Handles download, extraction/installation and version tracking in ~/. # # Parameters: @@ -2507,12 +2508,33 @@ fetch_and_deploy_release() { _deploy_unpacked_archive "$tmpdir/$filename" "$target" "$unpack_tmp" || return else mkdir -p "$target" - local target_file="$app" + local target_file="$app" unpack="" unpack_pkg="" [[ "${USE_ORIGINAL_FILENAME:-false}" == "true" ]] && target_file="$filename" - curl_download "$target/$target_file" "$asset_url" || { - msg_error "Download failed: $asset_url" - return 250 - } + # A compressed single binary lands unpacked; tarballs stay as they are. + case "$filename" in + *.tar.* | *.tgz | *.txz) ;; + *.gz) unpack=gzip unpack_pkg=gzip ;; + *.bz2) unpack=bzip2 unpack_pkg=bzip2 ;; + *.xz) unpack=xz unpack_pkg=xz-utils ;; + *.zst) unpack=zstd unpack_pkg=zstd ;; + esac + if [[ -n "$unpack" ]]; then + [[ "$target_file" == "$filename" ]] && target_file="${filename%.*}" + ensure_dependencies "$unpack_pkg" || return 100 + curl_download "$tmpdir/$filename" "$asset_url" || { + msg_error "Download failed: $asset_url" + return 250 + } + "$unpack" -dc "$tmpdir/$filename" >"$target/$target_file" || { + msg_error "Could not unpack $filename" + return 251 + } + else + curl_download "$target/$target_file" "$asset_url" || { + msg_error "Download failed: $asset_url" + return 250 + } + fi if [[ "$target_file" != *.jar && -f "$target/$target_file" ]]; then chmod +x "$target/$target_file" fi diff --git a/tests/forge-singlefile-compressed.sh b/tests/forge-singlefile-compressed.sh new file mode 100644 index 0000000..0fc6929 --- /dev/null +++ b/tests/forge-singlefile-compressed.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -uo pipefail + +CORE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +eval "$(awk '/^(fetch_and_deploy_release|_forge_workdir|_forge_is_script_app|_forge_descriptor|_forge_strip_v|_forge_previous_version)[(][)] [{]/,/^}/' "$CORE_DIR/lib/forge.func")" + +TEST_DIR="$(mktemp -d)" +trap 'rm -rf -- "$TEST_DIR"' EXIT +export HOME="$TEST_DIR/home" +mkdir -p "$HOME" +LOG="$TEST_DIR/log" +passed=0 failed=0 + +check() { + if [[ "$3" == "$2" ]]; then + echo "PASS $1" + passed=$((passed + 1)) + else + echo "FAIL $1: expected <$2>, got <$3>" >&2 + sed 's/^/ /' "$LOG" >&2 + failed=$((failed + 1)) + fi +} + +# shellcheck disable=SC2034 # read by the eval'd helpers +STD="" +jq() { command jq "$@" | tr -d '\r'; } +msg_info() { :; } +msg_ok() { echo "ok: $*" >>"$LOG"; } +msg_warn() { :; } +msg_error() { echo "error: $*" >>"$LOG"; } +ensure_dependencies() { echo "deps $*" >>"$LOG"; } +_forge_release_json() { printf '{"tag_name":"v1.0.0"}' >"$5"; } +_forge_asset_urls() { echo "https://example.invalid/$ASSET"; } +curl_download() { + local out="$1" + printf '#!/bin/sh\necho program\n' >"$TEST_DIR/plain" + case "$ASSET" in + *.tar.gz) tar -czf "$out" -C "$TEST_DIR" plain ;; + *.gz) gzip -c "$TEST_DIR/plain" >"$out" ;; + *.bz2) bzip2 -c "$TEST_DIR/plain" >"$out" ;; + *.xz) xz -c "$TEST_DIR/plain" >"$out" ;; + *) cp "$TEST_DIR/plain" "$out" ;; + esac +} + +deploy() { + : >"$LOG" + rm -rf "$TEST_DIR/opt" "$HOME"/.[!.]* + fetch_and_deploy_release github "$@" >/dev/null 2>&1 +} +runs() { [[ -x "$1" && "$("$1" 2>/dev/null)" == "program" ]] && echo runs || echo "does not run"; } + +for pair in gz:gzip bz2:bzip2 xz:xz; do + ext="${pair%%:*}" + command -v "${pair##*:}" >/dev/null 2>&1 || continue + ASSET="app-v1.0.0-linux-amd64.$ext" + deploy app owner/app singlefile latest "$TEST_DIR/opt/app" "app-v*-linux-amd64.$ext" + check ".$ext: the program is unpacked and executable" "runs" "$(runs "$TEST_DIR/opt/app/app")" + check ".$ext: no archive is left in the target" "1" "$(find "$TEST_DIR/opt/app" -type f | wc -l | tr -d ' ')" +done + +if command -v bzip2 >/dev/null 2>&1; then + ASSET="restic_0.18.0_linux_amd64.bz2" + USE_ORIGINAL_FILENAME=true deploy restic restic/restic singlefile latest "$TEST_DIR/opt/bin" "restic_*_linux_amd64.bz2" + check "original filename drops the compression suffix" "runs" "$(runs "$TEST_DIR/opt/bin/restic_0.18.0_linux_amd64")" +fi + +ASSET="zulip-server-12.4.tar.gz" +USE_ORIGINAL_FILENAME=true deploy zulip zulip/zulip singlefile latest "$TEST_DIR/opt/zulip" "zulip-server-*.tar.gz" +check "a tarball is kept as downloaded" "yes" "$(tar -tzf "$TEST_DIR/opt/zulip/zulip-server-12.4.tar.gz" >/dev/null 2>&1 && echo yes || echo no)" + +ASSET="app-linux-amd64" +deploy app owner/app singlefile latest "$TEST_DIR/opt/app" "app-linux-amd64" +check "a plain binary is unchanged" "runs" "$(runs "$TEST_DIR/opt/app/app")" +check "and needs no unpacker" "0" "$(grep -c '^deps' "$LOG")" + +echo "${passed} passed, ${failed} failed" +[[ "$failed" -eq 0 ]]