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
4 changes: 4 additions & 0 deletions .github/workflows/verify-split-files.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
34 changes: 28 additions & 6 deletions lib/forge.func
Original file line number Diff line number Diff line change
Expand Up @@ -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 ~/.<app>
#
# Parameters:
Expand Down Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions tests/forge-singlefile-compressed.sh
Original file line number Diff line number Diff line change
@@ -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 ]]
Loading