diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index bcbebff..16da361 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -19,17 +19,6 @@ jobs: runs-on: ubuntu-latest steps: - 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 with: node-version: '22' @@ -37,6 +26,9 @@ jobs: cache-dependency-path: | panel/web/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 run: | mkdir -p launcher/dist @@ -49,37 +41,35 @@ jobs: for app in panel/web launcher; do (cd "$app" && npm ci --no-audit --no-fund && npm run check && npm run build) 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 - run: docker build -t scopenet-panel:ci . + run: | + bash scripts/ci/install-linux.sh image + docker build -t scopenet-panel:ci . launcher-windows: name: Windows launcher runs-on: windows-latest steps: - 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 with: node-version: '22' cache: npm 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 working-directory: launcher - run: npm run build @@ -94,9 +84,10 @@ jobs: continue-on-error: true steps: - uses: actions/checkout@v5 - - name: Verify runner - run: | - test "$(uname -s)" = Linux - command -v cargo + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Install Linux build dependencies + run: bash scripts/ci/install-linux.sh rust - uses: Swatinem/rust-cache@v2 - run: cargo test -p scopenet-core --test online --locked -- --ignored --test-threads=2 --nocapture diff --git a/.gitea/workflows/integrations.yml b/.gitea/workflows/integrations.yml index 2f83610..566f58c 100644 --- a/.gitea/workflows/integrations.yml +++ b/.gitea/workflows/integrations.yml @@ -24,12 +24,8 @@ jobs: with: distribution: temurin java-version: '21' - - name: Verify integration runner - run: | - set -eu - test "$(uname -s)" = Linux - command -v java - java -version 2>&1 | grep -q '21\.' || { echo 'Paper needs Java 21'; exit 1; } + - name: Check Linux and Java dependencies + run: bash scripts/ci/install-linux.sh java 21 - name: Build Paper with Gradle 8.10.2 run: | 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: distribution: temurin java-version: ${{ matrix.mc.java }} - - name: Verify integration runner - run: | - 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: Check Linux and Java dependencies + run: bash scripts/ci/install-linux.sh java '${{ matrix.mc.java }}' - name: Build both loaders run: | sed -i 's/gradle-9.6.0-bin.zip/gradle-${{ matrix.mc.gradle }}-bin.zip/' integrations/gradle/wrapper/gradle-wrapper.properties diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 586f957..72b7271 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -53,57 +53,54 @@ jobs: runs-on: windows-latest steps: - 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 with: node-version: 22 cache: npm 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 - shell: bash + shell: pwsh env: VERSION: ${{ needs.version.outputs.version }} run: | - sed -i "0,/^version = \".*\"/s//version = \"$VERSION\"/" Cargo.toml - 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))" - 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))" + $cargo = Get-Content Cargo.toml -Raw + $cargo = [regex]::new('(?m)^version = ".*"').Replace($cargo, 'version = "' + $env:VERSION + '"', 1) + [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 working-directory: launcher - name: Build installer working-directory: launcher - shell: bash + shell: pwsh env: SCOPENET_PANEL_URL: ${{ vars.PANEL_URL }} SCOPENET_LOCK_PANEL: ${{ vars.LOCK_PANEL }} SCOPENET_REPO: ${{ gitea.server_url }}/${{ gitea.repository }} LAUNCHER_NAME: ${{ vars.LAUNCHER_NAME }} run: | - if [ -n "$LAUNCHER_NAME" ]; then - 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" - else + 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}]}}))" + npx tauri build --bundles nsis --config $config + } else { npx tauri build --bundles nsis - fi + } + if ($LASTEXITCODE -ne 0) { throw "Installer build failed with exit code $LASTEXITCODE" } - name: Collect installer - shell: bash + shell: pwsh run: | - mkdir -p out - cp target/release/bundle/nsis/*-setup.exe out/ - ls -la out + New-Item -ItemType Directory -Force out | Out-Null + Copy-Item target/release/bundle/nsis/*-setup.exe out/ + Get-ChildItem out - uses: actions/upload-artifact@v4 with: @@ -117,17 +114,22 @@ jobs: runs-on: ubuntu-latest steps: - 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 run: echo "IMAGE=${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY_OWNER,,}/scopenet-mc-panel" >> "$GITEA_ENV" - name: Stamp version 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 + - name: Check Linux container dependencies + run: bash scripts/ci/install-linux.sh image - uses: docker/login-action@v3 with: registry: gitea.jadonsandbox.com @@ -162,11 +164,9 @@ jobs: needs: [version, launcher, panel-image, server-integrations] runs-on: ubuntu-latest steps: - - name: Verify release runner - run: | - test "$(uname -s)" = Linux - command -v curl - command -v jq + - uses: actions/checkout@v5 + - name: Install Linux release dependencies + run: bash scripts/ci/install-linux.sh release - uses: actions/download-artifact@v4 with: name: windows-installer diff --git a/.github/workflows/runner-setup.yml b/.github/workflows/runner-setup.yml new file mode 100644 index 0000000..e0edfd0 --- /dev/null +++ b/.github/workflows/runner-setup.yml @@ -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 diff --git a/README.md b/README.md index 949b0c4..ac72fa0 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ The workflow builds the **Windows installer**, pushes the **panel image to Gitea ### 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 diff --git a/scripts/ci/install-linux.sh b/scripts/ci/install-linux.sh new file mode 100755 index 0000000..0c0d168 --- /dev/null +++ b/scripts/ci/install-linux.sh @@ -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." diff --git a/scripts/ci/install-windows.ps1 b/scripts/ci/install-windows.ps1 new file mode 100644 index 0000000..203424e --- /dev/null +++ b/scripts/ci/install-windows.ps1 @@ -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."