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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Bootstrap scripts
name: Shell scripts

on:
push:
Expand All @@ -7,8 +7,12 @@ on:
- 'scripts/bootstrap.ps1'
- 'scripts/git-repository.sh'
- 'scripts/git-repository.ps1'
- 'scripts/tests/bootstrap-git-repository.sh'
- '.github/workflows/ci-bootstrap-scripts.yml'
- 'scripts/install-linux.sh'
- 'scripts/install-linux-legacy.sh'
- 'scripts/package-linux.sh'
- 'scripts/package-linux.ps1'
- 'scripts/tests/**'
- '.github/workflows/ci-scripts.yml'
pull_request:
paths: *script_paths

Expand All @@ -26,3 +30,13 @@ jobs:

- name: bootstrap-git-test
run: make bootstrap-git-test

installer-staging:
name: Linux installer stages without a throwaway archive
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4

- name: install-linux-staging test
run: bash scripts/tests/install-linux-staging.sh
47 changes: 47 additions & 0 deletions Optimum.Tests/installer-release-coverage-tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,53 @@ public void OptimumOwnedRuntimeAssembliesEmitPortableSymbols(string relativePath
Assert.Contains("<DebugSymbols>true</DebugSymbols>", project);
}

[Fact]
public void LinuxInstallerStagesWithoutABuiltThenDeletedArchive()
{
// Regression guard: install-linux.sh ran package-linux.sh in its
// default targz mode into a temp directory, then copied only the
// staged folder into place and deleted the tar.gz unread. On a small
// or tmpfs /tmp that unused ~600 MB archive stalled the install before
// any files reached the target (issue #23 follow-up, install onto
// /mnt/zoomin). The installer now requests the folder alone, and the
// temp tree is removed by an EXIT trap set only for direct execution.
foreach (string relativePath in new[] { "scripts/install-linux.sh", "scripts/install-linux-legacy.sh" })
{
string installer = Read(relativePath);
Assert.Contains("--format none", installer);
Assert.Contains("trap cleanup_stage EXIT", installer);
Assert.DoesNotContain("rm -rf \"$(dirname \"$temp_source\")\"", installer);
}

string packager = Read("scripts/package-linux.sh");
Assert.Contains("\"$FORMAT\" != \"none\"", packager);
Assert.Contains("Skipping archive (--format none)", packager);

string powershellPackager = Read("scripts/package-linux.ps1");
Assert.Contains("'targz', 'zip', 'none'", powershellPackager);
Assert.Contains("$Format -eq 'none'", powershellPackager);
}

[Fact]
public void LinuxInstallerStagingShellTestPasses()
{
if (!OperatingSystem.IsLinux())
{
return;
}

string script = PatchReader.FindRepositoryFile("scripts/tests/install-linux-staging.sh");
using Process process = Process.Start(new ProcessStartInfo("bash", script)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
})!;
process.WaitForExit();

Assert.True(process.ExitCode == 0, process.StandardOutput.ReadToEnd() + process.StandardError.ReadToEnd());
}

private static string Match(string source, string pattern)
{
System.Text.RegularExpressions.Match match = Regex.Match(source, pattern);
Expand Down
33 changes: 20 additions & 13 deletions scripts/install-linux-legacy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ CREATE_MENU=1
CREATE_DESKTOP=0
INTERACTIVE=1

# Scratch directory for the staged package, removed on exit (see the run guard
# at the end of the file, which installs the trap only for direct execution).
STAGE_ROOT=""
cleanup_stage() {
if [[ -n "$STAGE_ROOT" && -d "$STAGE_ROOT" ]]; then
rm -rf "$STAGE_ROOT"
fi
return 0
}

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
Expand Down Expand Up @@ -580,17 +590,19 @@ build_and_package() {
fi

log "Packaging Linux build..."
local stage_root
stage_root="$(mktemp -d)"
local pkg_args=(--output "$stage_root")
STAGE_ROOT="$(mktemp -d)"
# --format none: we copy the staged folder into place below, so the archive
# step only builds a tar.gz we delete unread. On a small or tmpfs /tmp that
# extra ~600 MB has stalled the install before any files land.
local pkg_args=(--output "$STAGE_ROOT" --format none)
if [[ -n "$VERSION" ]]; then pkg_args+=(--version "$VERSION"); fi

bash "$SCRIPT_DIR/package-linux.sh" "${pkg_args[@]}"

local built_dir
built_dir="$(find "$stage_root" -maxdepth 1 -type d -name 'Optimum-v*-linux-x64' | sort | tail -n 1)"
built_dir="$(find "$STAGE_ROOT" -maxdepth 1 -type d -name 'Optimum-v*-linux-x64' | sort | tail -n 1)"
if [[ -z "$built_dir" ]]; then
die "Linux package folder not found under $stage_root"
die "Linux package folder not found under $STAGE_ROOT"
fi
BUILT_DIR="$built_dir"
}
Expand Down Expand Up @@ -730,23 +742,17 @@ main() {

# Step 4: Build or use existing package
local source_dir="$PACKAGE_DIR"
local temp_source=""

if [[ -z "$source_dir" ]]; then
BUILT_DIR=""
build_and_package
temp_source="$BUILT_DIR"
source_dir="$temp_source"
source_dir="$BUILT_DIR"
fi

# Step 5: Install
# Step 5: Install (the staged copy is removed by cleanup_stage on exit)
install_from_dir "$source_dir"
write_launcher

if [[ -n "$temp_source" ]]; then
rm -rf "$(dirname "$temp_source")"
fi

# Step 6: Icon
local icon_dir="$XDG_DATA_HOME/icons/hicolor/256x256/apps"
mkdir -p "$icon_dir"
Expand Down Expand Up @@ -789,5 +795,6 @@ main() {
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
trap cleanup_stage EXIT
main
fi
33 changes: 20 additions & 13 deletions scripts/install-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ CREATE_MENU=1
CREATE_DESKTOP=0
INTERACTIVE=1

# Scratch directory for the staged package, removed on exit (see the run guard
# at the end of the file, which installs the trap only for direct execution).
STAGE_ROOT=""
cleanup_stage() {
if [[ -n "$STAGE_ROOT" && -d "$STAGE_ROOT" ]]; then
rm -rf "$STAGE_ROOT"
fi
return 0
}

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
Expand Down Expand Up @@ -707,17 +717,19 @@ build_and_package() {
fi

log "Packaging Linux build..."
local stage_root
stage_root="$(mktemp -d)"
local pkg_args=(--output "$stage_root")
STAGE_ROOT="$(mktemp -d)"
# --format none: we copy the staged folder into place below, so the archive
# step only builds a tar.gz we delete unread. On a small or tmpfs /tmp that
# extra ~600 MB has stalled the install before any files land.
local pkg_args=(--output "$STAGE_ROOT" --format none)
if [[ -n "$VERSION" ]]; then pkg_args+=(--version "$VERSION"); fi

bash "$SCRIPT_DIR/package-linux.sh" "${pkg_args[@]}"

local built_dir
built_dir="$(find "$stage_root" -maxdepth 1 -type d -name 'Optimum-v*-linux-x64' | sort | tail -n 1)"
built_dir="$(find "$STAGE_ROOT" -maxdepth 1 -type d -name 'Optimum-v*-linux-x64' | sort | tail -n 1)"
if [[ -z "$built_dir" ]]; then
die "Linux package folder not found under $stage_root"
die "Linux package folder not found under $STAGE_ROOT"
fi
BUILT_DIR="$built_dir"
}
Expand Down Expand Up @@ -861,23 +873,17 @@ main() {

# Step 4: Build or use existing package
local source_dir="$PACKAGE_DIR"
local temp_source=""

if [[ -z "$source_dir" ]]; then
BUILT_DIR=""
build_and_package
temp_source="$BUILT_DIR"
source_dir="$temp_source"
source_dir="$BUILT_DIR"
fi

# Step 5: Install
# Step 5: Install (the staged copy is removed by cleanup_stage on exit)
install_from_dir "$source_dir"
write_launcher

if [[ -n "$temp_source" ]]; then
rm -rf "$(dirname "$temp_source")"
fi

# Step 6: Icon
local icon_dir="$XDG_DATA_HOME/icons/hicolor/256x256/apps"
mkdir -p "$icon_dir"
Expand Down Expand Up @@ -923,5 +929,6 @@ main() {
}

if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
trap cleanup_stage EXIT
main
fi
29 changes: 17 additions & 12 deletions scripts/package-linux.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ executable bit on the Optimum launcher; extract-then-`chmod +x Optimum` fixes it
Where to write the package. Default: repo root.

.PARAMETER Format
Archive format: targz (default) or zip.
Archive format: targz (default), zip, or none. Use none to stop after the
staged folder and skip the archive.

.PARAMETER Version
Vintage Story version. Default: 1.22.7.
Expand All @@ -28,7 +29,7 @@ pwsh ./scripts/package-linux.ps1 -Format zip -OutputDir /mnt/d/Downloads
[CmdletBinding()]
param(
[string]$OutputDir,
[ValidateSet('targz', 'zip')]
[ValidateSet('targz', 'zip', 'none')]
[string]$Format = 'targz',
[string]$Version,
[string]$ClientArchive
Expand Down Expand Up @@ -197,18 +198,22 @@ try {
Write-Host "Folder ready: $stageDir" -ForegroundColor Green

# 7. Package.
if ($Format -eq 'zip') {
$out = Join-Path $OutputDir "$name.zip"
if (Test-Path $out) { Remove-Item -Force $out }
Compress-Archive -Path $stageDir -DestinationPath $out -CompressionLevel Optimal
if ($Format -eq 'none') {
Write-Host "Skipping archive (-Format none): $stageDir" -ForegroundColor Green
} else {
$out = Join-Path $OutputDir "$name.tar.gz"
if (Test-Path $out) { Remove-Item -Force $out }
Invoke-NativeStep { tar -czf $out -C $OutputDir $name }
if ($LASTEXITCODE -ne 0) { throw "tar packaging failed for $out" }
if ($Format -eq 'zip') {
$out = Join-Path $OutputDir "$name.zip"
if (Test-Path $out) { Remove-Item -Force $out }
Compress-Archive -Path $stageDir -DestinationPath $out -CompressionLevel Optimal
} else {
$out = Join-Path $OutputDir "$name.tar.gz"
if (Test-Path $out) { Remove-Item -Force $out }
Invoke-NativeStep { tar -czf $out -C $OutputDir $name }
if ($LASTEXITCODE -ne 0) { throw "tar packaging failed for $out" }
}
$size = [math]::Round((Get-Item $out).Length / 1MB)
Write-Host "Done: $out (${size}MB)" -ForegroundColor Green
}
$size = [math]::Round((Get-Item $out).Length / 1MB)
Write-Host "Done: $out (${size}MB)" -ForegroundColor Green
} finally {
Pop-Location
}
12 changes: 9 additions & 3 deletions scripts/package-linux.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#!/usr/bin/env bash
# Builds a ready-to-run Optimum package for Linux (x64). Downloads the official
# Vintage Story Linux client, overlays the optimized DLLs, renames the launcher
# to Optimum, and packages as tar.gz (default), zip, or AppImage.
# to Optimum, and packages as tar.gz (default), zip, or AppImage. Pass
# --format none to stop after the staged folder and skip the archive, which is
# what the installer wants (it copies the folder into place, it does not need a
# tarball it would only delete).
# Requires a successful build first (dotnet build VintageStory.slnx -c Release).
#
# Usage:
Expand Down Expand Up @@ -39,8 +42,8 @@ while [[ $# -gt 0 ]]; do
esac
done

if [[ "$FORMAT" != "targz" && "$FORMAT" != "zip" && "$FORMAT" != "appimage" ]]; then
echo "Error: --format must be 'targz', 'zip', or 'appimage'" >&2
if [[ "$FORMAT" != "targz" && "$FORMAT" != "zip" && "$FORMAT" != "appimage" && "$FORMAT" != "none" ]]; then
echo "Error: --format must be 'targz', 'zip', 'appimage', or 'none'" >&2
exit 1
fi

Expand Down Expand Up @@ -437,6 +440,9 @@ DESKTOP
}

case "$FORMAT" in
none)
echo "Skipping archive (--format none): $STAGE_DIR"
;;
zip)
OUT="$OUTPUT_DIR/${NAME}.zip"
rm -f "$OUT"
Expand Down
57 changes: 57 additions & 0 deletions scripts/tests/install-linux-staging.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Verifies the Linux installer stages the package without building an archive
# it never uses. Regression cover for the issue #23 follow-up: an install onto
# /mnt/zoomin stalled while package-linux.sh built a throwaway tar.gz in a
# small /tmp, after the staged folder was already complete.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
TEST_ROOT="$(mktemp -d)"
trap 'rm -rf "$TEST_ROOT"' EXIT

# --- package-linux.sh --format validation ------------------------------------

if bash "$REPO_ROOT/scripts/package-linux.sh" --format bogus >/dev/null 2>"$TEST_ROOT/fmt-err"; then
echo 'package-linux.sh accepted an invalid --format' >&2
exit 1
fi
grep -Fq "targz" "$TEST_ROOT/fmt-err"
grep -Fq "none" "$TEST_ROOT/fmt-err"

# The 'none' arm must sit before the catch-all so it does not fall through to
# the tar.gz default.
none_line="$(grep -n '^\s*none)' "$REPO_ROOT/scripts/package-linux.sh" | head -1 | cut -d: -f1)"
star_line="$(grep -n '^\s*\*)' "$REPO_ROOT/scripts/package-linux.sh" | tail -1 | cut -d: -f1)"
[[ -n "$none_line" && -n "$star_line" && "$none_line" -lt "$star_line" ]]

# --- install-linux.sh / install-linux-legacy.sh cleanup ---------------------

for installer in install-linux.sh install-linux-legacy.sh; do
( # subshell: sourcing runs the file's top level, not main
trap - EXIT # drop the inherited cleanup trap; test only what sourcing adds
# shellcheck disable=SC1090
source "$REPO_ROOT/scripts/$installer"

# cleanup_stage is a no-op and succeeds when nothing was staged
# (set -e above fails the test if it returns non-zero).
STAGE_ROOT=""
cleanup_stage

# It removes the staged tree when one exists.
STAGE_ROOT="$TEST_ROOT/stage-$installer"
mkdir -p "$STAGE_ROOT/inner"
cleanup_stage
[[ ! -e "$STAGE_ROOT" ]]

# Sourcing must not install an EXIT trap (that would clobber the trap
# of any script, such as the prerequisite test, that sources this one).
[[ -z "$(trap -p EXIT)" ]]

# The installer asks package-linux.sh for the folder only.
grep -Fq -- '--format none' "$REPO_ROOT/scripts/$installer"
)
done

echo 'Linux installer staging test passed.'
Loading