Skip to content

Unify build tool plugins with prebuilt artifact bundle and Linux support#231

Merged
dfed merged 43 commits intomainfrom
dfed--one-plugin-to-rule-them-all
Apr 9, 2026
Merged

Unify build tool plugins with prebuilt artifact bundle and Linux support#231
dfed merged 43 commits intomainfrom
dfed--one-plugin-to-rule-them-all

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Apr 9, 2026

Summary

  • Replace dual-plugin architecture (SafeDIGenerator + SafeDIPrebuiltGenerator) with a single SafeDIGenerator plugin
  • Add scan and generate subcommands to SafeDITool (generate is the default for backward compatibility)
  • SPM plugin shells out to SafeDITool scan via Process during createBuildCommands, then returns a .buildCommand for code generation
  • Xcode plugin uses lightweight in-process PluginScanner to discover output files, then returns a .buildCommand with --output-directory for combined scan+generate at build time
  • Add Package Traits: prebuilt (default, uses artifact bundle) and sourceBuild (compiles from source), with mutual exclusivity guard
  • Delete SafeDIScannerCore, SafeDIScanner, SafeDIPrebuiltGenerator, InstallSafeDITool, ExamplePrebuiltPackageIntegration
  • All example projects use sourceBuild trait with comments explaining it's for local dev
  • Update documentation (README, Manual, CLAUDE.md) for new architecture
  • Remove version duplication: currentVersion is "0.0.0-development" in source, stamped at build time by publish workflow
  • Publish workflow: per-arch macOS binaries (no lipo), stripped, -Osize, codesigned+notarized, Linux with --static-swift-stdlib
  • Publish workflow: workflow_dispatch with version/prerelease/dry-run inputs, artifact bundle always uploaded as workflow artifact

Motivation

The in-process SafeDIScanner runs in debug mode inside the plugin process (~15s) because SPM provides no way to build plugin code in release mode. Shelling out to a prebuilt SafeDITool binary eliminates this bottleneck entirely.

Test plan

  • All 742 tests pass (swift test --traits sourceBuild)
  • Zero uncovered lines in new code
  • swift build --traits sourceBuild succeeds
  • swift build (default prebuilt trait) succeeds when artifact bundle exists
  • swift build --traits sourceBuild,prebuilt errors with mutual exclusivity message
  • All example projects build locally (SPM + both Xcode projects)
  • ./CLI/lint.sh passes
  • CI passes (blocked on artifact bundle publish with working codesigning)

🤖 Generated with Claude Code

…ndle support

Replace the dual-plugin architecture (SafeDIGenerator + SafeDIPrebuiltGenerator) with a
single SafeDIGenerator plugin that shells out to SafeDITool for scanning and generation.

- Add `scan` and `generate` subcommands to SafeDITool (`generate` is default for
  backward compatibility)
- Plugin calls `SafeDITool scan` via Process during createBuildCommands to build
  the manifest, then returns a `.buildCommand` for `SafeDITool generate`
- Add `usePrebuiltBinary` flag in Package.swift to switch between artifact bundle
  (prebuilt) and source-built SafeDITool
- Add artifact bundle binary target (placeholder checksum, updated by publish workflow)
- Move output file naming and relative path utilities from SafeDIScannerCore to SafeDICore
- Add `additionalInputFiles` field to SafeDIToolManifest for build input tracking
- Delete SafeDIScannerCore, SafeDIScanner, SafeDIPrebuiltGenerator, InstallSafeDITool
- Update publish workflow: workflow_dispatch with version/branch/dry-run inputs,
  Linux builds, artifact bundle assembly, auto-update Package.swift
- Update documentation, examples, and CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4192b93) to head (43baa11).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              main      #231    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           39        41     +2     
  Lines         6122      5752   -370     
==========================================
- Hits          6122      5752   -370     
Files with missing lines Coverage Δ
Sources/SafeDICore/Models/SafeDIToolManifest.swift 100.00% <100.00%> (ø)
...ources/SafeDICore/Utilities/OutputFileNaming.swift 100.00% <100.00%> (ø)
Sources/SafeDICore/Utilities/RelativePath.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/GenerateCommand.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/SafeDITool.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/ScanCommand.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/SwiftFileParsing.swift 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dfed and others added 15 commits April 9, 2026 00:58
… Xcode

