Skip to content

About

SwiftPM command plugin that reads your sources' imports and generates Package.swift for heavily modularized Swift projects. Works with PackageGeneratorCLI.

Topics

Resources

Code of conduct

Stars

13 stars

Watchers

1 watching

Forks

Repository files navigation

Package Generator

CI Release License: MIT

Website Β· Package Generator CLI Β· Migration guide Β· Performance Β· Architecture rules

A SwiftPM command plugin that generates Package.swift for heavily modularized projects. It reads the imports of every target, resolves each one to a local target, an external product or an Apple SDK, and writes the products and targets for you. Works well with hundreds of targets and with TCA-style feature modules.

The plugin runs Package Generator CLI, which parses your sources with swift-syntax. SwiftPM downloads the CLI as a binary target, so you never install it yourself.

  • Imports become dependencies: local targets become "Name", products of your root dependencies become .product(name:package:) with the URL-derived package identity
  • Apple SDKs ignored safely: a built-in list is skipped only after external products are checked, so a product named like an SDK (Charts) is kept
  • Verbatim parameters: parameters and additionalDependencies go into the manifest as written
  • Analysis: unused target detection, dependency weight, exported files
  • Backward compatible: the old targetsParameters config format still works
  • Config auto-discovery: finds packageGenerator.yaml, .yml or .json

Requirements: Swift 6.2 (Xcode 26) on an Apple silicon Mac; package-generator-cli ships as an arm64 macOS binary.

Quick Start

  1. Install (add to dependencies in your root Package.swift):

    .package(url: "https://github.com/mackoj/PackageGeneratorPlugin.git", from: "1.0.6")
  2. Create config at project root. Choose YAML or JSON:

    packageGenerator.yaml:

    pragmaMark: true
    includePackageCountMark: false
    spaces: 4
    headerFileURL: PackageHeader.swift
    
    packageDirectoryTargets:
      - path: Sources/Core
        targets:
          - name: Core
          - name: CoreTests
            type: test
      
      - path: Sources/Features
        targets:
          - name: Auth
            parameters:
              - 'swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]'
          - name: AuthTests
            type: test
    
    exclusions:
      imports:
        - MyPrivateFramework
  3. Run plugin (Xcode):

    • Right-click package β†’ "Package Generator"
    • Output is written directly to Package.swift (dryRun defaults to false)
    • Use dryRun: true to preview in Package_generated.swift first
  4. CI: swift package plugin --allow-writing-to-package-directory package-generator

Configuration Reference

Top-Level Settings

Key Type Default Purpose
verbose String "none" Verbose diagnostics scope: "none", "plugin", "cli", "all"
dryRun Bool false Generate Package_generated.swift instead of Package.swift
pragmaMark Bool false Add // MARK: comments grouping targets by path
includePackageCountMark Bool true Include the generated package count // MARK: comment
generateExportedFiles Bool false Generate exported.swift with @_exported import for each target
exportedFilesRelativePath String null Subdirectory for exported files (e.g., "Generated")
headerFileURL String null Path to file prepended to Package.swift
spaces Int 2 Indentation spaces
keepTempFiles Bool false Preserve YAML→JSON temp files (debug)
leafInfo Bool false Add dependency count comments to targets
unusedThreshold Int null Warn if local target used ≀ this (0 = warn if unused)
libraryType String "automatic" Linkage for every generated .library product: "automatic", "dynamic", "static"

verbose accepts a legacy true/false bool for backward compatibility (true = "all", false = "none").

packageDirectoryTargets

Array of directory groups. Each group declares path + targets.

packageDirectoryTargets:
  - path: Sources/Modules
    targets:
      - name: ModuleA
        type: regular                          # regular, test, macro, or executable
        path: null                             # override computed path if needed
        exclude: ["__Snapshots__", "Mocks"]   # exclude patterns
        parameters:                            # inline SPM target parameters
          - 'swiftSettings: [...]'
          - 'resources: [.process("Files")]'
        regularTargetName: null                # for tests: explicit link to regular target
        additionalDependencies:                # verbatim entries added to dependencies: []
          - '"AnotherTarget"'
          - '.product(name: "Foo", package: "foo-package")'
        libraryType: dynamic                   # overrides top-level libraryType for this product
        productName: FooDynamic                # names the product differently from the target

