diff --git a/.github/workflows/avalonia-rust.yml b/.github/workflows/avalonia-rust.yml
index d1c570e..05ff82d 100644
--- a/.github/workflows/avalonia-rust.yml
+++ b/.github/workflows/avalonia-rust.yml
@@ -76,10 +76,8 @@ jobs:
rid: linux-x64
platform: X11
architecture: x64
- - os: macos-15-intel
- rid: osx-x64
- platform: OSX
- architecture: x64
+ # osx-x64 is covered by cross-build on Apple silicon; Intel macOS
+ # runners took about an hour for this job.
- os: macos-14
rid: osx-arm64
platform: OSX
@@ -450,3 +448,18 @@ jobs:
throw 'Downloaded cross-package executable lost its execute permission.'
}
}
+
+ # Release builds the osx-x64 host on Apple silicon too; rehearse that
+ # here and launch the x64 app through Rosetta 2.
+ - name: Install Rosetta 2 for the x64 smoke test
+ if: matrix.rid == 'osx-x64'
+ run: |
+ /usr/bin/arch -x86_64 /usr/bin/true 2>/dev/null || sudo softwareupdate --install-rosetta --agree-to-license
+ /usr/bin/arch -x86_64 /usr/bin/true
+
+ - name: Build and launch a standalone app against the packaged crates and a cross-built host tarball
+ if: matrix.rid == 'osx-x64'
+ shell: pwsh
+ run: |
+ ./rust/package-host.ps1 -Rid ${{ matrix.rid }} -OutputRoot rust/artifacts/host
+ ./rust/tests/test-standalone-app.ps1 -Rid ${{ matrix.rid }} -TarballDirectory rust/artifacts/host -WorkDirectory "$env:RUNNER_TEMP/standalone-app"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d253302..75d905f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,24 +1,25 @@
name: Release
# Release procedure (see rust/PRODUCTIZATION.md#releasing):
-# 1. Prepare and merge the version, then dispatch phase=build from master.
-# -> stage tarballs and SHA256SUMS as Actions artifacts (no tag/release).
-# 2. Download SHA256SUMS from that run, commit host checksums to master.
-# Push the consumer tag vX.Y.Z on that checksum-bearing commit.
-# 3. Dispatch phase=publish with build_run_id and version X.Y.Z.
-# -> verify a standalone git consumer, create release vX.Y.Z.
-# Non-draft by default; draft=true stages a draft instead.
-# Optionally publish crates after approval in `crates-io`.
+# Prepare and merge the version, then dispatch phase=all from master:
+# build -> stage tarballs and SHA256SUMS as Actions artifacts.
+# pin-release -> commit host checksums on master, tag vX.Y.Z on it.
+# publish -> verify a standalone git consumer, create release vX.Y.Z.
+# Non-draft by default; draft=true stages a draft instead.
+# Optionally publish crates after approval in `crates-io`.
+# The manual fallback splits this into phase=build, a local checksum
+# commit and tag push, then phase=publish with build_run_id.
on:
workflow_dispatch:
inputs:
phase:
- description: Build hosts first; publish after committing checksums and pushing the final tag
- default: build
+ description: all builds, pins checksums on master, tags and publishes; build and publish are the manual two-dispatch path
+ default: all
required: true
type: choice
options:
+ - all
- build
- publish
version:
@@ -31,7 +32,7 @@ on:
default: false
type: boolean
build_run_id:
- description: Successful build-phase run ID whose artifacts and source must match the final tag
+ description: Publish phase only; successful build-phase run ID whose artifacts and source must match the final tag
default: ''
type: string
draft:
@@ -49,7 +50,7 @@ concurrency:
jobs:
version:
name: check version
- if: inputs.phase == 'build'
+ if: inputs.phase == 'build' || inputs.phase == 'all'
runs-on: ubuntu-22.04
permissions:
contents: read
@@ -68,7 +69,11 @@ jobs:
$version = Get-RustoloniaReleaseVersion -RustoloniaRoot $PWD
if ($env:GITHUB_REF -ne 'refs/heads/master') { throw 'Dispatch the host build from master after merging the release preparation.' }
if ($env:REQUESTED_VERSION -ne $version) { throw "Requested version $env:REQUESTED_VERSION does not match version $version." }
- if ($env:PUBLISH_CRATES -eq 'true') { throw 'publish_crates is only valid for the publish phase.' }
+ if ($env:PHASE -eq 'build' -and $env:PUBLISH_CRATES -eq 'true') { throw 'publish_crates is only valid for the publish and all phases.' }
+ if ($env:PHASE -eq 'all') {
+ if ($env:BUILD_RUN_ID) { throw 'build_run_id is only used by the publish phase; phase=all builds its own hosts.' }
+ if ($env:DRAFT -eq 'true' -and $env:PUBLISH_CRATES -eq 'true') { throw 'Cannot publish crates for a draft release.' }
+ }
if (@(git tag --list "v$version").Count -gt 0) { throw "Final tag v$version already exists; use the publish phase for its staged build." }
if ((Get-Content build/SharedVersion.props -Raw) -notmatch "$([regex]::Escape($version))") {
throw "build/SharedVersion.props does not match version $version."
@@ -77,7 +82,10 @@ jobs:
"prerelease=$($version.Contains('-').ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
env:
REQUESTED_VERSION: ${{ inputs.version }}
+ PHASE: ${{ inputs.phase }}
PUBLISH_CRATES: ${{ inputs.publish_crates }}
+ DRAFT: ${{ inputs.draft }}
+ BUILD_RUN_ID: ${{ inputs.build_run_id }}
build:
name: host (${{ matrix.rid }})
@@ -97,7 +105,9 @@ jobs:
# The oldest runner image sets the glibc floor (2.35).
- { os: ubuntu-22.04, rid: linux-x64 }
- { os: ubuntu-22.04-arm, rid: linux-arm64 }
- - { os: macos-15-intel, rid: osx-x64 }
+ # Intel macOS runners are slow; Apple silicon cross-builds osx-x64
+ # and smoke-launches it through Rosetta 2.
+ - { os: macos-14, rid: osx-x64, rust-target: x86_64-apple-darwin }
- { os: macos-14, rid: osx-arm64 }
steps:
- uses: actions/checkout@v4
@@ -125,14 +135,26 @@ jobs:
- name: Set up Rust
shell: pwsh
+ env:
+ RUST_TARGET: ${{ matrix.rust-target }}
run: |
rustup show
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ if ($env:RUST_TARGET) {
+ rustup target add $env:RUST_TARGET
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ }
cargo --version
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
rustc --version
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ - name: Install Rosetta 2 for the x64 smoke test
+ if: matrix.rust-target == 'x86_64-apple-darwin'
+ run: |
+ /usr/bin/arch -x86_64 /usr/bin/true 2>/dev/null || sudo softwareupdate --install-rosetta --agree-to-license
+ /usr/bin/arch -x86_64 /usr/bin/true
+
- name: Install Linux smoke-test display
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install --yes xvfb
@@ -216,9 +238,77 @@ jobs:
if-no-files-found: error
retention-days: 90
+ # phase=all: commit the staged hashes on the build source and tag it, as the
+ # manual path does locally. The push is atomic and never forced, so it fails
+ # without changing anything if master moved or the tag appeared meanwhile.
+ pin-release:
+ name: pin checksums and tag v${{ needs.version.outputs.version }}
+ needs: [version, stage-release]
+ if: inputs.phase == 'all'
+ runs-on: ubuntu-22.04
+ permissions:
+ contents: write
+ outputs:
+ commit: ${{ steps.pin.outputs.commit }}
+ env:
+ VERSION: ${{ needs.version.outputs.version }}
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ github.sha }}
+ fetch-depth: 0
+ persist-credentials: false
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: rustolonia-release-${{ needs.version.outputs.version }}
+ path: ${{ runner.temp }}/release/v${{ needs.version.outputs.version }}
+
+ - name: Verify staged asset hashes
+ working-directory: ${{ runner.temp }}/release/v${{ needs.version.outputs.version }}
+ run: sha256sum --check --strict SHA256SUMS
+
+ - name: Check that master is still the build source and the tag is free
+ shell: pwsh
+ run: |
+ $master = gh api "repos/$env:GH_REPO/git/ref/heads/master" --jq .object.sha
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ if ($master -ne $env:GITHUB_SHA) { throw "master moved from the build source $env:GITHUB_SHA to $master; dispatch a new release run." }
+ $tags = @(gh api "repos/$env:GH_REPO/git/matching-refs/tags/v$env:VERSION" --jq '.[].ref')
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ if ($tags -contains "refs/tags/v$env:VERSION") { throw "Tag v$env:VERSION already exists; use the publish phase." }
+
+ - id: pin
+ name: Commit host checksums and tag
+ shell: pwsh
+ run: |
+ git config user.name 'github-actions[bot]'
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+ ./rust/prepare-release.ps1 -Checksums -CommitAndTag -SourceRevision $env:GITHUB_SHA `
+ -SumsFile (Join-Path $env:RUNNER_TEMP 'release' "v$env:VERSION" 'SHA256SUMS')
+ "commit=$(git rev-parse HEAD)" >> $env:GITHUB_OUTPUT
+
+ # RELEASE_PUSH_TOKEN is only needed if rules on master or v* tags block
+ # GITHUB_TOKEN; see rust/PRODUCTIZATION.md#releasing.
+ - name: Push master and the tag
+ env:
+ PUSH_TOKEN: ${{ secrets.RELEASE_PUSH_TOKEN || github.token }}
+ run: |
+ set -euo pipefail
+ auth=$(printf 'x-access-token:%s' "$PUSH_TOKEN" | base64 -w0)
+ echo "::add-mask::$auth"
+ git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $auth" \
+ push --atomic "https://github.com/$GH_REPO.git" HEAD:refs/heads/master "refs/tags/v$VERSION"
+
publish:
name: publish ${{ inputs.version }}
- if: github.event_name == 'workflow_dispatch' && inputs.phase == 'publish'
+ needs: pin-release
+ # The publish phase runs alone; phase=all continues once pin-release pushed the tag.
+ if: >-
+ !cancelled() && github.event_name == 'workflow_dispatch' &&
+ (inputs.phase == 'publish' || (inputs.phase == 'all' && needs.pin-release.result == 'success'))
runs-on: ubuntu-22.04
permissions:
contents: write
@@ -227,7 +317,9 @@ jobs:
VERSION: ${{ inputs.version }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
+ PHASE: ${{ inputs.phase }}
BUILD_RUN_ID: ${{ inputs.build_run_id }}
+ PINNED_COMMIT: ${{ needs.pin-release.outputs.commit }}
DRAFT: ${{ inputs.draft }}
PUBLISH_CRATES: ${{ inputs.publish_crates }}
steps:
@@ -253,28 +345,37 @@ jobs:
shell: pwsh
run: |
if ($env:GITHUB_REF -ne 'refs/heads/master') { throw 'Dispatch publication from master.' }
- if ($env:BUILD_RUN_ID -notmatch '^[1-9][0-9]*$') { throw 'Pass the successful build-phase build_run_id.' }
if ($env:DRAFT -eq 'true' -and $env:PUBLISH_CRATES -eq 'true') { throw 'Cannot publish crates for a draft release.' }
- $runJson = gh api "repos/$env:GH_REPO/actions/runs/$env:BUILD_RUN_ID"
- if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- $run = $runJson | ConvertFrom-Json
- if ($run.conclusion -ne 'success' -or $run.event -ne 'workflow_dispatch' -or
- $run.head_branch -ne 'master' -or $run.path -ne '.github/workflows/release.yml') {
- throw 'The build must be a successful Release dispatch from master.'
+ if ($env:PHASE -eq 'all') {
+ # This run built the hosts from GITHUB_SHA and pushed the tag itself.
+ $sourceSha = $env:GITHUB_SHA
+ if ((git rev-parse HEAD) -ne $env:PINNED_COMMIT) { throw "v$env:VERSION does not point at the checksum commit $env:PINNED_COMMIT pushed by this run." }
+ }
+ else {
+ if ($env:BUILD_RUN_ID -notmatch '^[1-9][0-9]*$') { throw 'Pass the successful build-phase build_run_id.' }
+ $runJson = gh api "repos/$env:GH_REPO/actions/runs/$env:BUILD_RUN_ID"
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
+ $run = $runJson | ConvertFrom-Json
+ if ($run.conclusion -ne 'success' -or $run.event -ne 'workflow_dispatch' -or
+ $run.head_branch -ne 'master' -or $run.path -ne '.github/workflows/release.yml') {
+ throw 'The build must be a successful Release dispatch from master.'
+ }
+ $sourceSha = $run.head_sha
}
- git merge-base --is-ancestor $run.head_sha HEAD
+ git merge-base --is-ancestor $sourceSha HEAD
if ($LASTEXITCODE -ne 0) { throw 'The consumer tag must descend from the build source.' }
- $changed = @(git diff --name-only $run.head_sha HEAD |
+ $changed = @(git diff --name-only $sourceSha HEAD |
Where-Object { $_ -ne 'rust/rustolonia-sys/host-checksums.txt' })
if ($LASTEXITCODE -ne 0) { throw 'Could not compare the build source and consumer tag.' }
if ($changed.Count -gt 0) { throw "Sources changed since the build: $($changed -join ', ')" }
+ # Without a token, download-artifact reads this run's own artifacts (phase=all).
- uses: actions/download-artifact@v4
with:
name: rustolonia-release-${{ inputs.version }}
path: ${{ runner.temp }}/release/v${{ inputs.version }}
- run-id: ${{ inputs.build_run_id }}
- github-token: ${{ github.token }}
+ run-id: ${{ inputs.phase == 'publish' && inputs.build_run_id || github.run_id }}
+ github-token: ${{ inputs.phase == 'publish' && github.token || '' }}
- name: Verify staged asset hashes
shell: pwsh
@@ -321,7 +422,9 @@ jobs:
publish-crates:
name: publish crates ${{ inputs.version }}
needs: publish
- if: github.event_name == 'workflow_dispatch' && inputs.phase == 'publish' && inputs.publish_crates && !inputs.draft
+ if: >-
+ !cancelled() && needs.publish.result == 'success' && github.event_name == 'workflow_dispatch' &&
+ (inputs.phase == 'publish' || inputs.phase == 'all') && inputs.publish_crates && !inputs.draft
runs-on: ubuntu-22.04
environment: crates-io
permissions:
diff --git a/apps/archive-tool/Cargo.lock b/apps/archive-tool/Cargo.lock
index 9a19491..79dc435 100644
--- a/apps/archive-tool/Cargo.lock
+++ b/apps/archive-tool/Cargo.lock
@@ -697,14 +697,14 @@ dependencies = [
[[package]]
name = "rustolonia"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"rustolonia-sys",
]
[[package]]
name = "rustolonia-sys"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"dirs",
"flate2",
diff --git a/apps/hash-calculator/Cargo.lock b/apps/hash-calculator/Cargo.lock
index 5866506..6202ec2 100644
--- a/apps/hash-calculator/Cargo.lock
+++ b/apps/hash-calculator/Cargo.lock
@@ -486,14 +486,14 @@ dependencies = [
[[package]]
name = "rustolonia"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"rustolonia-sys",
]
[[package]]
name = "rustolonia-sys"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"dirs",
"flate2",
diff --git a/apps/pdf-viewer/Cargo.lock b/apps/pdf-viewer/Cargo.lock
index 588ae25..7e996cc 100644
--- a/apps/pdf-viewer/Cargo.lock
+++ b/apps/pdf-viewer/Cargo.lock
@@ -1588,14 +1588,14 @@ dependencies = [
[[package]]
name = "rustolonia"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"rustolonia-sys",
]
[[package]]
name = "rustolonia-sys"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"dirs",
"flate2",
diff --git a/apps/system-monitor/NeoHtop.App/Cargo.lock b/apps/system-monitor/NeoHtop.App/Cargo.lock
index 8fbe502..0da565d 100644
--- a/apps/system-monitor/NeoHtop.App/Cargo.lock
+++ b/apps/system-monitor/NeoHtop.App/Cargo.lock
@@ -563,14 +563,14 @@ dependencies = [
[[package]]
name = "rustolonia"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"rustolonia-sys",
]
[[package]]
name = "rustolonia-sys"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"dirs",
"flate2",
diff --git a/build/SharedVersion.props b/build/SharedVersion.props
index fff12c6..f831be4 100644
--- a/build/SharedVersion.props
+++ b/build/SharedVersion.props
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
Rustolonia
- 12.1.1
+ 12.1.2
Rustolonia contributors
Copyright $([System.DateTime]::Now.ToString(`yyyy`)) Rustolonia contributors. Upstream Avalonia material retains its original copyright notices.
https://github.com/Devolutions/rustolonia
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 71a732d..808d614 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -442,7 +442,7 @@ dependencies = [
[[package]]
name = "rustolonia"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"avalonia-sample",
"rustolonia-sys",
@@ -450,7 +450,7 @@ dependencies = [
[[package]]
name = "rustolonia-bindgen"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"serde",
"serde_json",
@@ -459,7 +459,7 @@ dependencies = [
[[package]]
name = "rustolonia-sys"
-version = "12.1.1"
+version = "12.1.2"
dependencies = [
"dirs",
"flate2",
diff --git a/rust/PLATFORMS.md b/rust/PLATFORMS.md
index 4db69e9..def66e3 100644
--- a/rust/PLATFORMS.md
+++ b/rust/PLATFORMS.md
@@ -91,7 +91,7 @@ Release tarballs (`rust/package-host.ps1`) define the minimum OS a published
| --- | --- | --- |
| `win-x64`, `win-arm64` | `windows-latest` / `windows-11-arm` runners | Windows 10 1607 / Server 2016 (.NET 10 support floor) |
| `linux-x64`, `linux-arm64` | `ubuntu-22.04` / `ubuntu-22.04-arm` runners | glibc 2.35 |
-| `osx-x64`, `osx-arm64` | `macos-15-intel` / `macos-14` runners, `MACOSX_DEPLOYMENT_TARGET=12.0` | macOS 12 |
+| `osx-x64`, `osx-arm64` | `macos-14` (Apple silicon) runners for both; osx-x64 is cross-built and smoke-tested under Rosetta 2; `MACOSX_DEPLOYMENT_TARGET=12.0` | macOS 12 |
Linux hosts are glibc-only; musl RIDs are not published. They need an X11
display (XWayland works), the X11 client libraries (`libX11`, `libICE`,
diff --git a/rust/PRODUCTIZATION.md b/rust/PRODUCTIZATION.md
index 52dd318..cb939ea 100644
--- a/rust/PRODUCTIZATION.md
+++ b/rust/PRODUCTIZATION.md
@@ -310,7 +310,10 @@ an absent target with the corresponding `rustup target add` command:
Packaging supports same-OS cross-architecture builds when the required native
and Rust toolchains are available. Executing a packaged application requires a
matching native runner. Cross-build artifacts are therefore not evidence of
-native runtime coverage.
+native runtime coverage. The exception is osx-x64: CI and the release build it
+on Apple silicon and launch the x64 app through Rosetta 2 instead of using the
+slow Intel macOS runners. Rosetta covers smoke launches only. Intel hardware
+and the managed test suite on x64 macOS are no longer exercised in CI.
### Signing hook
@@ -472,15 +475,67 @@ another organization repository alone does not prove this repository allows it.
### Releasing
Releases are driven by `.github/workflows/release.yml` and
-`rust/prepare-release.ps1`:
+`rust/prepare-release.ps1`. After the version bump is on `master`, one
+dispatch with `phase = all` (the default) builds, pins, tags, and publishes.
+
+#### One-dispatch release
1. `pwsh rust/prepare-release.ps1 -Version X.Y.Z` sets the version of the
three crates, the exact `rustolonia-sys` pin, `release-manifest.json`, and
`build/SharedVersion.props`, clears `host-checksums.txt`, and refreshes
- `Cargo.lock`. Commit to `master`, then dispatch `phase = build` with
+ `Cargo.lock`. Merge that change to `master` through a pull request.
+2. Dispatch **Release** from `master`:
+
+ ```powershell
+ gh workflow run release.yml --repo Devolutions/rustolonia --ref master -f phase=all -f version=12.1.2 -f publish_crates=false -f draft=false
+ ```
+
+ The run executes the build jobs described in step 2 of the manual path
+ below. Then `pin-release` verifies the staged `SHA256SUMS`, checks that
+ `master` still points at the commit the run built and that `vX.Y.Z` does
+ not exist, and runs
+ `prepare-release.ps1 -Checksums -SumsFile -CommitAndTag -SourceRevision `.
+ That makes one commit, "Pin rustolonia X.Y.Z release host checksums",
+ whose parent is the build commit and which changes only
+ `host-checksums.txt`, plus the annotated tag `vX.Y.Z` on it. Both are
+ pushed with one atomic, non-forced `git push --atomic`. If `master` moved
+ or the tag appeared in the meantime, nothing is pushed. The `publish` and
+ `publish-crates` jobs then run unchanged against that tag, as in steps 4
+ and 5 below. They take the artifacts from the same run and require the tag
+ to point at the commit `pin-release` pushed. `draft` and `publish_crates`
+ behave as in the manual path. Do not merge to `master` while the run is
+ building: the run would then fail before pushing, and a new dispatch is
+ needed.
+
+The push uses `GITHUB_TOKEN` with `contents: write`, which works while
+`master` and `v*` tags have no branch protection or rulesets (true for
+12.1.2: `master` is unprotected and the repository has no rulesets). Commits
+and tags pushed with `GITHUB_TOKEN` do not start other workflows. That is
+fine here, because publication happens in the same run, but **Avalonia Rust**
+CI does not run on the checksum commit. If `master` or tags are later
+protected, either let GitHub Actions bypass those rules or add a repository
+secret named `RELEASE_PUSH_TOKEN`. Use a fine-grained personal access token
+for an account on the bypass list, with **Contents: Read and write** on
+`Devolutions/rustolonia` only. The push step uses it when present, and
+pushes made with it do trigger CI. A GitHub App installation token cannot be
+stored as this secret because it expires after an hour. If the rules
+require signed commits, use the manual path.
+
+If any job before the push fails, nothing is committed or tagged, so fix the
+cause and dispatch `phase = all` again. If `publish` or `publish-crates`
+fails after the push, the tag already exists and a new `phase = all` run
+refuses to start. Re-run only the failed jobs of the same run
+(`gh run rerun RUN_ID --failed`). They reuse its artifacts, build commit,
+and pinned commit. `phase = publish` cannot take over, because it requires
+a `build_run_id` whose run succeeded.
+
+#### Manual two-dispatch release (fallback)
+
+1. Prepare and merge the version as above, then dispatch `phase = build` with
`version = X.Y.Z`. No build tag or GitHub release is created.
2. The build checks that the requested version matches every version, packages the
- default host on native runners for all six RIDs, builds and launches a
+ default host for all six RIDs (osx-x64 on Apple silicon, launched under
+ Rosetta 2; the rest on native runners), builds and launches a
standalone app against the packaged crates through the `rustolonia-sys`
download path (`rust/tests/test-standalone-app.ps1`), and stages an Actions
artifact named `rustolonia-release-X.Y.Z` with the six tarballs, their
@@ -491,9 +546,11 @@ Releases are driven by `.github/workflows/release.yml` and
`pwsh rust/prepare-release.ps1 -Checksums -SumsFile `
writes the default tarball hashes into
`host-checksums.txt`. Commit only this file to `master`, then push the
- final consumer tag `vX.Y.Z` on that commit. NativeAOT output is not
- guaranteed to be bit-for-bit reproducible, so the hashes always come from
- the release build itself.
+ final consumer tag `vX.Y.Z` on that commit. Adding `-CommitAndTag`
+ (optionally with `-SourceRevision `) makes the commit and
+ tag locally, with the same checks `phase = all` uses. NativeAOT output is
+ not guaranteed to be bit-for-bit reproducible, so the hashes always come
+ from the release build itself.
4. Run the **Release** workflow manually with `phase = publish` and `version = X.Y.Z` from
`master`, setting `build_run_id` to the successful build-phase run ID and
leaving `publish_crates = false` for a GitHub-only release.
@@ -516,26 +573,22 @@ Releases are driven by `.github/workflows/release.yml` and
This cannot be combined with `draft = true`. Re-running publication for
an existing release fails rather than overwriting assets.
-For the first GitHub-only release, this PR already prepares version `12.1.0`.
-After merging it, start **Release** from `master` with `phase = build`,
-`version = 12.1.0`, and `publish_crates = false`. The build phase validates
-the version and stages the six host builds without pushing any tags.
-Only the checksum-bearing `v12.1.0` tag is needed. Equivalently:
+For a GitHub-only release of 12.1.2 the manual path is, equivalently:
```powershell
-gh workflow run release.yml --ref master -f phase=build -f version=12.1.0 -f publish_crates=false
+gh workflow run release.yml --ref master -f phase=build -f version=12.1.2 -f publish_crates=false
# Wait for the build to succeed; replace RUN_ID below with its run ID.
-gh run download RUN_ID --name rustolonia-release-12.1.0 --dir release-assets
+gh run download RUN_ID --name rustolonia-release-12.1.2 --dir release-assets
# In an up-to-date master checkout:
pwsh ./rust/prepare-release.ps1 -Checksums -SumsFile release-assets/SHA256SUMS
git add rust/rustolonia-sys/host-checksums.txt
-git commit -m "Pin rustolonia 12.1.0 host checksums"
-git tag -a v12.1.0 -m "rustolonia 12.1.0"
-git push origin master v12.1.0
-gh workflow run release.yml --ref master -f phase=publish -f version=12.1.0 -f build_run_id=RUN_ID -f publish_crates=false -f draft=false
+git commit -m "Pin rustolonia 12.1.2 release host checksums"
+git tag -a v12.1.2 -m "rustolonia 12.1.2"
+git push origin master v12.1.2
+gh workflow run release.yml --ref master -f phase=publish -f version=12.1.2 -f build_run_id=RUN_ID -f publish_crates=false -f draft=false
```
-Do not push `v12.1.0` before committing the actual six host hashes: Cargo
+Do not push `vX.Y.Z` before committing the actual six host hashes: Cargo
downloads the source at the tag, not later commits on `master`. Wait for the
manual publication gate before telling consumers to use that tag. If any
source change is needed after staging the host build, run a new build before
@@ -544,9 +597,12 @@ mixing changed source with the existing artifacts. `-FromRelease` remains
available to read checksums from an existing `vX.Y.Z` release, not for staging.
The repository must allow GitHub Actions and the workflow's
-`contents: write` permission to publish the release.
-All six native runner labels must be available to the repository. No
-additional secret is required for a GitHub-only release. The provenance step
+`contents: write` permission to publish the release (and, for `phase = all`,
+to push the checksum commit and tag; see `RELEASE_PUSH_TOKEN` above).
+All five runner labels must be available to the repository (osx-x64 and
+osx-arm64 share `macos-14`). No
+additional secret is required for a GitHub-only release while `master` and
+tags are unprotected. The provenance step
uses GitHub's `id-token: write` and `attestations: write` permissions.
Signing is enabled by setting the `AVALONIA_RUST_SIGN_COMMAND` repository
diff --git a/rust/prepare-release.ps1 b/rust/prepare-release.ps1
index 0133ee4..5ae722f 100644
--- a/rust/prepare-release.ps1
+++ b/rust/prepare-release.ps1
@@ -13,8 +13,9 @@ Two steps, each followed by a commit:
rust/release-manifest.json and in build/SharedVersion.props,
clears rust/rustolonia-sys/host-checksums.txt and refreshes Cargo.lock
(and the crates' entries in the in-repository apps' lock files).
- Commit to master, then dispatch .github/workflows/release.yml with
- phase=build. It stages host tarballs and SHA256SUMS as Actions artifacts.
+ Commit to master, then dispatch .github/workflows/release.yml. With
+ phase=all (the default) that one run does everything below; with
+ phase=build it only stages host tarballs and SHA256SUMS as artifacts.
2. -Checksums (-SumsFile | -FromRelease)
Writes host-checksums.txt from SHA256SUMS (symbol tarballs are left out).
@@ -23,6 +24,11 @@ Two steps, each followed by a commit:
Commit, then push `v` on this commit. Dispatch phase=publish
with build_run_id to create the GitHub release (non-draft by default).
crates.io publishing and draft releases are opt-in.
+
+ -CommitAndTag also makes that commit (only host-checksums.txt) and the
+ annotated tag v, without pushing. It refuses a dirty tree, an
+ existing tag, or a HEAD other than -SourceRevision (the build source).
+ phase=all runs it, then pushes master and the tag atomically.
#>
[CmdletBinding(DefaultParameterSetName = 'Version')]
param(
@@ -38,6 +44,10 @@ param(
[string]$SumsFile,
[Parameter(ParameterSetName = 'Checksums')]
[switch]$FromRelease,
+ [Parameter(ParameterSetName = 'Checksums')]
+ [switch]$CommitAndTag,
+ [Parameter(ParameterSetName = 'Checksums')]
+ [string]$SourceRevision,
[string]$RustoloniaRoot = (Split-Path -Parent $PSScriptRoot)
)
@@ -90,11 +100,22 @@ if ($PSCmdlet.ParameterSetName -eq 'Version') {
}
}
$null = Get-RustoloniaReleaseVersion -RustoloniaRoot $root
- Write-Host "Version set to $Version. Commit to master, then dispatch phase=build (no tag needed)."
+ Write-Host "Version set to $Version. Merge to master, then dispatch release.yml with phase=all (no tag needed)."
return
}
$version = Get-RustoloniaReleaseVersion -RustoloniaRoot $root
+$checksumPath = 'rust/rustolonia-sys/host-checksums.txt'
+if ($SourceRevision -and -not $CommitAndTag) { throw '-SourceRevision is only used with -CommitAndTag.' }
+if ($CommitAndTag) {
+ $head = (& git -C $root rev-parse HEAD).Trim()
+ if ($SourceRevision) {
+ $source = (& git -C $root rev-parse --verify "$SourceRevision^{commit}").Trim()
+ if ($head -ne $source) { throw "HEAD $head is not the build source revision $source; rebuild from the current master." }
+ }
+ if (@(& git -C $root status --porcelain --untracked-files=no).Count -gt 0) { throw 'The working tree has changes; -CommitAndTag commits only host-checksums.txt.' }
+ if (@(& git -C $root tag --list "v$version").Count -gt 0) { throw "Tag v$version already exists." }
+}
if ($FromRelease) {
if ($SumsFile) { throw 'Pass either -SumsFile or -FromRelease, not both.' }
$download = Join-Path ([IO.Path]::GetTempPath()) ('rustolonia-sums-' + [guid]::NewGuid().ToString('N'))
@@ -127,3 +148,15 @@ foreach ($rid in $script:RidTargets.Keys) {
$lines = @($entries.Keys | Sort-Object -CaseSensitive | ForEach-Object { "$($entries[$_]) $_" })
Write-Utf8Lf $checksumFile ("$checksumHeader`n" + ($lines -join "`n") + "`n")
Write-Host "Wrote $($lines.Count) checksums for v$version to $checksumFile."
+if (-not $CommitAndTag) { return }
+
+$changed = @(& git -C $root status --porcelain --untracked-files=no | ForEach-Object { $_.Substring(3) })
+if ($changed.Count -eq 0) { throw "host-checksums.txt already lists these hashes; the build source must not carry v$version checksums." }
+if ($changed.Count -ne 1 -or $changed[0] -ne $checksumPath) { throw "Unexpected changes: $($changed -join ', ')" }
+& git -C $root add -- $checksumPath
+& git -C $root commit --quiet -m "Pin rustolonia $version release host checksums"
+& git -C $root tag -a "v$version" -m "rustolonia $version"
+$parent = (& git -C $root rev-parse 'HEAD^').Trim()
+$committed = @(& git -C $root diff --name-only $head HEAD)
+if ($parent -ne $head -or $committed.Count -ne 1 -or $committed[0] -ne $checksumPath) { throw 'The checksum commit must change only host-checksums.txt on top of the build source.' }
+Write-Host "Committed $((& git -C $root rev-parse HEAD).Trim()) and tagged v$version. Push with: git push --atomic origin HEAD:refs/heads/master v$version"
diff --git a/rust/release-manifest.json b/rust/release-manifest.json
index efc7d83..ca502ce 100644
--- a/rust/release-manifest.json
+++ b/rust/release-manifest.json
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
- "rustoloniaVersion": "12.1.1",
+ "rustoloniaVersion": "12.1.2",
"producerPin": "8eeda4f6f546165b3f72e63c9f42247abb306905",
"producerRemote": "https://github.com/AvaloniaUI/Avalonia",
"patches": [
diff --git a/rust/rustolonia-bindgen/Cargo.toml b/rust/rustolonia-bindgen/Cargo.toml
index 4ac8d6f..da3cf88 100644
--- a/rust/rustolonia-bindgen/Cargo.toml
+++ b/rust/rustolonia-bindgen/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rustolonia-bindgen"
-version = "12.1.1"
+version = "12.1.2"
edition = "2021"
license = "MIT"
description = "Code generator that emits rustolonia-sys/rustolonia from projection.ir.json"
diff --git a/rust/rustolonia-sys/Cargo.toml b/rust/rustolonia-sys/Cargo.toml
index 5c81e70..56b6cdf 100644
--- a/rust/rustolonia-sys/Cargo.toml
+++ b/rust/rustolonia-sys/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rustolonia-sys"
-version = "12.1.1"
+version = "12.1.2"
edition = "2021"
license = "MIT"
description = "Raw nano-COM bindings for the Avalonia NativeAOT host"
diff --git a/rust/rustolonia-sys/host-checksums.txt b/rust/rustolonia-sys/host-checksums.txt
index 9a8227d..2c3ece2 100644
--- a/rust/rustolonia-sys/host-checksums.txt
+++ b/rust/rustolonia-sys/host-checksums.txt
@@ -2,9 +2,3 @@
# version, in `sha256sum` format. build.rs refuses to use a downloaded host
# whose hash is not listed here. rust/prepare-release.ps1 fills this in from
# the release build before the consumer tag or crates are published.
-d4128440d820e43478f57e0390dc2014b01175b0ccce334790faf403b7a4fbe4 rustolonia-host-12.1.1-linux-arm64.tar.gz
-5a5df2ab385289e8856d40ec083abefde0859204cd9a2f0b546f0bcf0679f761 rustolonia-host-12.1.1-linux-x64.tar.gz
-d22d774c76b0b2afc8d1a8449720fb095d6633cce3479504e0f22a58fed1a42e rustolonia-host-12.1.1-osx-arm64.tar.gz
-a8b56f3fbe4cb6d1f372e70849b91dcf2d4258dd80f24f32a78581a0e402bb9b rustolonia-host-12.1.1-osx-x64.tar.gz
-fdae3d82a4bd7b9357293525d48191bcf7789dc8542d554ae711dff310cfd2b3 rustolonia-host-12.1.1-win-arm64.tar.gz
-767c8337cae2987a308beac38bf008850fc737589354d31345ae4753c2aa03bb rustolonia-host-12.1.1-win-x64.tar.gz
diff --git a/rust/rustolonia/Cargo.toml b/rust/rustolonia/Cargo.toml
index 1a26feb..934efbf 100644
--- a/rust/rustolonia/Cargo.toml
+++ b/rust/rustolonia/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "rustolonia"
-version = "12.1.1"
+version = "12.1.2"
edition = "2021"
license = "MIT"
description = "Idiomatic Rust API for Avalonia"
@@ -19,7 +19,7 @@ no-download = ["rustolonia-sys/no-download"]
dev-host-path = ["rustolonia-sys/dev-host-path"]
[dependencies]
-rustolonia-sys = { version = "=12.1.1", path = "../rustolonia-sys", default-features = false }
+rustolonia-sys = { version = "=12.1.2", path = "../rustolonia-sys", default-features = false }
[dev-dependencies]
avalonia-sample = { path = "../avalonia-sample" }
diff --git a/rust/tests/test-build-app.ps1 b/rust/tests/test-build-app.ps1
index 25ce200..aa0fbdb 100644
--- a/rust/tests/test-build-app.ps1
+++ b/rust/tests/test-build-app.ps1
@@ -729,6 +729,34 @@ version = "0.1.0"
[IO.File]::WriteAllLines($sumsFile, [string[]]($sumsLines | Select-Object -Skip 2))
Assert-Throws { & $prepareRelease -Checksums -SumsFile $sumsFile -RustoloniaRoot $releaseRoot } 'no entry for rustolonia-host-9\.8\.7-rc\.1-win-x64'
+ # phase=all commits the checksums on the build source and tags it.
+ [IO.File]::WriteAllLines($sumsFile, [string[]]$sumsLines)
+ & $prepareRelease -Version 9.8.7-rc.1 -SkipCargoUpdate -RustoloniaRoot $releaseRoot | Out-Null
+ & git -C $releaseRoot init --quiet
+ foreach ($setting in @('user.name=Release Test', 'user.email=release@example.invalid', 'commit.gpgsign=false', 'tag.gpgsign=false', 'core.autocrlf=false')) {
+ $key, $value = $setting -split '=', 2
+ & git -C $releaseRoot config $key $value
+ }
+ & git -C $releaseRoot add --all
+ & git -C $releaseRoot commit --quiet -m 'Prepare release 9.8.7-rc.1'
+ $releaseSource = (& git -C $releaseRoot rev-parse HEAD).Trim()
+ Assert-Throws { & $prepareRelease -Checksums -SumsFile $sumsFile -RustoloniaRoot $releaseRoot -SourceRevision $releaseSource } 'only used with -CommitAndTag'
+ $propsPath = Join-Path $releaseRoot 'build/SharedVersion.props'
+ Add-Content -LiteralPath $propsPath -Value ''
+ Assert-Throws { & $prepareRelease -Checksums -SumsFile $sumsFile -CommitAndTag -SourceRevision $releaseSource -RustoloniaRoot $releaseRoot } 'working tree has changes'
+ & git -C $releaseRoot checkout --quiet -- build/SharedVersion.props
+ & $prepareRelease -Checksums -SumsFile $sumsFile -CommitAndTag -SourceRevision $releaseSource -RustoloniaRoot $releaseRoot | Out-Null
+ Assert-True ((& git -C $releaseRoot rev-parse 'HEAD^').Trim() -eq $releaseSource) 'The checksum commit must sit directly on the build source.'
+ Assert-True ((& git -C $releaseRoot rev-parse 'v9.8.7-rc.1^{commit}').Trim() -eq (& git -C $releaseRoot rev-parse HEAD).Trim()) 'The release tag must point at the checksum commit.'
+ Assert-True ((& git -C $releaseRoot cat-file -t 'v9.8.7-rc.1').Trim() -eq 'tag') 'The release tag must be annotated.'
+ Assert-True (((& git -C $releaseRoot diff --name-only $releaseSource HEAD) -join ',') -eq 'rust/rustolonia-sys/host-checksums.txt') 'The checksum commit must change only host-checksums.txt.'
+ Assert-True ((& git -C $releaseRoot log -1 --format=%s).Trim() -eq 'Pin rustolonia 9.8.7-rc.1 release host checksums') 'Unexpected checksum commit message.'
+ Assert-True (@(& git -C $releaseRoot status --porcelain).Count -eq 0) 'The checksum commit must leave a clean tree.'
+ Assert-Throws { & $prepareRelease -Checksums -SumsFile $sumsFile -CommitAndTag -SourceRevision $releaseSource -RustoloniaRoot $releaseRoot } 'is not the build source revision'
+ Assert-Throws { & $prepareRelease -Checksums -SumsFile $sumsFile -CommitAndTag -RustoloniaRoot $releaseRoot } 'Tag v9\.8\.7-rc\.1 already exists'
+ & git -C $releaseRoot tag -d v9.8.7-rc.1 | Out-Null
+ Assert-Throws { & $prepareRelease -Checksums -SumsFile $sumsFile -CommitAndTag -RustoloniaRoot $releaseRoot } 'already lists these hashes'
+
if ($RunNativeSmoke) {
$nativeTempRoot = [IO.Path]::GetTempPath()
if ($IsMacOS) {
diff --git a/rust/tests/test-host-download.ps1 b/rust/tests/test-host-download.ps1
index e52407a..dbafcc9 100644
--- a/rust/tests/test-host-download.ps1
+++ b/rust/tests/test-host-download.ps1
@@ -15,6 +15,9 @@ With -AppManifest, builds that standalone Cargo package (binary -AppName)
instead of a repository example. -ShipHost copies the cached host next to the
executable before launching, as a distributable build without the
`dev-host-path` feature requires.
+
+An osx-x64 -Rid on Apple silicon builds for x86_64-apple-darwin and
+launches through Rosetta 2, which must be installed.
#>
param(
[Parameter(Mandatory)]
@@ -77,15 +80,24 @@ foreach ($name in $overrides.Keys) {
try {
$targetDir = Join-Path $WorkDirectory 'target'
$exeSuffix = if ($IsWindows) { '.exe' } else { '' }
+ $ridTarget = Get-RidTargetInfo -Rid $Rid
+ $cargoTarget = @()
+ $outputDir = Join-Path $targetDir 'release'
+ if ($ridTarget.Arch -ne (Get-CurrentRuntimeArchitecture)) {
+ # Apple silicon runs x64 apps through Rosetta 2; other cross RIDs cannot launch here.
+ if (-not ($IsMacOS -and $ridTarget.Arch -eq 'x64')) { throw "A $Rid app cannot be launched on this $(Get-CurrentRuntimeArchitecture) machine." }
+ $cargoTarget = @('--target', $ridTarget.Triple)
+ $outputDir = Join-Path $targetDir $ridTarget.Triple 'release'
+ }
if ($AppManifest) {
if (-not $AppName) { throw '-AppName is required with -AppManifest.' }
- cargo build --manifest-path $AppManifest --release --target-dir $targetDir
- $exe = Join-Path $targetDir 'release' ($AppName + $exeSuffix)
+ cargo build --manifest-path $AppManifest --release --target-dir $targetDir @cargoTarget
+ $exe = Join-Path $outputDir ($AppName + $exeSuffix)
$Example = $AppName
} else {
cargo build --locked --manifest-path (Join-Path $RustoloniaRoot 'rust' 'Cargo.toml') `
- -p rustolonia --example $Example --release --target-dir $targetDir
- $exe = Join-Path $targetDir 'release' 'examples' ($Example + $exeSuffix)
+ -p rustolonia --example $Example --release --target-dir $targetDir @cargoTarget
+ $exe = Join-Path $outputDir 'examples' ($Example + $exeSuffix)
}
$hostFile = Join-Path $cache $version $Rid (Get-RidTargetInfo -Rid $Rid).HostFileName