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
158 changes: 154 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions wrappers/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <its 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
Expand Down
115 changes: 75 additions & 40 deletions wrappers/dotnet/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,35 @@

.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(
[string[]] $Targets = @('x86_64-pc-windows-msvc', 'aarch64-pc-windows-msvc'),
[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' }
Expand All @@ -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
Expand Down
Loading