additionalDependencies injects strings verbatim into the target's dependencies: array, bypassing import analysis. Use it when a target needs a dependency that isn't imported in source (common with macros, plugins, or transitive requirements). Auto-resolved imports are emitted first (sorted), then additionalDependencies in declaration order.

libraryType sets the product linkage: automatic (default) omits type: and lets SwiftPM decide, dynamic and static emit .library(name:, type: .dynamic/.static, targets:). Set it per target to override the top-level libraryType. Ignored for test and macro targets, which produce no product, and for executable targets, which render as .executableTarget with an .executable(name:targets:) product (named by productName when set).

productName names the generated product differently from the target it wraps, emitting .library(name: "<productName>", targets: ["<target>"]). It takes precedence over a mappers.targets entry for the same target.

A dynamic product usually needs one. Xcode refuses to build a target dynamically while a product of the same name exists, as soon as another target in the same package links that target:

error: Swift package target 'FooCore' is linked as a static library by 'BarTests' and 4 other
targets, but cannot be built dynamically because there is a package product with the same name.

Giving the product its own name resolves it:

      - name: FooCore
        libraryType: dynamic
        productName: Foo      # .library(name: "Foo", type: .dynamic, targets: ["FooCore"])

Path Resolution (shortest-path logic):

  • Regular target: <path>/Sources/<name> or custom path
  • Test target: <path>/Tests/<name> or custom path
  • If computed path doesn't exist, recursive search in Sources/Tests wins shortest match
  • Test auto-pairs with regular via suffix strip (e.g., ModuleATests β†’ ModuleA)

mappers

Override import β†’ product mapping + target renaming.

mappers:
  imports:
    # Only needed when auto-discovery can't resolve a product.
    # Auto-discovery reads root Package.swift dependencies and maps
    # each product to its URL-derived package identity automatically.
    TrackerBinary: '.product(name: "Tracker", package: "clickstream-mobile-sdk-kmp-releases")'
  
  targets:
    Sources/App/Helpers/Foundation: FoundationHelpers
  • imports: manually map an import name to an SPM .product() string; merged with auto-discovered products (manual overrides win)
  • targets: maps a target path to the name of its generated product; the product still points at the real target. Prefer the per-target productName key, which is local to the target it renames
  • Auto-discovery reads all direct dependencies in your root Package.swift and maps each product to its URL-derived package identity (last path component of the URL); mappers.imports is only needed for edge cases where the import name differs from the product name

exclusions

Suppress imports from Package.swift generation.

exclusions:
  imports:           # Third-party frameworks to skip
    - SomePrivateLib
  targets:           # Targets to exclude entirely
    - ParserCLI
    - HelperBinary

Apple frameworks (UIKit, Foundation, SwiftUI, etc.) are auto-excluded via the built-in SDK list β€” no configuration needed.

Backward Compatibility

Old config format with targetsParameters dict still works:

# OLD FORMAT (still supported)
targetsParameters:
  ModuleA:
    - 'exclude: ["__Snapshots__"]'
    - 'resources: [.process("Files")]'
  ModuleB:
    - 'swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]'

# NEW FORMAT (recommended)
packageDirectoryTargets:
  - path: Sources/Modules
    targets:
      - name: ModuleA
        type: regular
        parameters:
          - 'exclude: ["__Snapshots__"]'
          - 'resources: [.process("Files")]'
      - name: ModuleB
        type: regular
        parameters:
          - 'swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]'

Both formats generate identical output. Inline parameters is preferred (simpler migration path).

Advanced Features

Exported Files

Generate exported.swift re-exporting local dependencies:

generateExportedFiles: true
exportedFilesRelativePath: Generated

For target Auth importing Core, generates:

// Generated/exported.swift
@_exported import Core

Useful for reducing boilerplate in modular architectures.

Pragma Mark Grouping

Group targets by path with section comments:

pragmaMark: true

Output:

// MARK: -
// MARK: Core
.target(name: "Core", ...),
.testTarget(name: "CoreTests", ...),

// MARK: -
// MARK: Features
.target(name: "Auth", ...),
.testTarget(name: "AuthTests", ...),

Dependency Weight

Show dependency count + mark heaviest:

leafInfo: true