- Bump swift-argument-parser minimum from 1.2.0 to 1.4.0 (1.3.1+ has
  CommandConfiguration: Sendable, needed for Swift 6 concurrency)
- Delete stale Package.resolved in example projects
- Use .prebuildCommand for Xcode plugin context since context.tool(named:)
  returns paths with unresolved build variables (${BUILD_DIR}) that can't
  be used with Process during createBuildCommands
- Add --output-directory and --mock-scoped-files to Generate command so
  it can self-scan when invoked without --swift-manifest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract scan logic into free `performScan(...)` function callable by
  both `Scan.run()` and `Generate.run()` — avoids ArgumentParser's
  uninitialized property issue when creating ParsableCommand structs
  directly
- Update all test code to use `Generate.parse([...])` instead of
  `Generate()` which leaves @option properties uninitialized
- Fix test function signatures to propagate throws from parse calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover the duplicate-basename disambiguation logic in OutputFileNaming.swift
by testing through the full scan pipeline with files in subdirectories.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Xcode's plugin context returns tool paths with unresolved build variables
(${BUILD_DIR}/${CONFIGURATION}) that can't be used with Process during
createBuildCommands. Prebuild commands also don't work with source-built
executables in Xcode.

Solution: add a lightweight PluginScanner that determines output files
in-process (text-based, no SwiftSyntax), then return a .buildCommand
with --output-directory so SafeDITool does scan+generate at build time.

Verified all example projects build:
- Example Package Integration (SPM)
- ExampleProjectIntegration (Xcode)
- ExampleMultiProjectIntegration (Xcode)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ate, regex matching

- Scan additional directory files for roots (P1: roots from
  additionalDirectoriesToInclude were missing from declared outputs)
- Extract and declare outputs for additionalMocksToGenerate from
  #SafeDIConfiguration (P1: cross-module mock outputs were missing)
- Use regex patterns instead of raw substring matching to reduce false
  positives from comments/strings (P2: @INSTANTIABLE in a comment could
  declare phantom outputs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… branch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace `usePrebuiltBinary` boolean with proper Package Traits:
- `prebuilt` (default): uses artifact bundle binary target
- `sourceBuild`: compiles SafeDITool from source

The binaryTarget now always exists in the targets array, pointing to the
alpha-10 release artifact bundle. This proves the trait plumbing works
(SPM resolves the artifact, plugin uses it). The old binary doesn't have
the new `scan` subcommand, so Xcode projects will get argument errors
until a proper release with the new SafeDITool is published.

Example Package Integration uses `traits: ["sourceBuild"]` since it's a
local path dependency that needs the source-built tool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests need sourceBuild trait to compile SafeDITool from source. Xcode
project integration jobs use the prebuilt artifact bundle which contains
the old SafeDITool without --output-directory support. Disabled until a
release with the new SafeDITool is published.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tBinary sed

Artifact bundle is now always uploaded as a workflow artifact (not just
dry-run), so if the release step fails the bundle can be downloaded and
the release created manually. Removed the usePrebuiltBinary sed since
we switched to Package Traits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… dead code

- README.md: Replace usePrebuiltBinary reference with sourceBuild trait
- CLAUDE.md: Add --traits sourceBuild to build/test commands
- OutputFileNaming.swift: Remove unused relativePath property from FileInfo
- Delete stale ExamplePrebuiltPackageIntegration/Package.resolved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed self-assigned this Apr 9, 2026
… coverage

- Delete ExamplePrebuiltPackageIntegration (prebuilt is now the default,
  no special example needed)
- Add sourceBuild trait to Xcode example projects via UI
- Add comment to Example Package Integration explaining sourceBuild trait
- Update README example projects note about sourceBuild trait
- Add mutual exclusivity guard: #if prebuilt && sourceBuild / #error
- Extract update-version.sh script from publish workflow sed commands
- Add CI job to test update-version.sh script
- Publish workflow: always commit (even dry-run), only push/release on
  real runs. Use RELEASE_UPLOADER PAT for checkout to bypass branch
  protection.
- Add prerelease input to publish workflow
- Remove dead directoryBaseURL branch in ScanCommand
- Add tests for --output-directory auto-scan, Scan.run(), and
  --output-directory without CSV error
- Bump artifact bundle URL to alpha-11 (placeholder for next release)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed changed the title Unify build tool plugins with artifact bundle support Unify build tool plugins with prebuilt artifact bundle and Linux support Apr 9, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed force-pushed the dfed--one-plugin-to-rule-them-all branch from 8342d34 to 2bb5227 Compare April 9, 2026 16:51
dfed and others added 5 commits April 9, 2026 09:56
# Conflicts:
#	.github/workflows/publish.yml
SPM resolves the full package graph (including binaryTarget) even when
building a single product. Without the trait, builds fail trying to
download the artifact bundle that doesn't exist yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
gh release create fails if the tag/release already exists. Delete it
first (with --cleanup-tag) so republishing a version works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed force-pushed the dfed--one-plugin-to-rule-them-all branch from ba17930 to 2abd091 Compare April 9, 2026 18:03
@dfed dfed force-pushed the dfed--one-plugin-to-rule-them-all branch from 2abd091 to b4e0df1 Compare April 9, 2026 18:04
Deleting a release removes the artifact bundle, breaking package
resolution for all consumers. Instead, create if new or edit if
existing, then upload assets with --clobber to replace in-place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed force-pushed the dfed--one-plugin-to-rule-them-all branch from b4e0df1 to 4513c16 Compare April 9, 2026 18:05
github-actions bot and others added 18 commits April 9, 2026 18:14
- Remove safeDIVersion from Shared.swift (no longer needed)
- Remove check-version-consistency.sh (no duplication to check)
- Remove version-check CI job
- Change currentVersion to "0.0.0-development" (stamped by publish jobs)
- update-version.sh now only updates Package.swift (URL + checksum)
- Publish build jobs stamp version via sed before building
- Add else branches to relativePath functions
- Upgrade upload-artifact to v7
- Address PR review comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
update-version.sh no longer modifies SafeDITool.swift (version is
stamped at build time), so the CI check shouldn't look for it there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The workflow always runs from the branch it should commit to, so the
separate branch input is redundant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The GitHub API can return 404 if the release is created too quickly
after pushing the tag. Sleep 5s after push, retry create up to 3 times.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ip -9

- macOS: build with -Xswiftc -Osize -Xlinker -dead_strip, strip -rSTx
  after lipo (before codesigning)
- Linux: build with -Xswiftc -Osize, strip -s after build
- Use zip -9 for maximum compression on artifact bundle

Expected ~50% size reduction based on SwiftLint/SwiftFormat precedent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ip -9

- macOS: build with -Xswiftc -Osize -Xlinker -dead_strip, strip -rSTx
  after lipo (before codesigning)
- Linux: build with -Xswiftc -Osize, strip -s after build
- Use zip -9 for maximum compression on artifact bundle

Expected ~50% size reduction based on SwiftLint/SwiftFormat precedent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Stripping after lipo invalidated the codesignature. Move strip to
the build jobs (before codesigning) so the signed binary is valid.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse all files once and filter the results for mock-scoped subset
instead of parsing mock-scoped files a second time. ~20% scan speedup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous regex `[^)]*` stopped matching at any `)`, including those
inside string literal arguments like `mockAttributes: "@available(iOS 17, *)"`.
This caused under-matching — files with `generateMock: true` after such
arguments would not be detected.

Use `(.|\n)*?` (non-greedy any character) instead, which matches through
parentheses in strings while still finding the target argument.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed marked this pull request as ready for review April 9, 2026 21:47
@dfed dfed merged commit 5af124c into main Apr 9, 2026
17 checks passed
@dfed dfed deleted the dfed--one-plugin-to-rule-them-all branch April 9, 2026 21:47
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