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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ jobs:
run: cargo test

- name: Archive installer coexistence tests
run: python -m unittest scripts.test_installers.WindowsInstallerCoexistenceTests
run: python -m unittest scripts.test_installers.UnixInstallerCoexistenceTests

- name: Integration tests — positive (freestanding)
shell: bash
Expand Down Expand Up @@ -413,6 +413,14 @@ jobs:
- name: Run unit tests (53 tests)
run: cargo test

- name: Install Python
uses: actions/setup-python@v6
with:
python-version: "3.x"

- name: Archive installer coexistence tests
run: python -m unittest scripts.test_installers.WindowsInstallerCoexistenceTests

- name: Integration tests — positive (freestanding)
shell: pwsh
run: |
Expand Down
98 changes: 94 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,34 @@ jobs:
throw "The release contract lists no profiles for ${env:RELEASE_TARGET}."
}
$binRoot = Join-Path $env:RUNNER_TEMP 'oscan-release-binaries'
$sharedBinDir = Join-Path $binRoot 'shared'
New-Item -ItemType Directory -Path $sharedBinDir -Force | Out-Null
Remove-Item Env:OSCAN_DISTRIBUTION_PROFILE, Env:OSCAN_DEFAULT_BACKEND, `
Env:OSCAN_DISTRIBUTION_BACKEND, Env:OSCAN_INPROCESS_TOOLCHAIN_DIR, `
Env:OSCAN_INPROCESS_ASSETS_DIR, Env:RUSTFLAGS -ErrorAction SilentlyContinue
$env:OSCAN_VERSION = $env:RELEASE_TAG
if ($env:RELEASE_TARGET -eq 'windows-x86_64') {
$env:RUSTFLAGS = '-C target-feature=+crt-static'
}
Write-Host "::group::cargo build shared profile selectors"
# backend-c satisfies the package build contract without pulling in
# LLVM; selector code itself is backend-independent and is built once.
& cargo build --release --no-default-features --features backend-c `
--bin oscan-selector --bin oscan-default
if ($LASTEXITCODE -ne 0) {
throw "cargo build failed for the shared profile selectors."
}
$executableSuffix = [System.IO.Path]::GetExtension($env:RELEASE_BINARY_NAME)
foreach ($selectorName in @('oscan-selector', 'oscan-default')) {
$selectorOutput = Join-Path 'target/release' "$selectorName$executableSuffix"
if (-not (Test-Path -LiteralPath $selectorOutput -PathType Leaf)) {
throw "cargo produced no shared selector at $selectorOutput."
}
Copy-Item -LiteralPath $selectorOutput `
-Destination (Join-Path $sharedBinDir "$selectorName$executableSuffix") -Force
}
Write-Host "::endgroup::"

foreach ($profile in $profiles) {
$variant = $variants[$profile]
$profileSpec = $contract['profiles'][$profile]
Expand All @@ -406,7 +434,7 @@ jobs:
$env:OSCAN_DISTRIBUTION_PROFILE = $profile
$env:OSCAN_DEFAULT_BACKEND = [string]$profileSpec['default_backend']
$env:OSCAN_VERSION = $env:RELEASE_TAG
& cargo build --release --no-default-features --features $featureList
& cargo build --release --no-default-features --features $featureList --bin oscan
if ($LASTEXITCODE -ne 0) {
throw "cargo build failed for profile '$profile'."
}
Expand All @@ -433,6 +461,14 @@ jobs:
$variants = $contract['variants'][$target]['profiles']
$profiles = $env:RELEASE_PROFILES.Split(',', [System.StringSplitOptions]::RemoveEmptyEntries)
$binRoot = Join-Path $env:RUNNER_TEMP 'oscan-release-binaries'
$executableSuffix = [System.IO.Path]::GetExtension($env:RELEASE_BINARY_NAME)
$selectorBinary = Join-Path (Join-Path $binRoot 'shared') "oscan-selector$executableSuffix"
$defaultManagerBinary = Join-Path (Join-Path $binRoot 'shared') "oscan-default$executableSuffix"
foreach ($sharedBinary in @($selectorBinary, $defaultManagerBinary)) {
if (-not (Test-Path -LiteralPath $sharedBinary -PathType Leaf)) {
throw "No shared selector binary was built for $target (expected $sharedBinary)."
}
}
$outputDir = Join-Path $env:RUNNER_TEMP 'oscan-release-artifacts'
$uploadDir = Join-Path $env:GITHUB_WORKSPACE 'release-upload'
New-Item -ItemType Directory -Path $uploadDir -Force | Out-Null
Expand All @@ -456,6 +492,8 @@ jobs:
Profile = $profile
Version = $version
BinaryPath = $binary
SelectorBinaryPath = $selectorBinary
DefaultManagerBinaryPath = $defaultManagerBinary
OutputDir = $outputDir
}
if ($components -contains 'direct_link_sidecar') {
Expand Down Expand Up @@ -560,7 +598,10 @@ jobs:
}
$adminRoot = Join-Path $msiScratch 'admin'
New-Item -ItemType Directory -Path $adminRoot -Force | Out-Null
$env:LOCALAPPDATA = Join-Path $msiScratch 'local-app-data'
New-Item -ItemType Directory -Path $env:LOCALAPPDATA -Force | Out-Null
$produced = @()
$sharedSelectorDigests = $null

foreach ($profile in $profiles) {
$variant = $contract['variants'][$target]['profiles'][$profile]
Expand All @@ -569,6 +610,15 @@ jobs:
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."
}
$bundleSelectorDigests = @(
(Get-FileHash -LiteralPath (Join-Path $bundleDir 'oscan-selector.exe') -Algorithm SHA256).Hash,
(Get-FileHash -LiteralPath (Join-Path $bundleDir 'oscan-default.exe') -Algorithm SHA256).Hash
)
if ($null -eq $sharedSelectorDigests) {
$sharedSelectorDigests = $bundleSelectorDigests
} elseif (($bundleSelectorDigests -join ',') -ne ($sharedSelectorDigests -join ',')) {
throw "The $profile package does not contain the exact shared selector binaries used by its sibling profiles."
}
$msiName = ([string]$variant['msi_name_template']).Replace('{version}', $version)
if ($produced -contains $msiName) {
throw "Two Windows profiles claim the same MSI name '$msiName'."
Expand All @@ -593,10 +643,12 @@ jobs:
}

$installedRoot = Join-Path $adminRoot "PFiles64\Oscan\profiles\$profile\$version"
$installedSelectorDir = Join-Path $adminRoot 'PFiles64\Oscan\bin'
& ./scripts/smoke-release.ps1 `
-Target $target `
-Profile $profile `
-InstalledPackageDir $installedRoot `
-InstalledSelectorDir $installedSelectorDir `
-Version $version `
-ScratchDir (Join-Path $msiScratch "smoke-$profile")
if ($LASTEXITCODE -ne 0) {
Expand All @@ -619,13 +671,51 @@ jobs:
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'
$selector = Join-Path $adminRoot 'PFiles64\Oscan\bin\oscan.exe'
if (-not (Test-Path -LiteralPath $selector -PathType Leaf)) {
throw "The coinstalled MSI image has no shared oscan selector."
}
& $selector --version | Out-Null
$defaultManager = Join-Path $adminRoot 'PFiles64\Oscan\bin\oscan-default.exe'
if (-not (Test-Path -LiteralPath $defaultManager -PathType Leaf)) {
throw "The coinstalled MSI image has no oscan-default manager."
}
$selectorVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($selector).ProductVersion
if ($selectorVersion -ne $version) {
throw "The selector PE version is '$selectorVersion', expected '$version'."
}

foreach ($selection in @('c', 'full')) {
& $defaultManager set $selection | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Could not explicitly select the '$selection' profile."
}
$selectedVersion = & $selector --version | Out-String
if ($LASTEXITCODE -ne 0 -or $selectedVersion -notmatch "distribution: $selection") {
throw "The shared selector did not dispatch to the explicitly selected '$selection' profile."
}
}
$fullShim = Join-Path $adminRoot 'PFiles64\Oscan\bin\oscan-full.cmd'
$hiddenFullShim = "$fullShim.hidden"
Move-Item -LiteralPath $fullShim -Destination $hiddenFullShim
try {
& $selector --version 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
throw "Removing the explicitly selected profile silently fell back to another MSI profile."
}
} finally {
Move-Item -LiteralPath $hiddenFullShim -Destination $fullShim
}
& $defaultManager auto | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "The coinstalled MSI selector failed."
throw "Could not enable automatic profile selection."
}
$automaticMode = & $defaultManager show | Out-String
if ($LASTEXITCODE -ne 0 -or $automaticMode -notmatch 'default profile: auto \(currently full\)') {
throw "The selection manager did not persist automatic mode with the expected current profile."
}
$automaticVersion = & $selector --version | Out-String
if ($LASTEXITCODE -ne 0 -or $automaticVersion -notmatch 'distribution: full') {
throw "Automatic mode did not resolve the expected full > llvm > cranelift > c priority."
}

