Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
fc1175a
docs: add Avalonia installer implementation plan
Zaldaryon Aug 27, 2026
f843411
docs: record decisions on the four installer open questions
Zaldaryon Aug 27, 2026
98b6a91
docs: consent posture C, click-through required for decompilation
Zaldaryon Aug 27, 2026
a7dbc3e
build: scaffold the Avalonia installer projects (Phase 0)
Zaldaryon Aug 27, 2026
06d9236
feat(installer): Core prerequisites, path guards, NDJSON, consent (Ph…
Zaldaryon Aug 27, 2026
930c7fd
fix(installer): drain probe stdout and stderr concurrently
Zaldaryon Aug 27, 2026
0d6e7dc
fix(installer): tighten Core detection and path guard after review
Zaldaryon Aug 27, 2026
500b1a0
feat(installer): the seven Optimum.Cli verbs and the build driver (Ph…
Zaldaryon Aug 27, 2026
87620d0
fix(installer): close Phase 2 review findings
Zaldaryon Aug 27, 2026
b8b1db9
feat(installer): the Avalonia wizard GUI (Phase 3)
Zaldaryon Aug 27, 2026
8a2082e
fix(installer): close Phase 3 GUI review findings
Zaldaryon Aug 27, 2026
b93285f
feat(installer): transactional deploy, shortcuts, registered uninstal…
Zaldaryon Aug 27, 2026
cb69291
fix(installer): close Phase 4 transactional-install review findings
Zaldaryon Aug 27, 2026
663ec1a
feat(installer): Velopack packaging and self-update (Phase 5)
Zaldaryon Aug 27, 2026
569abd8
chore(installer): tidy the release workflow version resolution
Zaldaryon Aug 27, 2026
94785c2
fix(installer): close Phase 5 review findings
Zaldaryon Aug 27, 2026
39e56bc
docs: installer license scope, README, CONTRIBUTING; fix uninstall.sh…
Zaldaryon Aug 27, 2026
6431813
feat(installer): acquire source outside a checkout
Zaldaryon Aug 27, 2026
7478b79
fix(installer): wire appimagetool installation
Zaldaryon Aug 27, 2026
ed11c9c
wip(installer): usability rework checkpoint
Zaldaryon Aug 27, 2026
58018f9
feat(installer): finish the usability rework with the Strata design
Zaldaryon Aug 28, 2026
c83b43f
fix(installer): make prerequisite detection and the build driver Wind…
Zaldaryon Aug 28, 2026
68d3f66
fix(installer): centre button labels and stop the display heading cli…
Zaldaryon Aug 28, 2026
035ae33
feat(installer): executors for the in-place SDK and ilspycmd acquisition
Zaldaryon Aug 28, 2026
4fab3c9
fix(installer): wire prerequisite actions by kind and scroll the row …
Zaldaryon Aug 28, 2026
a181709
fix(installer): grayscale text AA and optically centre button labels
Zaldaryon Aug 28, 2026
f7d03a8
feat(installer): adopt SukiUI for the theme, window, and controls
Zaldaryon Aug 28, 2026
77b81d9
feat(installer): bundle Figtree, Fraunces and Geist Mono; 16px a11y t…
Zaldaryon Aug 28, 2026
cf33aca
fix(installer): bundled fonts fell back to Segoe UI on Windows
Zaldaryon Aug 28, 2026
d8ec326
feat(installer): accessibility-first type scale and larger window
Zaldaryon Aug 28, 2026
2c4320d
feat(installer): heavier body text and monospace tool summary
Zaldaryon Aug 28, 2026
1aeaee5
fix(installer): stop the type scale from stretching the window chrome…
Zaldaryon Aug 28, 2026
361117e
refactor(installer): rebuild every view on stock SukiUI patterns
Zaldaryon Aug 28, 2026
75b65e8
feat(installer): switch fonts to Lexend and JetBrains Mono
Zaldaryon Aug 28, 2026
bdab6f2
fix(installer): render the ready-tools summary in JetBrains Mono
Zaldaryon Aug 28, 2026
3bd5b05
fix(installer): raise body text to 15px and fix the caption contrast
Zaldaryon Aug 28, 2026
5879b13
fix(installer): replace WaveProgress with a vector ring; fix heading …
Zaldaryon Aug 28, 2026
d4bb34d
fix(installer): show the validation error inline under the install field
Zaldaryon Aug 28, 2026
249a171
fix(installer): rebuild the completion screen top-aligned with a real…
Zaldaryon Aug 28, 2026
5d01c21
fix(installer): give the path fields a persistent visible label
Zaldaryon Aug 28, 2026
0f86b1e
refactor(installer): drop GroupBox-inside-GlassCard, use an h5 header
Zaldaryon Aug 28, 2026
8f36e6f
fix(installer): mark every step done on the rail after a successful i…
Zaldaryon Aug 28, 2026
5481f72
feat(installer): honour the OS reduce-motion preference
Zaldaryon Aug 28, 2026
8b43a45
fix(installer): give the build log the full height instead of a colla…
Zaldaryon Aug 28, 2026
781881a
style(installer): treat the step counter as an eyebrow (13px, tracked)
Zaldaryon Aug 28, 2026
78005a8
fix(installer): static step rail so the label stops jumping
Zaldaryon Aug 28, 2026
e2ef77c
feat(installer): theme the installer in the Optimum brand colours
Zaldaryon Aug 28, 2026
c57eb8e
fix(installer): colour log lines by content, not by which stream they…
Zaldaryon Aug 28, 2026
7e646a5
feat(installer): recess the build-notice and build-log panes for cont…
Zaldaryon Aug 28, 2026
77f285c
fix(installer): the Options step fits without a scrollbar with every …
Zaldaryon Aug 28, 2026
5e17030
fix(installer): keep routine compiler and SDK warnings out of the vis…
Zaldaryon Aug 28, 2026
b0a0352
feat(installer): Launch Optimum spins, locks, and closes the installer
Zaldaryon Aug 28, 2026
5d36611
fix(installer): balance the progress card, one centred text block
Zaldaryon Aug 28, 2026
4662e4b
fix(installer): centre the step rail so its side gaps match
Zaldaryon Aug 28, 2026
e388aba
feat(installer): a faint grain over the window so it is not a flat co…
Zaldaryon Aug 28, 2026
da6a3b2
fix(installer): opaque cards so the window grain does not bleed throu…
Zaldaryon Aug 28, 2026
beb74c1
fix(installer): tint the opaque card fill to the window surface, not …
Zaldaryon Aug 28, 2026
8be4233
feat(installer): cyan theme throughout
Zaldaryon Aug 28, 2026
197a3ac
ci(installer): name release artifacts Optimum-v<version>-<rid>-Instal…
Zaldaryon Aug 28, 2026
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
6 changes: 6 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
"commands": [
"ilspycmd"
]
},
"vpk": {
"version": "1.2.0",
"commands": [
"vpk"
]
}
}
}
118 changes: 118 additions & 0 deletions .github/workflows/ci-installer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Installer CI

on:
push:
paths: &installer_paths
- 'Optimum.Bootstrap.Core/**'
- 'Optimum.Bootstrap.Core.Tests/**'
- 'Optimum.Cli/**'
- 'Optimum.Cli.Tests/**'
- 'Optimum.Installer/**'
- 'Optimum.Installer.Tests/**'
- 'Optimum.Installer.slnf'
- 'Directory.Build.props'
- 'Directory.Build.targets'
- 'global.json'
- 'NuGet.config'
- '.github/workflows/ci-installer.yml'
pull_request:
paths: *installer_paths

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-test:
name: Build and test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: nuget-installer-${{ hashFiles('Optimum.Bootstrap.Core/**/*.csproj', 'Optimum.Cli/**/*.csproj', 'Optimum.Installer/**/*.csproj', 'Optimum.*.Tests/**/*.csproj') }}
restore-keys: |
nuget-installer-

- name: Test
run: dotnet test Optimum.Installer.slnf -c Release --nologo

cli-contract:
name: CLI contract
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Build the CLI
run: dotnet build Optimum.Cli/Optimum.Cli.csproj -c Release --nologo

- name: Verbs answer and the consent gate holds
run: |
set -euo pipefail
cli() { dotnet exec Optimum.Cli/bin/Release/net10.0/optimum.dll "$@"; }

test "$(cli --version)" = "$(cat VERSION)"

cli capabilities --json | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['pinnedVersion']"
cli preflight --json | python3 -c "import json,sys; d=json.load(sys.stdin); assert any(x['id']=='Dotnet' for x in d)"

# build without --acknowledge-decompile must refuse and still emit a terminal result.
set +e
cli build --json --output /tmp/should-not-build > stream.ndjson
code=$?
set -e
test "$code" -eq 2
python3 scripts/check-ndjson-stream.py < stream.ndjson
python3 -c "import json; r=json.loads(open('stream.ndjson').read().splitlines()[-1]); assert r['reason']=='bad-input', r"
test ! -e /tmp/should-not-build

velopack-smoke:
name: Velopack on .NET 10
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Restore local tools
run: dotnet tool restore

- name: Pack the real installer, then a delta
run: |
set -euo pipefail
rel=$(mktemp -d)

dotnet publish Optimum.Installer/Optimum.Installer.csproj -c Release -r linux-x64 \
--self-contained -o publish/linux-x64 --nologo
dotnet vpk pack -u Optimum.Installer -v 0.0.1 -p publish/linux-x64 \
-e Optimum.Installer --channel linux -o "$rel"

# A no-op change so the second pack produces a real delta.
date > publish/linux-x64/velopack-smoke-marker.txt
dotnet vpk pack -u Optimum.Installer -v 0.0.2 -p publish/linux-x64 \
-e Optimum.Installer --channel linux -o "$rel"

ls -la "$rel"
test -f "$rel"/Optimum.Installer-0.0.2-linux-delta.nupkg
test -f "$rel"/Optimum.Installer-0.0.2-linux-full.nupkg
test -f "$rel"/Optimum.Installer.AppImage
echo "Velopack packs the real Optimum.Installer for linux-x64 and builds a delta."
17 changes: 16 additions & 1 deletion .github/workflows/ci-platform-bootstrap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ jobs:
bootstrap-linux:
name: Bootstrap linux
runs-on: ubuntu-24.04
timeout-minutes: 45
timeout-minutes: 60
if: inputs.platform == 'all' || inputs.platform == 'linux'
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -389,6 +389,20 @@ jobs:
shell: bash
run: dotnet test Optimum.Launcher.Tests/Optimum.Launcher.Tests.csproj -c Release --no-build --nologo

- name: Optimum.Cli build drives the pipeline and emits a conformant stream
timeout-minutes: 25
shell: bash
run: |
set -euo pipefail
out="$RUNNER_TEMP/optimum-package"
dotnet run --project Optimum.Cli -c Release -- build \
--json --acknowledge-decompile \
--client-archive "$PWD/${{ steps.client.outputs.path }}" \
--output "$out" | tee "$RUNNER_TEMP/build-stream.ndjson"
python3 scripts/check-ndjson-stream.py < "$RUNNER_TEMP/build-stream.ndjson"
pkg="$(ls -d "$out"/Optimum-v*/ | head -n1)"
dotnet run --project Optimum.Cli -c Release -- validate --package "$pkg"

- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
Expand All @@ -397,6 +411,7 @@ jobs:
path: |
.vanilla/*/vintagestory/Logs/
*.log
${{ runner.temp }}/build-stream.ndjson
retention-days: 7

bootstrap-linux-arm:
Expand Down
188 changes: 188 additions & 0 deletions .github/workflows/release-installer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Release installer

# Packages the Avalonia installer with Velopack for Windows and Linux and
# publishes the feed. macOS binaries are built for archival but not published:
# there is no Apple Developer Program account yet (INSTALLER-PLAN.md section 2).
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (defaults to the VERSION file)'
required: false
type: string
publish:
description: 'Upload the Velopack feed to GitHub releases'
required: true
default: false
type: boolean

concurrency:
group: release-installer
cancel-in-progress: false

permissions:
contents: write

jobs:
pack:
strategy:
fail-fast: true
matrix:
include:
# `label` is only the artifact name; vpk picks the Velopack channel
# from the RID (win / linux / osx).
- { os: windows-latest, rid: win-x64, exe: Optimum.Installer.exe, label: win }
- { os: ubuntu-latest, rid: linux-x64, exe: Optimum.Installer, label: linux }
- { os: macos-14, rid: osx-arm64, exe: Optimum.Installer, label: osx-arm64 }
- { os: macos-13, rid: osx-x64, exe: Optimum.Installer, label: osx-x64 }
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }}
steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Resolve version
id: v
shell: bash
run: |
version="${{ inputs.version }}"
[ -n "$version" ] || version="$(cat VERSION)"
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Restore local tools
run: dotnet tool restore

- name: Publish self-contained
run: >
dotnet publish Optimum.Installer/Optimum.Installer.csproj -c Release
-r ${{ matrix.rid }} --self-contained
-o publish/${{ matrix.rid }} --nologo

- name: Pack with Velopack
shell: bash
run: |
set -euo pipefail
dotnet vpk pack \
--packId Optimum.Installer \
--packVersion "${{ steps.v.outputs.version }}" \
--packDir publish/${{ matrix.rid }} \
--mainExe ${{ matrix.exe }} \
--outputDir releases

# vpk has no name-override flag. Rename the user-facing artifacts (setup,
# portable, AppImage) to Optimum-v<version>-<rid>-Installer.<ext> and fix
# the reference in assets.<channel>.json so `vpk upload` still finds them.
# The .nupkg / releases.<channel>.json / RELEASES-<channel> feed files keep
# their Velopack names - the updater fetches those by name and users never
# see them. "Installer" in the name keeps these distinct from the
# ready-to-run game packages (Optimum-v<version>-<rid>.{zip,AppImage,dmg}).
- name: Rename artifacts to the Optimum scheme
shell: bash
run: |
set -euo pipefail
v="${{ steps.v.outputs.version }}"
rid="${{ matrix.rid }}"
case "$rid" in
win-x64) channel=win ;;
linux-x64) channel=linux ;;
osx-*) channel=osx ;;
*) echo "unknown rid: $rid" >&2; exit 1 ;;
esac
cd releases
assets="assets.$channel.json"

rename() {
local old="$1" new="$2" tmp
[ -f "$old" ] || { echo "skip (absent): $old"; return 0; }
mv "$old" "$new"
tmp="$(mktemp)"
jq --arg o "$old" --arg n "$new" \
'map(if .RelativeFileName == $o then .RelativeFileName = $n else . end)' \
"$assets" > "$tmp"
mv "$tmp" "$assets"
echo "renamed: $old -> $new"
}

case "$channel" in
win)
rename "Optimum.Installer-win-Setup.exe" "Optimum-v$v-$rid-Setup.exe"
rename "Optimum.Installer-win-Portable.zip" "Optimum-v$v-$rid-Installer-Portable.zip"
;;
linux)
rename "Optimum.Installer.AppImage" "Optimum-v$v-$rid-Installer.AppImage"
;;
osx)
rename "Optimum.Installer-osx-Setup.pkg" "Optimum-v$v-$rid-Installer.pkg"
rename "Optimum.Installer-osx-Portable.zip" "Optimum-v$v-$rid-Installer-Portable.zip"
;;
esac

echo "--- releases/ ---"; ls -la
echo "--- $assets ---"; cat "$assets"

- name: Sign (Windows)
if: matrix.rid == 'win-x64' && env.WINDOWS_CERT_BASE64 != ''
run: echo "signtool step goes here once a certificate secret exists"

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: installer-${{ matrix.label }}
path: releases/
retention-days: 14

publish:
if: ${{ inputs.publish }}
needs: pack
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json

- name: Restore local tools
run: dotnet tool restore

- name: Resolve version
id: v
shell: bash
run: |
version="${{ inputs.version }}"
[ -n "$version" ] || version="$(cat VERSION)"
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Download the feeds
uses: actions/download-artifact@v4
with:
pattern: installer-*
path: feeds

# One upload per channel, in sequence, into the same tag. macOS is not
# published (INSTALLER-PLAN.md section 2).
- name: Publish to GitHub releases
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
version="${{ steps.v.outputs.version }}"
tag="installer-v$version"
for channel in win linux; do
dotnet vpk upload github \
--repoUrl "https://github.com/${{ github.repository }}" \
--outputDir "feeds/installer-$channel" \
--channel "$channel" \
--tag "$tag" \
--releaseName "Optimum installer v$version" \
--publish
done
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Optimum-v*-mac-*/
bin/
obj/
dist/
publish/
/releases/

# IDE
.vs/
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ make run # launch and test in-game

See the [Building from Source](https://github.com/StratumServer/Optimum/wiki/Building-from-Source) wiki page for prerequisites and details.

### Installer

The installer stack (`Optimum.Bootstrap.Core`, `Optimum.Cli`, `Optimum.Installer`)
builds and tests without a bootstrap, so it has its own commands:

```bash
make installer-test # dotnet test Optimum.Installer.slnf
make installer-pack INSTALLER_RID=linux-x64 # Velopack package for one RID
```

The design and phase status are in `INSTALLER-PLAN.md`. New installer code is
tested against an in-memory `ISystemProbe`; the shell scripts under `scripts/`
stay as the execution layer the engine drives.

## Architecture

Optimum has three components:
Expand Down
Loading
Loading