Add shared runner dependency setup for Linux and Windows
CI / Real Minecraft installs (manual) (push) Skipped
CI / Windows launcher (push) Failing after 13s
CI / Rust, web and container (push) Canceled after 0s
Server integrations / paper (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.20.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.21.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.1.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.3 (push) Canceled after 0s

This commit is contained in:
SCOPEDD committed 2026-10-01 01:04:03 -04:00
1 parent 23ab34ab72
commit a378f9904c
7 files changed
+262 -83

No files matched your search

+25 -34
View File
@@ -19,17 +19,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Verify Linux runner
run: |
set -eu
test "$(uname -s)" = Linux
for tool in cargo rustc node npm docker; do command -v "$tool" >/dev/null || { echo "Missing $tool on ubuntu-latest runner"; exit 1; }; done
test "$(node -p 'process.versions.node.split(".")[0]')" -ge 22
rustup component list --installed | grep -q rustfmt
rustup component list --installed | grep -q clippy
pkg-config --exists webkit2gtk-4.1 ayatana-appindicator3-0.1 librsvg-2.0 || { echo 'Missing Tauri GTK/WebKit development packages'; exit 1; }
docker info >/dev/null || { echo 'Docker daemon unavailable'; exit 1; }
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
@@ -37,6 +26,9 @@ jobs:
cache-dependency-path: | cache-dependency-path: |
panel/web/package-lock.json panel/web/package-lock.json
launcher/package-lock.json launcher/package-lock.json
- name: Install Linux build dependencies
run: bash scripts/ci/install-linux.sh rust
- uses: Swatinem/rust-cache@v2
- name: Check Rust - name: Check Rust
run: | run: |
mkdir -p launcher/dist mkdir -p launcher/dist
@@ -49,37 +41,35 @@ jobs:
for app in panel/web launcher; do for app in panel/web launcher; do
(cd "$app" && npm ci --no-audit --no-fund && npm run check && npm run build) (cd "$app" && npm ci --no-audit --no-fund && npm run check && npm run build)
done done
- id: docker
run: |
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
echo 'ready=true' >> "$GITEA_OUTPUT"
else
echo 'ready=false' >> "$GITEA_OUTPUT"
fi
- uses: docker/setup-docker-action@v5
if: steps.docker.outputs.ready != 'true'
- uses: docker/setup-buildx-action@v3
- name: Build panel container - name: Build panel container
run: docker build -t scopenet-panel:ci . run: |
bash scripts/ci/install-linux.sh image
docker build -t scopenet-panel:ci .
launcher-windows: launcher-windows:
name: Windows launcher name: Windows launcher
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Find PowerShell 7 on Windows runner
shell: powershell
run: |
$pwsh = (Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -First 1).Source
if (-not $pwsh) { $pwsh = Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe' }
if (-not (Test-Path $pwsh)) { throw "PowerShell 7 is not available to the runner at $pwsh" }
$version = & $pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()'
Write-Host "Using PowerShell $version at $pwsh"
Add-Content -Path $env:GITEA_PATH -Value (Split-Path $pwsh)
- name: Verify Windows runner
shell: pwsh
run: |
if (-not $IsWindows) { throw 'windows-latest must be a Windows host runner' }
foreach ($tool in @('cargo', 'rustc', 'node', 'npm')) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "Missing $tool" }
}
if ([int](node -p 'process.versions.node.split(".")[0]') -lt 22) { throw 'Node 22+ required' }
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
cache: npm cache: npm
cache-dependency-path: launcher/package-lock.json cache-dependency-path: launcher/package-lock.json
- name: Install Windows build dependencies
shell: powershell
run: .\scripts\ci\install-windows.ps1
- uses: Swatinem/rust-cache@v2
- run: npm ci --no-audit --no-fund - run: npm ci --no-audit --no-fund
working-directory: launcher working-directory: launcher
- run: npm run build - run: npm run build
@@ -94,9 +84,10 @@ jobs:
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Verify runner - uses: actions/setup-node@v4
run: | with:
test "$(uname -s)" = Linux node-version: '22'
command -v cargo - name: Install Linux build dependencies
run: bash scripts/ci/install-linux.sh rust
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test -p scopenet-core --test online --locked -- --ignored --test-threads=2 --nocapture - run: cargo test -p scopenet-core --test online --locked -- --ignored --test-threads=2 --nocapture
+4 -12
View File
@@ -24,12 +24,8 @@ jobs:
with: with:
distribution: temurin distribution: temurin
java-version: '21' java-version: '21'
- name: Verify integration runner - name: Check Linux and Java dependencies
run: | run: bash scripts/ci/install-linux.sh java 21
set -eu
test "$(uname -s)" = Linux
command -v java
java -version 2>&1 | grep -q '21\.' || { echo 'Paper needs Java 21'; exit 1; }
- name: Build Paper with Gradle 8.10.2 - name: Build Paper with Gradle 8.10.2
run: | run: |
sed -i 's/gradle-9.6.0-bin.zip/gradle-8.10.2-bin.zip/' integrations/gradle/wrapper/gradle-wrapper.properties sed -i 's/gradle-9.6.0-bin.zip/gradle-8.10.2-bin.zip/' integrations/gradle/wrapper/gradle-wrapper.properties
@@ -63,12 +59,8 @@ jobs:
with: with:
distribution: temurin distribution: temurin
java-version: ${{ matrix.mc.java }} java-version: ${{ matrix.mc.java }}
- name: Verify integration runner - name: Check Linux and Java dependencies
run: | run: bash scripts/ci/install-linux.sh java '${{ matrix.mc.java }}'
set -eu
test "$(uname -s)" = Linux
command -v java
java -version 2>&1 | grep -q '${{ matrix.mc.java }}\.' || { echo 'Wrong Java version for this matrix entry'; exit 1; }
- name: Build both loaders - name: Build both loaders
run: | run: |
sed -i 's/gradle-9.6.0-bin.zip/gradle-${{ matrix.mc.gradle }}-bin.zip/' integrations/gradle/wrapper/gradle-wrapper.properties sed -i 's/gradle-9.6.0-bin.zip/gradle-${{ matrix.mc.gradle }}-bin.zip/' integrations/gradle/wrapper/gradle-wrapper.properties
+36 -36
View File
@@ -53,57 +53,54 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Verify Windows runner
shell: pwsh
run: |
if (-not $IsWindows) { throw 'windows-latest must be a Windows host runner' }
foreach ($tool in @('cargo', 'node', 'npm')) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "Missing $tool" }
}
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
key: release
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 22 node-version: 22
cache: npm cache: npm
cache-dependency-path: launcher/package-lock.json cache-dependency-path: launcher/package-lock.json
- name: Install Windows build dependencies
shell: powershell
run: .\scripts\ci\install-windows.ps1
- uses: Swatinem/rust-cache@v2
with:
key: release
- name: Stamp version - name: Stamp version
shell: bash shell: pwsh
env: env:
VERSION: ${{ needs.version.outputs.version }} VERSION: ${{ needs.version.outputs.version }}
run: | run: |
sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" Cargo.toml $cargo = Get-Content Cargo.toml -Raw
node -e "const f='launcher/src-tauri/tauri.conf.json';const c=require('./'+f);c.version=process.env.VERSION;require('fs').writeFileSync(f,JSON.stringify(c,null,2))" $cargo = [regex]::new('(?m)^version = ".*"').Replace($cargo, 'version = "' + $env:VERSION + '"', 1)
node -e "const f='launcher/package.json';const c=require('./'+f);c.version=process.env.VERSION;require('fs').writeFileSync(f,JSON.stringify(c,null,2))" [IO.File]::WriteAllText((Join-Path (Get-Location) 'Cargo.toml'), $cargo)
node -e "for(const f of ['launcher/src-tauri/tauri.conf.json','launcher/package.json']){const fs=require('fs');const c=JSON.parse(fs.readFileSync(f));c.version=process.env.VERSION;fs.writeFileSync(f,JSON.stringify(c,null,2))}"
- run: npm ci --no-audit --no-fund - run: npm ci --no-audit --no-fund
working-directory: launcher working-directory: launcher
- name: Build installer - name: Build installer
working-directory: launcher working-directory: launcher
shell: bash shell: pwsh
env: env:
SCOPENET_PANEL_URL: ${{ vars.PANEL_URL }} SCOPENET_PANEL_URL: ${{ vars.PANEL_URL }}
SCOPENET_LOCK_PANEL: ${{ vars.LOCK_PANEL }} SCOPENET_LOCK_PANEL: ${{ vars.LOCK_PANEL }}
SCOPENET_REPO: ${{ gitea.server_url }}/${{ gitea.repository }} SCOPENET_REPO: ${{ gitea.server_url }}/${{ gitea.repository }}
LAUNCHER_NAME: ${{ vars.LAUNCHER_NAME }} LAUNCHER_NAME: ${{ vars.LAUNCHER_NAME }}
run: | run: |
if [ -n "$LAUNCHER_NAME" ]; then if ($env:LAUNCHER_NAME) {
CONFIG=$(node -e "console.log(JSON.stringify({productName: process.env.LAUNCHER_NAME, app: {windows: [{...require('./src-tauri/tauri.conf.json').app.windows[0], title: process.env.LAUNCHER_NAME}]}}))") $config = node -e "console.log(JSON.stringify({productName: process.env.LAUNCHER_NAME, app: {windows: [{...require('./src-tauri/tauri.conf.json').app.windows[0], title: process.env.LAUNCHER_NAME}]}}))"
npx tauri build --bundles nsis --config "$CONFIG" npx tauri build --bundles nsis --config $config
else } else {
npx tauri build --bundles nsis npx tauri build --bundles nsis
fi }
if ($LASTEXITCODE -ne 0) { throw "Installer build failed with exit code $LASTEXITCODE" }
- name: Collect installer - name: Collect installer
shell: bash shell: pwsh
run: | run: |
mkdir -p out New-Item -ItemType Directory -Force out | Out-Null
cp target/release/bundle/nsis/*-setup.exe out/ Copy-Item target/release/bundle/nsis/*-setup.exe out/
ls -la out Get-ChildItem out
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
@@ -117,17 +114,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Verify container runner
run: |
test "$(uname -s)" = Linux
command -v docker
docker info >/dev/null
docker buildx version >/dev/null
- name: Image name - name: Image name
run: echo "IMAGE=${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY_OWNER,,}/scopenet-mc-panel" >> "$GITEA_ENV" run: echo "IMAGE=${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY_OWNER,,}/scopenet-mc-panel" >> "$GITEA_ENV"
- name: Stamp version - name: Stamp version
run: sed -i "0,/^version = \".*\"/s//version = \"${{ needs.version.outputs.version }}\"/" Cargo.toml run: sed -i "0,/^version = \".*\"/s//version = \"${{ needs.version.outputs.version }}\"/" Cargo.toml
- id: docker
run: |
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
echo 'ready=true' >> "$GITEA_OUTPUT"
else
echo 'ready=false' >> "$GITEA_OUTPUT"
fi
- uses: docker/setup-docker-action@v5
if: steps.docker.outputs.ready != 'true'
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- name: Check Linux container dependencies
run: bash scripts/ci/install-linux.sh image
- uses: docker/login-action@v3 - uses: docker/login-action@v3
with: with:
registry: gitea.jadonsandbox.com registry: gitea.jadonsandbox.com
@@ -162,11 +164,9 @@ jobs:
needs: [version, launcher, panel-image, server-integrations] needs: [version, launcher, panel-image, server-integrations]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Verify release runner - uses: actions/checkout@v5
run: | - name: Install Linux release dependencies
test "$(uname -s)" = Linux run: bash scripts/ci/install-linux.sh release
command -v curl
command -v jq
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
with: with:
name: windows-installer name: windows-installer
+48
View File
@@ -0,0 +1,48 @@
name: Runner setup
on:
workflow_dispatch:
jobs:
linux:
name: Linux image and JAR dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install Rust and system packages
run: bash scripts/ci/install-linux.sh rust
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- run: bash scripts/ci/install-linux.sh java 21
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- run: bash scripts/ci/install-linux.sh java 25
- id: docker
run: |
if command -v docker >/dev/null && docker info >/dev/null 2>&1; then
echo 'ready=true' >> "$GITHUB_OUTPUT"
else
echo 'ready=false' >> "$GITHUB_OUTPUT"
fi
- uses: docker/setup-docker-action@v5
if: steps.docker.outputs.ready != 'true'
- uses: docker/setup-buildx-action@v3
- run: bash scripts/ci/install-linux.sh image
windows:
name: Windows installer dependencies
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install PowerShell, Rust and MSVC
shell: powershell
run: .\scripts\ci\install-windows.ps1
+3 -1
View File
@@ -82,7 +82,9 @@ The workflow builds the **Windows installer**, pushes the **panel image to Gitea
### Gitea runner setup ### Gitea runner setup
Register a Linux host runner with the `ubuntu-latest` label and a Windows host runner with `windows-latest`. The CI workflows check the host OS and required tools before building. The Linux host needs Rust with `rustfmt` and `clippy`, Node 22+, Java 21 and 25 for the integration matrix, Docker with Buildx, `pkg-config`, the Tauri WebKit/GTK development libraries, `curl`, and `jq`. The Windows host needs Rust, Node 22+, PowerShell and Git Bash. Enable Actions for the repository and allow its job token to write releases and packages. See [Gitea's runner labels](https://docs.gitea.com/runner/labels/) and [token permissions](https://docs.gitea.com/usage/actions/token-permissions/). Register a Linux host runner with the `ubuntu-latest` label and a Windows host runner with `windows-latest`. Linux builds the panel Docker image and server JARs; Windows builds the NSIS installer. The workflows use [the Linux setup tool](scripts/ci/install-linux.sh) and [the Windows setup tool](scripts/ci/install-windows.ps1) to install or check job dependencies. Node 22 and Java 21/25 are downloaded by `setup-node` and `setup-java`; the Linux tool installs Rust, Tauri system packages, and release utilities, while the Windows tool installs Rust and downloads PowerShell 7.6.6 if the runner cannot find it. Docker and Buildx are set up by Docker's Actions. The Linux runner must allow a Docker daemon to start or provide one, and system package installation needs root or passwordless sudo. Enable Actions for the repository and allow its job token to write releases and packages. See [Gitea's runner labels](https://docs.gitea.com/runner/labels/) and [token permissions](https://docs.gitea.com/usage/actions/token-permissions/).
The setup tools also run directly on matching hosts: `bash scripts/ci/install-linux.sh rust` prepares Rust and Tauri builds, `bash scripts/ci/install-linux.sh image` checks Docker, `bash scripts/ci/install-linux.sh java 21` checks a Java integration runner, and `powershell -File scripts/ci/install-windows.ps1` prepares the Windows installer runner. Gitea uses them in CI and release jobs; GitHub provides a manual **Runner setup** workflow for both platforms. Run the Linux `rust` mode as root or with passwordless sudo; the Windows script may need administrator rights to install Microsoft C++ Build Tools.
## Repository layout ## Repository layout
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Install job-local dependencies on Linux Actions runners.
set -euo pipefail
mode="${1:-}"
case "$mode" in
rust|image|java|release) ;;
*) echo "Usage: $0 {rust|image|java|release}" >&2; exit 2 ;;
esac
[[ "$(uname -s)" == Linux ]] || { echo 'This setup tool requires Linux.' >&2; exit 1; }
add_path() {
export PATH="$1:$PATH"
for path_file in "${GITHUB_PATH:-}" "${GITEA_PATH:-}"; do
[[ -z "$path_file" ]] || printf '%s\n' "$1" >> "$path_file"
done
}
install_packages() {
local apt=()
local missing=()
command -v apt-get >/dev/null || { echo 'Automatic system package installation requires apt-get (Ubuntu/Debian runner).' >&2; exit 1; }
for package in "$@"; do
dpkg-query -W -f='${Status}' "$package" 2>/dev/null | grep -q 'install ok installed' || missing+=("$package")
done
[[ ${#missing[@]} -gt 0 ]] || return 0
if [[ "$(id -u)" != 0 ]]; then
command -v sudo >/dev/null || { echo 'System packages require root or passwordless sudo.' >&2; exit 1; }
sudo -n true || { echo 'Passwordless sudo is required for system packages.' >&2; exit 1; }
apt=(sudo -n)
fi
"${apt[@]}" apt-get update
DEBIAN_FRONTEND=noninteractive "${apt[@]}" apt-get install -y --no-install-recommends "${missing[@]}"
}
require() {
command -v "$1" >/dev/null || { echo "Missing $1 after dependency setup." >&2; exit 1; }
}
case "$mode" in
rust)
install_packages build-essential curl file libayatana-appindicator3-dev librsvg2-dev libssl-dev libwebkit2gtk-4.1-dev libxdo-dev pkg-config wget
if ! command -v rustup >/dev/null; then
installer="$(mktemp)"
trap 'rm -f "$installer"' EXIT
curl -fsSL https://sh.rustup.rs -o "$installer"
sh "$installer" -y --profile minimal --default-toolchain stable
fi
add_path "${CARGO_HOME:-$HOME/.cargo}/bin"
rustup toolchain install stable --profile minimal --component rustfmt --component clippy
rustup default stable
require cargo
require rustc
require node
require npm
[[ "$(node -p 'process.versions.node.split(".")[0]')" -ge 22 ]] || { echo 'Node 22+ is required.' >&2; exit 1; }
pkg-config --exists webkit2gtk-4.1 ayatana-appindicator3-0.1 librsvg-2.0
;;
image)
require docker
docker info >/dev/null || { echo 'Docker daemon is unavailable. Mount the host socket or run a Docker-capable host runner.' >&2; exit 1; }
docker buildx version >/dev/null || { echo 'Docker Buildx is missing. Install the Docker buildx plugin on the host runner.' >&2; exit 1; }
;;
java)
require java
[[ -n "${2:-}" ]] || { echo 'Java major version is required.' >&2; exit 2; }
java -version 2>&1 | grep -q "${2}\." || { echo "Java $2 is required for this integration build." >&2; exit 1; }
require bash
;;
release)
if ! command -v curl >/dev/null || ! command -v jq >/dev/null; then install_packages curl jq; fi
require curl
require jq
;;
esac
echo "Linux $mode dependencies are ready."
+70
View File
@@ -0,0 +1,70 @@
# Installs job-local PowerShell and Rust dependencies for the Windows launcher.
param([ValidateSet('Launcher')][string]$Mode = 'Launcher')
$ErrorActionPreference = 'Stop'
if (-not [Environment]::OSVersion.Platform.ToString().StartsWith('Win')) {
throw 'This setup tool requires Windows.'
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if (-not $env:RUNNER_TEMP) { $env:RUNNER_TEMP = $env:TEMP }
function Add-RunnerPath([string]$Directory) {
$env:PATH = "$Directory;$env:PATH"
foreach ($pathFile in @($env:GITHUB_PATH, $env:GITEA_PATH)) {
if ($pathFile) { Add-Content -Path $pathFile -Value $Directory }
}
}
$pwsh = Get-Command pwsh.exe -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Source
if (-not $pwsh) {
$machinePwsh = Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe'
if (Test-Path $machinePwsh) { $pwsh = $machinePwsh }
}
if (-not $pwsh) {
$version = '7.6.6'
$installDir = Join-Path $env:RUNNER_TEMP "scopenet-pwsh-$version"
$pwsh = Join-Path $installDir 'pwsh.exe'
if (-not (Test-Path $pwsh)) {
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
$archive = Join-Path $env:RUNNER_TEMP "PowerShell-$version-win-x64.zip"
Invoke-WebRequest -UseBasicParsing -Uri "https://github.com/PowerShell/PowerShell/releases/download/v$version/PowerShell-$version-win-x64.zip" -OutFile $archive
Expand-Archive -Path $archive -DestinationPath $installDir -Force
Remove-Item $archive
}
}
Add-RunnerPath (Split-Path $pwsh)
Write-Host "PowerShell $(& $pwsh -NoProfile -Command '$PSVersionTable.PSVersion.ToString()') is ready."
$cargoBin = Join-Path $env:USERPROFILE '.cargo\bin'
Add-RunnerPath $cargoBin
if (-not (Get-Command rustup.exe -ErrorAction SilentlyContinue)) {
$installer = Join-Path $env:RUNNER_TEMP 'scopenet-rustup-init.exe'
Invoke-WebRequest -UseBasicParsing -Uri 'https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe' -OutFile $installer
& $installer -y --profile minimal --default-toolchain stable
if ($LASTEXITCODE -ne 0) { throw "rustup installer failed with exit code $LASTEXITCODE" }
Remove-Item $installer
}
& rustup toolchain install stable-x86_64-pc-windows-msvc --profile minimal --component clippy --component rustfmt
if ($LASTEXITCODE -ne 0) { throw "Rust toolchain install failed with exit code $LASTEXITCODE" }
& rustup default stable-x86_64-pc-windows-msvc
if ($LASTEXITCODE -ne 0) { throw "Rust toolchain selection failed with exit code $LASTEXITCODE" }
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
$vsPath = if (Test-Path $vswhere) { & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath }
if (-not $vsPath) {
$installer = Join-Path $env:RUNNER_TEMP 'scopenet-vs-buildtools.exe'
Invoke-WebRequest -UseBasicParsing -Uri 'https://aka.ms/vs/17/release/vs_buildtools.exe' -OutFile $installer
$arguments = @('--quiet', '--wait', '--norestart', '--add', 'Microsoft.VisualStudio.Workload.VCTools', '--includeRecommended')
$process = Start-Process -FilePath $installer -ArgumentList $arguments -Wait -PassThru
if ($process.ExitCode -eq 3010) { throw 'Microsoft C++ Build Tools installed, but Windows must be restarted before this runner can build.' }
if ($process.ExitCode -ne 0) { throw "Microsoft C++ Build Tools installation failed with exit code $($process.ExitCode). Run the runner with administrator rights." }
Remove-Item $installer
$vsPath = if (Test-Path $vswhere) { & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath }
if (-not $vsPath) { throw 'Microsoft C++ Build Tools installation did not provide the x64 MSVC compiler.' }
}
Write-Host "Microsoft C++ Build Tools are ready at $vsPath."
foreach ($tool in @('cargo.exe', 'rustc.exe', 'node.exe', 'npm.cmd')) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "Missing $tool after dependency setup." }
}
$nodeMajor = [int](& node -p 'process.versions.node.split(".")[0]')
if ($nodeMajor -lt 22) { throw 'Node 22+ is required.' }
Write-Host "Windows $Mode dependencies are ready."