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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ CI uses the release host for each target and records checksums of the staged dyl

Use `-Arch x86_64` on an Intel Mac. The output contains `Jiffshot.app` and a DMG; the Rustolonia host and its companion libraries are kept together in the app's `Contents/Frameworks` directory, and `jiffshot-cli` is in `Contents/MacOS`. Packaging requires both executables and rejects any selected-architecture component whose deployment target is newer than macOS 13; this check does not substitute for testing on macOS 13. By default, the output is ad-hoc signed for local testing, not notarized for distribution. For local distribution builds, pass `-Identity` with a Developer ID Application identity and `-NotaryProfile` with an existing `xcrun notarytool` keychain profile. Release CI can also prepare an app for external notarization and build its disk image after the app's ticket has been stapled.

Packaging, CI and release builds, including dry runs, require the signed staged host's native popup-gate ABI, deferred termination, and outgoing file-drag support. They also verify that the bundle discovers its own host without environment overrides and that an isolated executable cannot fall back to an unstaged host. The termination and drag probes load the exact staged `libAvaloniaNative.dylib` in separate processes and call its capability exports. For local debugging only, `-AllowUncheckedHost` skips these checks and warns that the app may not start; the packager refuses to Developer ID sign or notarize unchecked bundles. `RUSTOLONIA_HOST_DIR` does not verify its contents: verify the source or a trusted archive checksum before staging a host, and keep the host and its companion libraries from the same verified build together. These probes must run on a Mac capable of executing the selected architecture. Capability checks do not prove provenance or validate tray behavior, shutdown ordering, capture-frame exclusion, or overall feature parity.
Packaging, CI and release builds, including dry runs, require the signed staged host's native popup-gate ABI, deferred termination, and outgoing file-drag support. They also verify that the bundle discovers its own host without environment overrides and that an isolated copy, re-signed ad hoc outside the bundle, cannot fall back to an unstaged host. The termination and drag probes load the exact staged `libAvaloniaNative.dylib` in separate processes and call its capability exports. For local debugging only, `-AllowUncheckedHost` skips these checks and warns that the app may not start; the packager refuses to Developer ID sign or notarize unchecked bundles. `RUSTOLONIA_HOST_DIR` does not verify its contents: verify the source or a trusted archive checksum before staging a host, and keep the host and its companion libraries from the same verified build together. These probes must run on a Mac capable of executing the selected architecture. Capability checks do not prove provenance or validate tray behavior, shutdown ordering, capture-frame exclusion, or overall feature parity.

Grant Jiffshot Screen Recording (called Screen & System Audio Recording on newer macOS versions) and Accessibility permissions in System Settings when prompted. Camera and Microphone access are needed only when those recording sources are used. Permissions are tied to the application identity; granting them to a terminal or a separate test tool does not authorize the packaged app. The experimental port still needs end-to-end testing on both architectures and macOS versions.

Expand Down
4 changes: 3 additions & 1 deletion package/macos/JiffshotMacPackage.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ function Get-MacDiscoveredHost([string]$Binary, [string]$WorkingDirectory) {
return $null
}
$detail = if ($result.Stderr.Trim()) { $result.Stderr.Trim() } else { "exit status $($result.ExitCode)" }
throw "The Rustolonia host discovery probe failed: $detail"
throw "The Rustolonia host discovery probe failed for '$Binary': $detail"
}

function Confirm-MacHostDiscovery([string]$Binary, [string]$HostPath, [string]$Temporary) {
Expand All @@ -331,6 +331,8 @@ function Confirm-MacHostDiscovery([string]$Binary, [string]$HostPath, [string]$T
[void][IO.Directory]::CreateDirectory($isolated)
$unbundled = Join-Path $isolated 'jiffshot'
[IO.File]::Copy($Binary, $unbundled)
# The bundle signature seals Info.plist, which is absent beside this copy.
Sign-MacPath $unbundled '-' $null $null
$found = Get-MacDiscoveredHost $unbundled $isolated
if ($null -ne $found) { throw "The application can discover an unstaged Rustolonia host: $found" }
}
Expand Down
54 changes: 54 additions & 0 deletions package/macos/Package.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ Describe 'macOS packaging' {
Mock Invoke-MacCommand { $result }
{ Get-MacDiscoveredHost '/fixture/jiffshot' '/fixture' } | Should -Throw '*host discovery probe failed*'
}
Mock Invoke-MacCommand { [pscustomobject]@{ ExitCode = 137; Stdout = ''; Stderr = '' } }
{ Get-MacDiscoveredHost '/fixture/jiffshot' '/fixture' } |
Should -Throw "*host discovery probe failed for '/fixture/jiffshot': exit status 137*"
Mock Invoke-MacCommand { throw 'timed out' }
{ Get-MacDiscoveredHost '/fixture/jiffshot' '/fixture' } | Should -Throw '*Could not check Rustolonia host discovery*'
}
Expand All @@ -207,6 +210,7 @@ Describe 'macOS packaging' {
[Environment]::SetEnvironmentVariable($name, '/must-not-leak')
}
Mock Resolve-MacPhysicalPath { $Path }
Mock Sign-MacPath {}
$script:ProbeCount = 0
Mock Invoke-MacCommand {
$script:ProbeCount++
Expand Down Expand Up @@ -238,6 +242,9 @@ Describe 'macOS packaging' {
$TimeoutSeconds -eq 30 -and
@($variables | Where-Object { $Environment.ContainsKey($_) }).Count -eq 0
}
Should -Invoke Sign-MacPath -Times 1 -Exactly -ParameterFilter {
$Path -ceq $unbundled -and $Identity -ceq '-' -and -not $Keychain -and -not $Entitlements
}
[IO.File]::ReadAllText($unbundled) | Should -BeExactly 'fixture'
}
finally {
Expand All @@ -253,6 +260,7 @@ Describe 'macOS packaging' {
[IO.File]::WriteAllText($binary, 'fixture')
[IO.File]::WriteAllText($hostPath, 'host')
Mock Resolve-MacPhysicalPath { $Path }
Mock Sign-MacPath {}
$script:ProbeCount = 0
Mock Get-MacDiscoveredHost {
$script:ProbeCount++
Expand All @@ -271,6 +279,52 @@ Describe 'macOS packaging' {
{ Confirm-MacHostDiscovery $binary $hostPath $script:Work } | Should -Throw '*unstaged Rustolonia host*'
}

It 'fails before the isolated probe if the copied executable cannot be signed' {
$binary = Join-Path $script:Work 'jiffshot'
$hostPath = Join-Path $script:Work 'host.dylib'
[IO.File]::WriteAllText($binary, 'fixture')
[IO.File]::WriteAllText($hostPath, 'host')
Mock Resolve-MacPhysicalPath { $Path }
Mock Get-MacDiscoveredHost { $hostPath }
Mock Sign-MacPath { throw 'fixture signing failed' }

{ Confirm-MacHostDiscovery $binary $hostPath $script:Work } | Should -Throw '*fixture signing failed*'
Should -Invoke Get-MacDiscoveredHost -Times 1 -Exactly
Should -Invoke Sign-MacPath -Times 1 -Exactly
}

It 'keeps the isolated copy of a signed bundle executable valid' {
$app = Join-Path $script:Work 'Fixture.app'
$macos = Join-Path $app 'Contents/MacOS'
$frameworks = Join-Path $app 'Contents/Frameworks'
[void][IO.Directory]::CreateDirectory($macos)
[void][IO.Directory]::CreateDirectory($frameworks)
$script:SignedFixtureBinary = Join-Path $macos 'jiffshot'
$script:SignedFixtureHost = Join-Path $frameworks 'librustolonia_host.dylib'
[IO.File]::Copy('/usr/bin/true', $script:SignedFixtureBinary)
[IO.File]::Copy('/usr/bin/true', $script:SignedFixtureHost)
Invoke-MacRun '/usr/bin/codesign' @('--force', '--sign', '-', $script:SignedFixtureHost)
Write-MacPlist (Join-Path $app 'Contents/Info.plist') ([ordered]@{
CFBundleIdentifier = 'com.devolutions.jiffshot.package-test'
CFBundleExecutable = 'jiffshot'
CFBundlePackageType = 'APPL'
})
Invoke-MacRun '/usr/bin/codesign' @('--force', '--sign', '-', '--options', 'runtime', $app)
Invoke-MacRun '/usr/bin/codesign' @('--verify', '--deep', '--strict', $app)

$script:VerifiedIsolatedCopy = $false
Mock Get-MacDiscoveredHost {
if ($Binary -ceq $script:SignedFixtureBinary) { return $script:SignedFixtureHost }
[void](Invoke-MacCommand -File '/usr/bin/codesign' -Arguments @('--verify', '--strict', $Binary))
[void](Invoke-MacCommand -File $Binary)
$script:VerifiedIsolatedCopy = $true
}
Confirm-MacHostDiscovery $script:SignedFixtureBinary $script:SignedFixtureHost $script:Work

$script:VerifiedIsolatedCopy | Should -BeTrue
Invoke-MacRun '/usr/bin/codesign' @('--verify', '--deep', '--strict', $app)
}

It 'rejects existing package output' {
$binary = Join-Path $script:Work 'jiffshot'
foreach ($name in @('jiffshot', 'jiffshot-cli') + $script:HostLibraries) {
Expand Down
Loading