Output:

  .target(
    name: "Core",// [3|2]
    ...
  .target(
    name: "Auth",// [5|3|πŸš›]

First number = total deps, second = local deps. πŸš› = highest local count.

Unused Target Detection

Warn about targets never imported:

unusedThreshold: 0

Logs: πŸ“¦ UnusedModule is used 0 times

Examples

Large Modular Project

pragmaMark: true
generateExportedFiles: true
exportedFilesRelativePath: Generated
headerFileURL: PackageHeader.swift
spaces: 4
leafInfo: true
unusedThreshold: 1

packageDirectoryTargets:
  - path: Packages/Core
    targets:
      - name: Foundation
      - name: Models
        parameters:
          - 'resources: [.process("Assets.xcassets")]'
      - name: CoreTests
        type: test
  
  - path: Packages/Features
    targets:
      - name: Auth
        parameters:
          - 'swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]'
      - name: Cart
      - name: FeaturesTests
        type: test

exclusions:
  imports:
    - CrashlyticsCore

TCA + SwiftUI Project

pragmaMark: true
spaces: 4
headerFileURL: PackageHeader.swift

packageDirectoryTargets:
  - path: Sources/App
    targets:
      - name: AppCore
        parameters:
          - 'swiftSettings: [.enableUpcomingFeature("StrictConcurrency")]'
          - 'swiftSettings: [.defaultIsolation(MainActor.self)]'
      - name: AppUI
      - name: AppTests
        type: test

  - path: Sources/Features/Home
    targets:
      - name: HomeFeature
      - name: HomeUI
      - name: HomeTests
        type: test

Diagnostics

Errors appear in Xcode Report Navigator:

  • ❌ Error: Config file not found β€” no packageGenerator.{yaml,yml,json} at root
  • ❌ Error: YAML decode failed β€” invalid YAML syntax
  • ⚠️ Dropped unresolved import 'SomeLib'. If it's a system framework, ignore this. β€” import not in local targets or auto-discovered/mapped products; add a mappers.imports entry if it's a real dependency
  • πŸ“¦ UnusedModule is used 0 times β€” target never imported by others

Use verbose: "all" for full diagnostics, verbose: "cli" or verbose: "plugin" for targeted output.

FAQ

Q: Why is my target path not found?
A: Check packageDirectoryTargets[].path. Plugin uses shortest-path logic in Sources/ and Tests/. If multiple folders match target name, shortest depth wins.

Q: My old config broke after upgrade.
A: V2 supports old targetsParameters dictβ€”no changes needed. Migrate to inline parameters when ready.

Q: How do I exclude Apple frameworks?
A: They're auto-excluded via the built-in SDK list. No configuration needed. The exclusions.apple config key from older versions is silently ignored if present.

Q: Can I use both YAML and JSON?
A: Yes. Plugin auto-detects by filename. Change file extension to switch formats.

Q: What if a test target doesn't match a regular target?
A: Falls back to first available regular target in same group.

CI/CD

# Generate with all validations
swift package plugin --allow-writing-to-package-directory package-generator

# With custom config file
swift package plugin --allow-writing-to-package-directory package-generator --confFile myconfig.yaml

Fails if Package.swift has errors or config invalid.

Troubleshooting

  • Plugin not visible in Xcode? Run "Resolve Packages" in Package.swift.
  • YAML not working? The plugin converts YAML with its bundled yaml-converter tool; check the YAML syntax, or try the same config as packageGenerator.json.
  • dry-run keeps running? Check dryRun: false in config + check error logs.

Development

swift build --build-tests && swift test                        # plugin, yaml-converter and unit tests
swift test --package-path Architecture                         # architecture rules (swift-llm-check)
swift package --package-path Benchmarks benchmark              # benchmarks (package-benchmark, needs jemalloc)

A command plugin cannot be imported, so the tests and benchmarks compile the plugin's PackagePlugin-free files through symbolic links. Read CODING_STANDARDS.md before changing code; docs/architecture-rules.md lists the rules CI enforces and docs/performance.md how to measure and profile.

License

MIT

About

SwiftPM command plugin that reads your sources' imports and generates Package.swift for heavily modularized Swift projects. Works with PackageGeneratorCLI.

Topics

Resources

Code of conduct

Stars

13 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages