Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
115 changes: 80 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ jobs:
RELEASE_RUNTIME_PROFILES: ${{ matrix.runtime_profiles }}
RELEASE_BINARY_NAME: ${{ matrix.binary_name }}
RELEASE_BINARY_PATH: ${{ matrix.binary_path }}
RELEASE_MSI_PROFILE: ${{ matrix.msi_profile }}
RELEASE_MSI_PROFILES: ${{ matrix.msi_profiles }}
RELEASE_MSIS: ${{ matrix.msis }}
RELEASE_NEEDS_PROVIDER_ARCHIVE: ${{ matrix.needs_provider_archive }}
RELEASE_NEEDS_INPROCESS_TOOLCHAIN: ${{ matrix.needs_inprocess_toolchain }}
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
Expand Down Expand Up @@ -531,56 +532,100 @@ jobs:
Write-Host "Packaged and smoked: $($produced -join ', ')"
$LASTEXITCODE = 0

# One installer, for the recommended package. The MSI is cut from the
# staged bundle directory the assemble step above left in place, so it
# contains exactly what the verified archive contains.
- name: Build the recommended MSI installer
if: matrix.msi_profile != ''
# Each Windows profile gets an independent MSI family cut from the exact
# staged bundle that was already packaged and smoked above.
- name: Build and smoke Windows MSI installers
if: matrix.msi_profiles != ''
shell: pwsh
run: |
$target = $env:RELEASE_TARGET
$profile = $env:RELEASE_MSI_PROFILE
$version = $env:RELEASE_VERSION
$profiles = $env:RELEASE_MSI_PROFILES.Split(
',', [System.StringSplitOptions]::RemoveEmptyEntries
)
$expected = $env:RELEASE_MSIS.Split(
',', [System.StringSplitOptions]::RemoveEmptyEntries
)
if ($profiles.Count -ne $expected.Count) {
throw "MSI profile/name matrix length mismatch: $($profiles.Count) profiles, $($expected.Count) names."
}
$contract = Get-Content 'packaging/toolchains/release-contract.json' -Raw |
ConvertFrom-Json -AsHashtable
$variant = $contract['variants'][$target]['profiles'][$profile]
$bundleName = ([string]$variant['archive_root_template']).Replace('{version}', $version)
$outputDir = Join-Path $env:RUNNER_TEMP 'oscan-release-artifacts'
$bundleDir = Join-Path (Join-Path $outputDir 'stage') $bundleName
if (-not (Test-Path -LiteralPath $bundleDir -PathType Container)) {
throw "The staged $target/$profile bundle is missing at $bundleDir; the MSI must be built from the bundle that was packaged and smoked."
}
$uploadDir = Join-Path $env:GITHUB_WORKSPACE 'release-upload'
New-Item -ItemType Directory -Path $uploadDir -Force | Out-Null
$msiPath = Join-Path $uploadDir "$bundleName.msi"
& ./scripts/build-msi.ps1 -BundleDir $bundleDir -Version $version -OutputPath $msiPath
if (-not (Test-Path -LiteralPath $msiPath -PathType Leaf)) {
throw "The MSI build produced no installer at $msiPath."
}
$msiScratch = Join-Path $env:RUNNER_TEMP "oscan-release-msi/$target-$profile"
$msiScratch = Join-Path $env:RUNNER_TEMP "oscan-release-msi/$target"
if (Test-Path -LiteralPath $msiScratch) {
Remove-Item -LiteralPath $msiScratch -Recurse -Force
}
$adminRoot = Join-Path $msiScratch 'admin'
New-Item -ItemType Directory -Path $adminRoot -Force | Out-Null
$msiLog = Join-Path $msiScratch 'administrative-extract.log'
$process = Start-Process -FilePath msiexec.exe -ArgumentList @(
'/a', "`"$msiPath`"", '/qn', "TARGETDIR=`"$adminRoot`"",
'/l*v', "`"$msiLog`""
) -Wait -PassThru
if ($process.ExitCode -ne 0) {
Get-Content -LiteralPath $msiLog -Tail 100
throw "Administrative extraction of $msiPath failed with exit code $($process.ExitCode)."
$produced = @()

foreach ($profile in $profiles) {
$variant = $contract['variants'][$target]['profiles'][$profile]
$bundleName = ([string]$variant['archive_root_template']).Replace('{version}', $version)
$bundleDir = Join-Path (Join-Path $outputDir 'stage') $bundleName
if (-not (Test-Path -LiteralPath $bundleDir -PathType Container)) {
throw "The staged $target/$profile bundle is missing at $bundleDir; the MSI must be built from the bundle that was packaged and smoked."
}
$msiName = ([string]$variant['msi_name_template']).Replace('{version}', $version)
if ($produced -contains $msiName) {
throw "Two Windows profiles claim the same MSI name '$msiName'."
}
$msiPath = Join-Path $uploadDir $msiName
& ./scripts/build-msi.ps1 `
-BundleDir $bundleDir `
-Version $version `
-OutputPath $msiPath
if (-not (Test-Path -LiteralPath $msiPath -PathType Leaf)) {
throw "The MSI build produced no installer at $msiPath."
}

$msiLog = Join-Path $msiScratch "administrative-extract-$profile.log"
$process = Start-Process -FilePath msiexec.exe -ArgumentList @(
'/a', "`"$msiPath`"", '/qn', "TARGETDIR=`"$adminRoot`"",
'/l*v', "`"$msiLog`""
) -Wait -PassThru
if ($process.ExitCode -ne 0) {
Get-Content -LiteralPath $msiLog -Tail 100
throw "Administrative extraction of $msiPath failed with exit code $($process.ExitCode)."
}

$installedRoot = Join-Path $adminRoot "PFiles64\Oscan\profiles\$profile\$version"
& ./scripts/smoke-release.ps1 `
-Target $target `
-Profile $profile `
-InstalledPackageDir $installedRoot `
-Version $version `
-ScratchDir (Join-Path $msiScratch "smoke-$profile")
if ($LASTEXITCODE -ne 0) {
throw "The MSI payload smoke failed for $target/$profile."
}

$qualifiedCommand = Join-Path $adminRoot "PFiles64\Oscan\bin\oscan-$profile.cmd"
if (-not (Test-Path -LiteralPath $qualifiedCommand -PathType Leaf)) {
throw "The $profile MSI did not install its qualified command."
}
& $qualifiedCommand --version | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "The administratively extracted command '$qualifiedCommand' failed."
}
$produced += $msiName
}

$missing = @($expected | Where-Object { $produced -notcontains $_ })
$unexpected = @($produced | Where-Object { $expected -notcontains $_ })
if ($missing.Count -gt 0 -or $unexpected.Count -gt 0) {
throw "MSI set differs from the release contract. Missing: $($missing -join ', '); unexpected: $($unexpected -join ', ')."
}
$selector = Join-Path $adminRoot 'PFiles64\Oscan\bin\oscan.cmd'
if (-not (Test-Path -LiteralPath $selector -PathType Leaf)) {
throw "The coinstalled MSI image has no shared oscan selector."
}
$installedRoot = Join-Path $adminRoot 'PFiles64\Oscan'
& ./scripts/smoke-release.ps1 `
-Target $target `
-Profile $profile `
-InstalledPackageDir $installedRoot `
-Version $version `
-ScratchDir (Join-Path $msiScratch 'smoke')
& $selector --version | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "The MSI payload smoke failed for $target/$profile."
throw "The coinstalled MSI selector failed."
}

- name: Upload packaged artifacts
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ to use LLVM, Cranelift, and C from one compiler with `--backend`.

| Platform | Recommended download | Notes |
|---|---|---|
| Windows x86_64 | `oscan-vX.Y.Z-windows-x86_64-llvm.msi` or `oscan-vX.Y.Z-windows-x86_64-llvm.zip` | LLVM remains the transition default; `-full.zip` includes all backends |
| Windows x86_64 | `oscan-vX.Y.Z-windows-x86_64-llvm.msi` or `.zip` | Every profile has both formats; `-full.msi`/`.zip` includes all backends |
| Linux x86_64 | `oscan-vX.Y.Z-linux-x86_64-llvm.tar.xz` | `-full.tar.xz` includes all backends; LLVM requires glibc 2.34+ |
| macOS x86_64 | `oscan-vX.Y.Z-macos-x86_64-c.tar.gz` | Requires Xcode Command Line Tools |

Windows and Linux provide `full`, `llvm`, `cranelift`, and `c` profiles. Full
contains one backend-neutral compiler; the others are smaller slim packages.
Windows publishes both `.msi` and `.zip` for all four profiles. Profile MSIs
upgrade independently and may be installed together. If the legacy flat LLVM
MSI is installed, run the current LLVM MSI once to migrate it before installing
another MSI profile.
Each release
includes `SHA256SUMS`; keep the downloaded file's original name and verify it
before installing. The [installation guide](docs/guide.md#installation) has
Expand Down
44 changes: 31 additions & 13 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,19 @@ list and what each package contains.

**Windows x86_64:**

1. Download `oscan-vX.Y.Z-windows-x86_64-llvm.zip` (recommended slim),
`-full.zip`, `-cranelift.zip`, or `-c.zip`; the transition MSI remains
`oscan-vX.Y.Z-windows-x86_64-llvm.msi`
1. Download the recommended slim
`oscan-vX.Y.Z-windows-x86_64-llvm.msi` (or `.zip`), or choose the matching
`-full`, `-cranelift`, or `-c` asset. Every profile is published as both
`.msi` and `.zip`.
2. Verify exactly that asset against the release's `SHA256SUMS` (filter the
file to the asset you downloaded rather than checking every line):
```powershell
$asset = 'oscan-vX.Y.Z-windows-x86_64-llvm.zip'
$expected = (Select-String -Path SHA256SUMS -Pattern "\s\*?$([regex]::Escape($asset))$").Line.Split(' ')[0]
if ((Get-FileHash $asset -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected.ToLowerInvariant()) { throw "checksum mismatch" }
```
3. Extract and run the included `install.ps1`; pass `-SetDefault` to change an
existing unqualified `oscan` selection. For the MSI, just run it.
3. For a ZIP, extract and run `install.ps1`; pass `-SetDefault` to change an
existing unqualified `oscan` selection. For an MSI, just run it.
4. Verify the qualified command, for example `oscan-full --version`

`scripts/install-latest.ps1 -Profile full -SetDefault` installs the all-backend
Expand Down Expand Up @@ -153,17 +154,34 @@ and preserves every other profile.
`-Uninstall` or `--uninstall`. It removes only the named profile and qualified
command. If it was selected, the unqualified selector is removed rather than
redirected implicitly.
- **Upgrade (Windows MSI):** Run the new LLVM MSI; it replaces the previous MSI install in place.
- **Uninstall (Windows MSI):** Use **Settings → Apps → Installed apps → Oscan → Uninstall**, or run `msiexec /x oscan-vX.Y.Z-windows-x86_64-llvm.msi /quiet`. Do **not** just delete the installed directory: that leaves the product registered with Windows Installer.

The legacy LLVM MSI retains its existing product identity and unqualified PATH
command. The quick installer and MSI detect a per-user archive selector for the
same Windows account and refuse the ambiguous PATH setup unless explicitly
overridden with `-AllowMsiCommandConflict` or
`OSCAN_ALLOW_ARCHIVE_CONFLICT=1`, respectively.
- **Upgrade (Windows MSI):** Run the newer MSI for that profile. `Oscan Full`,
`Oscan LLVM`, `Oscan Cranelift`, and `Oscan C` are independent product
families, so upgrading one preserves the others.
- **Uninstall (Windows MSI):** Use **Settings → Apps → Installed apps** and
uninstall the named profile, or run
`msiexec /x oscan-vX.Y.Z-windows-x86_64-<profile>.msi /quiet`. Do **not**
delete its directory manually.

MSIs install isolated payloads under
`Program Files\Oscan\profiles\<profile>\<version>` and expose
`oscan-full`, `oscan-llvm`, `oscan-cranelift`, and `oscan-c` through a shared
`Program Files\Oscan\bin`. Their reference-counted `oscan` selector chooses the
first installed command in this order: full, LLVM, Cranelift, C; uninstalling a
profile falls through to the next one. The LLVM family retains the historical
UpgradeCode so a new LLVM MSI upgrades the old single-product MSI.

The quick installer and MSI detect a per-user archive selector for the same
Windows account and a matching profile-qualified archive command. They refuse
the ambiguous PATH setup unless explicitly overridden with
`-AllowMsiCommandConflict` or `OSCAN_ALLOW_ARCHIVE_CONFLICT=1`, respectively.
Legacy flat archive installs are left intact and reported; confirm the new
qualified command works, then remove the old directory/PATH entry.

The pre-profile LLVM MSI installed `oscan.exe` directly under
`Program Files\Oscan`. Run the current LLVM MSI once to migrate that product
before installing Full, Cranelift, or C; those MSI families block while the
legacy executable could shadow the shared selector.

---

## Basics
Expand Down
32 changes: 17 additions & 15 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Release builds are handled by GitHub Actions workflows. Two manual workflows mus

`packaging/toolchains/release-contract.json` (`schema_version: 3`) is the
single source of truth, and every published artifact is one **(target,
profile) pair**. A release therefore publishes **nine archives, one MSI and
profile) pair**. A release therefore publishes **nine archives, four MSIs and
one `SHA256SUMS`** — and nothing else:

| Target | Profile | Archive | Components staged |
Expand All @@ -21,11 +21,12 @@ one `SHA256SUMS`** — and nothing else:
| `linux-x86_64` | `c` | `oscan-v{version}-linux-x86_64-c.tar.xz` | compiler, pinned C toolchain |
| `macos-x86_64` | `c` | `oscan-v{version}-macos-x86_64-c.tar.gz` | compiler only (uses the host's Apple Command Line Tools) |

Plus exactly **one installer**: `oscan-v{version}-windows-x86_64-llvm.msi`,
cut from the *already staged and smoked* Windows LLVM bundle directory. The
MSI profile is declared by the contract (`ci-matrix`'s `msi_profile`), so
adding a second installer is a contract change, not a workflow tweak. It
remains `llvm`. `native` is never an
Each Windows row also publishes a matching `.msi`, cut from the *already staged
and smoked* bundle directory. MSI names, product names, and distinct
UpgradeCodes are declared by the contract; the LLVM UpgradeCode remains the
historical one for in-place migration. The other families block while the
legacy flat LLVM executable remains, forcing that migration before their shared
selector can be shadowed. `native` is never an
artifact label — it survives only as the compiler's deprecated `--backend`
alias for `cranelift`.

Expand Down Expand Up @@ -95,19 +96,20 @@ deliberately not cached.
also resolves the pinned LLVM SDK/source, builds or restores the patched
static LLVM/LLD toolchain, and prepares strict in-memory link inputs. It then
builds one compiler per profile and, for each profile, runs assemble → smoke
→ stage for upload. Windows additionally cuts the recommended LLVM MSI from
the staged strict bundle.
→ stage for upload. Windows additionally cuts and administratively
coextracts all four profile MSIs from those staged bundles, then smokes each
extracted payload and its qualified command.
3. `checksums` — collect every `.zip`/`.msi`/`.tar.gz`/`.tar.xz`, refuse
duplicate asset names, and write one `SHA256SUMS` over all of them.
4. `publish` — only when publishing is enabled; uploads the archives, the MSI
4. `publish` — only when publishing is enabled; uploads the archives, the MSIs
and `SHA256SUMS` to the GitHub release.

### Dry run without publishing

Run the workflow manually (Actions → "Oscan Release" → Run workflow) with a
version and **`publish` cleared**. `prepare` then reports
`should_publish=false`, the `publish` job is skipped, and the run still builds,
smokes and checksums all nine archives and the MSI — the supported way to
smokes and checksums all nine archives and four MSIs — the supported way to
rehearse a release. A tag push (`v*`) always publishes.

### Assembling and smoking one variant by hand
Expand Down Expand Up @@ -766,9 +768,9 @@ input is prepared once and reused by that target's profiles):
object variants; `-ToolchainArchive` or `-LlvmProviderArchive` where
declared), so no package receives undeclared inputs. Only full and C carry
a C compiler; slim LLVM and Cranelift freestanding final links need none.
7. **Cut the recommended MSI** from the staged Windows LLVM bundle directory
(Windows only), so the installer contains exactly what the smoked archive
contains.
7. **Cut all four Windows profile MSIs** from their staged bundle directories,
coextract them into one administrative image, and smoke every isolated
payload plus the shared/qualified command layout.

CI (`ci.yml`) keeps its main `linux` and `windows` jobs building *without*
`OSCAN_EMBED_ASSETS_DIR`, so the dev/external-toolchain path stays covered.
Expand Down Expand Up @@ -876,7 +878,7 @@ git push origin v0.0.12
```

The Release workflow then builds one compiler per profile for each target,
assembles and smokes all nine archives, cuts the recommended Windows LLVM
MSI, writes `SHA256SUMS` over everything, and publishes the result to GitHub
assembles and smokes all nine archives, cuts all four Windows profile MSIs,
writes `SHA256SUMS` over everything, and publishes the result to GitHub
Releases. To rehearse without publishing, use the manual workflow with
`publish` cleared (see "Dry run without publishing" above).
Loading
Loading