- name: Upload packaged artifacts
Expand Down
65 changes: 65 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "oscan"
version = "0.1.0"
edition = "2021"
default-run = "oscan"

# Backend selection at compile time. The default developer build enables
# every backend and keeps the existing capability-based default behavior;
Expand Down Expand Up @@ -58,6 +59,7 @@ tempfile = "3"
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Threading",
] }

Expand All @@ -66,3 +68,4 @@ windows-sys = { version = "0.61", features = [
# time (docs/design/native-link-embedding.md §5.2) using the same crate
# `src/backend/native_assets.rs` uses at runtime.
sha2 = "0.10"
winresource = "0.1"
26 changes: 19 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,41 @@ Install full, or select a slim package explicitly:

```powershell
iwr -useb https://raw.githubusercontent.com/lucabol/Oscan/master/scripts/install-latest.ps1 -OutFile install-latest.ps1
.\install-latest.ps1 -Profile full -SetDefault
.\install-latest.ps1 -Profile full
oscan-default set full
.\install-latest.ps1 -Backend llvm
.\install-latest.ps1 -Backend cranelift
.\install-latest.ps1 -Backend c
```

Archive profiles coexist as `oscan-full`, `oscan-llvm`, `oscan-cranelift`, and
`oscan-c`. The first archive install safely creates `oscan`; later installs
preserve that selection unless `-SetDefault` is passed. Open a new terminal,
then check:
`oscan-c`. Plain `oscan` uses a per-user sticky selection shared by archive and
MSI installs. Installing or upgrading a profile never changes an existing
selection. If none exists, the first `oscan` invocation chooses the
highest-priority installed profile and persists that exact choice.

```powershell
oscan-default set full # explicit and sticky
oscan-default show
oscan --version
oscan-default auto # optional full > llvm > cranelift > c fallback
```

Removing the explicitly selected profile does not silently switch compilers:
`oscan` fails with instructions until that profile is reinstalled or
`oscan-default set <profile>`/`oscan-default auto` is run. MSI selection is
always performed after installation as the target user; `-SetDefault` remains
archive-only because MSI installation is elevated.

### Linux

Download an archive and `SHA256SUMS`, verify it, extract it, and run the
included installer. This example installs full and selects it as the default:

```bash
tar xf oscan-vX.Y.Z-linux-x86_64-full.tar.xz
./oscan-vX.Y.Z-linux-x86_64-full/install.sh --set-default
./oscan-vX.Y.Z-linux-x86_64-full/install.sh
oscan-default set full
oscan --version
oscan app.osc --backend cranelift
```
Expand All @@ -106,8 +118,8 @@ tar xf oscan-vX.Y.Z-macos-x86_64-c.tar.gz
```

Run the included `install.sh` to create `oscan-c` without disturbing another
profile, then run `oscan-c --version`. Pass `--set-default` only when the plain
`oscan` command should select this profile.
profile, then run `oscan-c --version`. Select it with `oscan-default set c`;
`--set-default` remains a compatible installer shortcut.

### Build from source

Expand Down
Loading
Loading