diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 37273f6..8db59b5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -257,8 +257,8 @@ jobs: env: CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - build-dotnet: - environment: artifactory-publish + # Each OS runner builds and tests the natives for its own OS; publish-dotnet packs them into one package. + build-dotnet-windows: if: ${{ inputs.nuget }} runs-on: windows-latest @@ -281,9 +281,159 @@ jobs: with: dotnet-version: 10.0.x - # Builds both Windows natives, runs the managed tests, and packs into wrappers/dotnet/package. + - name: Build and test + run: ./build.ps1 -Targets x86_64-pc-windows-msvc,aarch64-pc-windows-msvc -NoPack + shell: pwsh + working-directory: wrappers/dotnet + + - name: Upload runtimes + uses: actions/upload-artifact@v7 + with: + name: dotnet-runtimes-windows + path: wrappers/dotnet/Devolutions.Slauth/runtimes/ + if-no-files-found: error + + build-dotnet-macos: + # The Developer ID certificate lives in this environment, next to the Swift job that also signs with it. + environment: cloudsmith-publish + if: ${{ inputs.nuget }} + runs-on: macos-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Setup rust + run: | + rustup target add x86_64-apple-darwin + rustup target add aarch64-apple-darwin + + - name: Configure .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + # The runner is arm64, so the managed tests exercise the arm64 dylib; x86_64 is cross-compiled. + - name: Build and test + run: ./build.ps1 -Targets x86_64-apple-darwin,aarch64-apple-darwin -NoPack + shell: pwsh + working-directory: wrappers/dotnet + + - name: Setup code signing + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 + echo -n "$CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + security import $CERTIFICATE_PATH -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + env: + CERTIFICATE_BASE64: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE }} + CERTIFICATE_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.APPLE_APP_DEV_ID_APP_CERTIFICATE_PASSWORD }} + + # Apps that bundle the package are notarized, which requires every nested Mach-O to carry a Developer ID signature. + - name: Sign dylibs + run: | + for dylib in wrappers/dotnet/Devolutions.Slauth/runtimes/osx-*/native/libslauth.dylib; do + codesign --force --timestamp --options runtime --sign "$SIGNING_IDENTITY" "$dylib" + codesign --verify --strict --verbose "$dylib" + done + env: + SIGNING_IDENTITY: "Developer ID Application: Devolutions inc." + + - name: Upload runtimes + uses: actions/upload-artifact@v7 + with: + name: dotnet-runtimes-macos + path: wrappers/dotnet/Devolutions.Slauth/runtimes/ + if-no-files-found: error + + build-dotnet-linux: + if: ${{ inputs.nuget }} + # One native runner per architecture, so the managed tests run on both. Ubuntu 22.04 keeps the glibc + # requirement at 2.35 rather than whatever the latest image ships. + strategy: + matrix: + include: + - runner: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + arch: x64 + - runner: ubuntu-22.04-arm + target: aarch64-unknown-linux-gnu + arch: arm64 + runs-on: ${{ matrix.runner }} + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Setup rust + run: rustup target add ${{ matrix.target }} + + - name: Configure .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Build and test + run: ./build.ps1 -Targets ${{ matrix.target }} -NoPack + shell: pwsh + working-directory: wrappers/dotnet + + - name: Upload runtimes + uses: actions/upload-artifact@v7 + with: + name: dotnet-runtimes-linux-${{ matrix.arch }} + path: wrappers/dotnet/Devolutions.Slauth/runtimes/ + if-no-files-found: error + + publish-dotnet: + environment: artifactory-publish + if: ${{ inputs.nuget }} + needs: [build-dotnet-windows, build-dotnet-macos, build-dotnet-linux] + runs-on: windows-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Download runtimes + uses: actions/download-artifact@v8 + with: + pattern: dotnet-runtimes-* + merge-multiple: true + path: wrappers/dotnet/Devolutions.Slauth/runtimes + + # A package missing one of these would still pack and publish, so check before going further. + - name: Check runtimes + run: | + $expected = @( + 'win-x64/native/slauth.dll', + 'win-arm64/native/slauth.dll', + 'osx-x64/native/libslauth.dylib', + 'osx-arm64/native/libslauth.dylib', + 'linux-x64/native/libslauth.so', + 'linux-arm64/native/libslauth.so' + ) + $missing = $expected | Where-Object { -not (Test-Path (Join-Path 'wrappers/dotnet/Devolutions.Slauth/runtimes' $_)) } + if ($missing) { + throw "Missing runtimes: $($missing -join ', ')" + } + shell: pwsh + + - name: Configure .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Generate package - run: ./build.ps1 + run: ./build.ps1 -PackOnly shell: pwsh working-directory: wrappers/dotnet diff --git a/wrappers/dotnet/README.md b/wrappers/dotnet/README.md index c2fdd9c..80e5239 100644 --- a/wrappers/dotnet/README.md +++ b/wrappers/dotnet/README.md @@ -80,6 +80,11 @@ managed tests, and packs. Defaults to `win-x64` and `win-arm64`; pass `-Targets` Adding a platform means one entry in `$RidByTarget` in `build.ps1` — the wrapper and csproj need no change. +A runner can only build (and test) the natives for its own OS, so the publish workflow splits the work: +each OS job runs `./build.ps1 -Targets -NoPack`, and one job copies every staged +`runtimes/` folder together and runs `./build.ps1 -PackOnly`. The published package carries `win-x64`, +`win-arm64`, `osx-x64`, `osx-arm64` (Developer ID signed), `linux-x64` and `linux-arm64`. + ### Prerequisite for cross-compiling `ring` needs a C compiler for some targets. Building `aarch64-pc-windows-msvc` requires LLVM/clang on diff --git a/wrappers/dotnet/build.ps1 b/wrappers/dotnet/build.ps1 index f67b2b0..934d878 100644 --- a/wrappers/dotnet/build.ps1 +++ b/wrappers/dotnet/build.ps1 @@ -18,6 +18,14 @@ .PARAMETER SkipTests Skip the managed test run. The tests exercise the real C ABI, so leave them on where possible. + +.PARAMETER NoPack + Build, stage and test, but do not pack. CI uses this on each OS runner, since a runner can only + build (and test) the natives for its own OS, then hands the staged runtimes to a single pack job. + +.PARAMETER PackOnly + Skip the native builds and tests and pack whatever is already staged under runtimes/. CI uses this + once every OS runner's runtimes have been copied in. #> [CmdletBinding()] param( @@ -25,11 +33,20 @@ param( [string] $Version, [string] $Configuration = 'Release', [string] $OutputPath = './package', - [switch] $SkipTests + [switch] $SkipTests, + [switch] $NoPack, + [switch] $PackOnly ) $ErrorActionPreference = 'Stop' +if ($NoPack -and $PackOnly) { + throw '-NoPack and -PackOnly cannot be used together.' +} + +# `pwsh -File build.ps1 -Targets a,b` binds "a,b" as one string, so accept a comma list as well as an array. +$Targets = @($Targets | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $RidByTarget = @{ 'x86_64-pc-windows-msvc' = @{ Rid = 'win-x64'; File = 'slauth.dll' } 'aarch64-pc-windows-msvc' = @{ Rid = 'win-arm64'; File = 'slauth.dll' } @@ -54,58 +71,76 @@ if (-not $Version) { } } -Write-Host "Packing Devolutions.Slauth $Version" -ForegroundColor Cyan - -# Start from a clean runtimes tree so a removed target can't leave a stale native behind in the package. -if (Test-Path $RuntimesDir) { - Remove-Item $RuntimesDir -Recurse -Force -} +Write-Host "Devolutions.Slauth $Version" -ForegroundColor Cyan -foreach ($target in $Targets) { - if (-not $RidByTarget.ContainsKey($target)) { - throw "Unknown Rust target '$target'. Add it to `$RidByTarget in this script." +if (-not $PackOnly) { + # Start from a clean runtimes tree so a removed target can't leave a stale native behind in the package. + if (Test-Path $RuntimesDir) { + Remove-Item $RuntimesDir -Recurse -Force } - $mapping = $RidByTarget[$target] + foreach ($target in $Targets) { + if (-not $RidByTarget.ContainsKey($target)) { + throw "Unknown Rust target '$target'. Add it to `$RidByTarget in this script." + } - Write-Host "==> $target ($($mapping.Rid))" -ForegroundColor Yellow + $mapping = $RidByTarget[$target] - Push-Location $RepoRoot - try { - & rustup target add $target - if ($LASTEXITCODE -ne 0) { throw "rustup target add $target failed." } + Write-Host "==> $target ($($mapping.Rid))" -ForegroundColor Yellow - & cargo build --release --target $target - if ($LASTEXITCODE -ne 0) { throw "cargo build for $target failed." } - } - finally { - Pop-Location - } + Push-Location $RepoRoot + try { + & rustup target add $target + if ($LASTEXITCODE -ne 0) { throw "rustup target add $target failed." } + + & cargo build --release --target $target + if ($LASTEXITCODE -ne 0) { throw "cargo build for $target failed." } + } + finally { + Pop-Location + } - $built = Join-Path $RepoRoot "target/$target/release/$($mapping.File)" - if (-not (Test-Path $built)) { - throw "Expected native library not found: $built" + $built = Join-Path $RepoRoot "target/$target/release/$($mapping.File)" + if (-not (Test-Path $built)) { + throw "Expected native library not found: $built" + } + + $destination = Join-Path $RuntimesDir "$($mapping.Rid)/native" + New-Item $destination -ItemType Directory -Force | Out-Null + Copy-Item $built (Join-Path $destination $mapping.File) -Force } - $destination = Join-Path $RuntimesDir "$($mapping.Rid)/native" - New-Item $destination -ItemType Directory -Force | Out-Null - Copy-Item $built (Join-Path $destination $mapping.File) -Force + if (-not $SkipTests) { + # The test project loads the host-architecture cdylib from target/release, so make sure it exists. + Push-Location $RepoRoot + try { + & cargo build --release + if ($LASTEXITCODE -ne 0) { throw 'cargo build (host) failed.' } + } + finally { + Pop-Location + } + + Write-Host '==> Running managed tests' -ForegroundColor Yellow + & dotnet test (Join-Path $WrapperDir 'Devolutions.Slauth.Tests/Devolutions.Slauth.Tests.csproj') --configuration $Configuration + if ($LASTEXITCODE -ne 0) { throw 'Managed tests failed.' } + } } - -if (-not $SkipTests) { - # The test project loads the host-architecture cdylib from target/release, so make sure it exists. - Push-Location $RepoRoot - try { - & cargo build --release - if ($LASTEXITCODE -ne 0) { throw 'cargo build (host) failed.' } +else { + # Another runner staged these, so fail here rather than publish a package missing a platform. + $staged = @(Get-ChildItem (Join-Path $RuntimesDir '*/native/*') -File -ErrorAction SilentlyContinue) + if ($staged.Count -eq 0) { + throw "No native libraries staged under $RuntimesDir. Run without -PackOnly, or copy the runtimes in first." } - finally { - Pop-Location + + foreach ($file in $staged) { + Write-Host " staged $([System.IO.Path]::GetRelativePath($RuntimesDir, $file.FullName))" } +} - Write-Host '==> Running managed tests' -ForegroundColor Yellow - & dotnet test (Join-Path $WrapperDir 'Devolutions.Slauth.Tests/Devolutions.Slauth.Tests.csproj') --configuration $Configuration - if ($LASTEXITCODE -ne 0) { throw 'Managed tests failed.' } +if ($NoPack) { + Write-Host "Done. Runtimes staged in $RuntimesDir" -ForegroundColor Green + return } Write-Host '==> Packing' -ForegroundColor Yellow