diff --git a/.github/packaging/macos/build.sh b/.github/packaging/macos/build.sh
new file mode 100755
index 0000000000..3849ffdaba
--- /dev/null
+++ b/.github/packaging/macos/build.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Build a universal Incus client PKG installer.
+set -eu
+
+VERSION="${VERSION:?VERSION must be set}"
+here="$(dirname "$0")"
+out="installers"
+root="pkgroot"
+res="pkgresources"
+
+mkdir -p "${out}" "${root}/usr/local/bin" "${res}"
+cp COPYING "${res}/LICENSE.txt"
+
+CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o incus.amd64 ./cmd/incus
+CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o incus.arm64 ./cmd/incus
+lipo -create -output "${root}/usr/local/bin/incus" incus.amd64 incus.arm64
+chmod 0755 "${root}/usr/local/bin/incus"
+
+pkgbuild --root "${root}" \
+ --identifier org.linuxcontainers.incus \
+ --version "${VERSION}" \
+ --install-location / \
+ incus-component.pkg
+
+productbuild --distribution "${here}/distribution.xml" \
+ --package-path . \
+ --resources "${res}" \
+ "${out}/incus.macos.pkg"
+
+rm -rf incus.amd64 incus.arm64 incus-component.pkg "${root}" "${res}"
diff --git a/.github/packaging/macos/distribution.xml b/.github/packaging/macos/distribution.xml
new file mode 100644
index 0000000000..468f5016ed
--- /dev/null
+++ b/.github/packaging/macos/distribution.xml
@@ -0,0 +1,16 @@
+
+
+ Incus
+
+
+
+
+
+
+
+
+
+
+
+ incus-component.pkg
+
diff --git a/.github/packaging/windows/build.sh b/.github/packaging/windows/build.sh
new file mode 100755
index 0000000000..609225328c
--- /dev/null
+++ b/.github/packaging/windows/build.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Build per-architecture Incus client MSI installers.
+set -eu
+
+VERSION="${VERSION:?VERSION must be set}"
+here="$(dirname "$0")"
+out="installers"
+
+mkdir -p "${out}"
+bash "${here}/make-license-rtf.sh" COPYING "${out}/license.rtf"
+
+build_one() {
+ goarch="$1"
+ wixarch="$2"
+ name="$3"
+
+ CGO_ENABLED=0 GOOS=windows GOARCH="${goarch}" go build -o "${out}/incus.exe" ./cmd/incus
+ wix build -arch "${wixarch}" \
+ -ext WixToolset.UI.wixext \
+ -d Version="${VERSION}" \
+ -d BinPath="${out}/incus.exe" \
+ -d LicenseRtf="${out}/license.rtf" \
+ -o "${out}/incus.windows.${name}.msi" \
+ "${here}/incus.wxs"
+}
+
+build_one amd64 x64 x86_64
+build_one arm64 arm64 aarch64
+
+rm -f "${out}/incus.exe" "${out}/license.rtf"
diff --git a/.github/packaging/windows/incus.wxs b/.github/packaging/windows/incus.wxs
new file mode 100644
index 0000000000..5d05d3097d
--- /dev/null
+++ b/.github/packaging/windows/incus.wxs
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.github/packaging/windows/make-license-rtf.sh b/.github/packaging/windows/make-license-rtf.sh
new file mode 100755
index 0000000000..5441f87648
--- /dev/null
+++ b/.github/packaging/windows/make-license-rtf.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# Wrap a plain-text license into a minimal RTF for the WiX license dialog.
+set -eu
+
+src="$1"
+dst="$2"
+
+{
+ printf '{\\rtf1\\ansi\\deff0{\\fonttbl{\\f0 Courier New;}}\\fs16\n'
+ sed -e 's/\\/\\\\/g' -e 's/{/\\{/g' -e 's/}/\\}/g' -e 's/$/\\par/' "$src"
+ printf '}\n'
+} > "$dst"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c07e316ad7..edd4f27f69 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,9 +11,96 @@ permissions:
attestations: write
jobs:
+ version:
+ name: Format version
+ runs-on: ubuntu-latest
+ outputs:
+ display: ${{ steps.version.outputs.display }}
+ full: ${{ steps.version.outputs.full }}
+ steps:
+ - name: Format version
+ id: version
+ run: |
+ raw="${GITHUB_REF_NAME#v}"
+ IFS='.' read -r major minor patch <<< "$raw"
+ echo "full=${major}.${minor}.${patch}" >> $GITHUB_OUTPUT
+ if [ "${patch}" = "0" ]; then
+ echo "display=${major}.${minor}" >> $GITHUB_OUTPUT
+ else
+ echo "display=${major}.${minor}.${patch}" >> $GITHUB_OUTPUT
+ fi
+
+ build-msi:
+ name: Build Windows installer
+ runs-on: windows-latest
+ needs: version
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Install Go
+ uses: actions/setup-go@v6
+ with:
+ go-version: stable
+
+ # Pinned to v5, the last release before WiX v6/v7 moved to the Open
+ # Source Maintenance Fee license requiring EULA acceptance.
+ - name: Install WiX
+ shell: pwsh
+ run: |
+ dotnet tool install --global wix --version 5.0.2
+ echo "$env:USERPROFILE\.dotnet\tools" | Out-File -FilePath $env:GITHUB_PATH -Append
+
+ - name: Add WiX extensions
+ shell: pwsh
+ run: wix extension add -g WixToolset.UI.wixext/5.0.2
+
+ - name: Build installers
+ shell: bash
+ env:
+ VERSION: ${{ needs.version.outputs.full }}
+ run: bash .github/packaging/windows/build.sh
+
+ - name: Upload installers
+ uses: actions/upload-artifact@v4
+ with:
+ name: installers-windows
+ path: installers/*.msi
+ if-no-files-found: error
+
+ build-pkg:
+ name: Build macOS installer
+ runs-on: macos-latest
+ needs: version
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Install Go
+ uses: actions/setup-go@v6
+ with:
+ go-version: stable
+
+ - name: Build installer
+ env:
+ VERSION: ${{ needs.version.outputs.full }}
+ run: bash .github/packaging/macos/build.sh
+
+ - name: Upload installer
+ uses: actions/upload-artifact@v4
+ with:
+ name: installers-macos
+ path: installers/*.pkg
+ if-no-files-found: error
+
goreleaser:
name: Release
runs-on: ubuntu-latest
+ needs: [version, build-msi, build-pkg]
steps:
- name: Checkout
@@ -29,16 +116,12 @@ jobs:
- name: Install syft
uses: anchore/sbom-action/download-syft@v0
- - name: Format version
- id: version
- run: |
- raw="${GITHUB_REF_NAME#v}"
- IFS='.' read -r major minor patch <<< "$raw"
- if [ "${patch}" = "0" ]; then
- echo "value=${major}.${minor}" >> $GITHUB_OUTPUT
- else
- echo "value=${major}.${minor}.${patch}" >> $GITHUB_OUTPUT
- fi
+ - name: Download installers
+ uses: actions/download-artifact@v4
+ with:
+ pattern: installers-*
+ path: installers
+ merge-multiple: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7
@@ -48,9 +131,16 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- INCUS_VERSION: ${{ steps.version.outputs.value }}
+ INCUS_VERSION: ${{ needs.version.outputs.display }}
- name: Handle attestation
uses: actions/attest@v4
with:
subject-checksums: ./dist/checksums.txt
+
+ - name: Handle installer attestation
+ uses: actions/attest@v4
+ with:
+ subject-path: |
+ installers/*.msi
+ installers/*.pkg
diff --git a/.gitignore b/.gitignore
index c49fbf1525..42de1f4060 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
po/*.mo
po/*.po~
incus-*.tar.xz
+installers
.vagrant
*~
tags
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 5114bb02d9..bc874f71dc 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -162,6 +162,9 @@ release:
owner: lxc
name: incus
name_template: "Incus {{ .Env.INCUS_VERSION }}"
+ extra_files:
+ - glob: ./installers/*.msi
+ - glob: ./installers/*.pkg
sboms:
- id: archive