Skip to content

Preserve Unix execute bit in Linux/macOS release zips#373

Merged
erikdarlingdata merged 4 commits into
devfrom
fix/linux-macos-exec-bit
Jun 24, 2026
Merged

Preserve Unix execute bit in Linux/macOS release zips#373
erikdarlingdata merged 4 commits into
devfrom
fix/linux-macos-exec-bit

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Summary

Linux and macOS release artifacts were built with PowerShell's Compress-Archive on the windows-latest runner, which cannot store Unix file-mode bits. The self-contained apphost PlanViewer.App therefore extracted as non-executable, and the app failed to launch ("permission denied") on Linux/macOS — the blocker reported by a tester.

What changed

  • .github/scripts/zip_with_exec.py (new): zips a directory with create_system=3 (Unix) so extractors honor the mode, marking the apphost — and createdump if present — 0755, everything else 0644. Hard-fails if the apphost is missing, so a broken archive fails the release loudly instead of shipping.
  • release.yml / nightly.yml: the Linux flat zip and both macOS .app bundles now go through the helper. Windows stays on Compress-Archive (no Unix execute bit; signed-binary path untouched).
  • Info.plist: added CFBundleDocumentTypes so Finder shows the app icon for .sqlplan files and offers Performance Studio in "Open With".
  • PlanAnalyzer.Node.cs: fixed culture-dependent percent formatting (:P0{x * 100:N0}%). Pre-existing bug on dev unrelated to packaging, but it blocks CI: :P0 renders 100% on en-US and 100 % on InvariantCulture, so WarningCharacterizationTests failed on the ubuntu runner (baseline was recorded on en-US). Had to fix it here to get this PR green. Verified the full Core suite passes under both default and DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1.
  • Version bumped 1.13.0 → 1.14.0.

Why not just use .NET / pwsh?

ZipArchive can set ExternalAttributes, but on Windows it stamps the archive host byte as Windows (create_system=0), so unzip/macOS ignore the Unix mode. Verified empirically — only create_system=3 (Python's zipfile) yields an executable apphost on extraction.

Test plan

  • Helper tested locally: exec bit lands only on the apphost/createdump, create_system=3 on every entry, missing-apphost guard fires, optional-createdump skipped cleanly when absent, archive round-trips intact.
  • Info.plist validated with plistlib; both workflow YAMLs parse.
  • End-to-end verified by the release.yml run on merge to main (produces the v1.14.0 Linux/macOS zips).

Known follow-up (not in this PR)

On macOS the .sqlplan association registers the icon and launches the app, but the app discovers the file to open from argv/named-pipe only; macOS delivers it via a kAEOpenDocuments Apple Event (IActivatableApplicationLifetime). Double-click-to-open the plan needs that handler wired up — tracked separately.

🤖 Generated with Claude Code

erikdarlingdata and others added 4 commits June 24, 2026 16:32
Compress-Archive on the windows-latest runner can't store Unix file-mode bits, so the self-contained apphost (PlanViewer.App) extracted as non-executable on Linux/macOS and the app would not launch. .NET's ZipArchive is no better: on Windows it stamps the archive host byte as Windows (create_system=0), so unzip/macOS ignore the mode even when ExternalAttributes carries 0755.

Add .github/scripts/zip_with_exec.py (Python zipfile with create_system=3) to zip the Linux flat output and both macOS .app bundles, marking the apphost (and createdump if present) 0755. It hard-fails if the apphost is missing so a broken archive fails the release loudly. Wire it into release.yml and nightly.yml; Windows stays on Compress-Archive (no Unix bit needed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add CFBundleDocumentTypes to Info.plist so Finder shows the app icon for .sqlplan files and routes them to Performance Studio via the Open With menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ":P0" format is culture-sensitive in the space before the percent sign (en-US -> "100%", InvariantCulture -> "100 %"). Warning text therefore differed between en-US dev machines and CI runners that default to InvariantCulture, breaking the golden-master WarningCharacterizationTests (baseline recorded "100%", ubuntu produced "100 %").

Format the three percents as "{x * 100:N0}%" with a literal percent sign. N0 preserves the digits and group separators :P0 produced on en-US, so the committed baseline stays valid; the literal % drops the culture-dependent space, making output identical on every platform. Verified: full Core suite passes under default and DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata merged commit d63e7c5 into dev Jun 24, 2026
3 checks passed
@erikdarlingdata erikdarlingdata deleted the fix/linux-macos-exec-bit branch June 24, 2026 20:45
@erikdarlingdata erikdarlingdata mentioned this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant