Simplify the management of your Package.swift file with PackageDSL

PackageDSL

Simplify the management of your Package.swift file with PackageDSL:

import PackageDescription

let package = Package {
  BushelCommand()
  BushelLibraryApp()
  BushelMachineApp()
  BushelSettingsApp()
  BushelApp()
}
testTargets: {
  BushelCoreTests()
}
.supportedPlatforms {
  WWDC2023()
}
.defaultLocalization(.english)

Why?

I was having a difficult time managing a large Package.swift file. Why go with this instead:

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "BushelKit",
  defaultLocalization: "en",
  platforms: [.macOS(.v14), .iOS(.v17), .watchOS(.v10), .tvOS(.v17)],
  products: [
    // Products define the executables and libraries a package produces, making them visible to other packages.
    .library(
      name: "BushelApp",
      targets: ["BushelApp"]
    ),
    .library(
      name: "BushelSettingsApp",
      targets: ["BushelSettingsApp"]
    ),
    .library(
      name: "BushelLibraryApp",
      targets: ["BushelLibraryApp"]
    ),
    .library(
      name: "BushelMachineApp",
      targets: ["BushelMachineApp"]
    ),
    .executable(name: "bushel", targets: ["bushel"])
  ],
  dependencies: [
    .package(url: "https://github.com/brightdigit/FelinePine.git", from: "0.1.0-alpha.3"),
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0")
  ],
  targets: [
    .target(name: "BushelCore"),
    .target(name: "BushelLocalization"),
    .target(name: "BushelUT", dependencies: ["BushelCore"]),
    .target(name: "BushelLogging", dependencies: ["FelinePine"]),
    .target(name: "BushelArgs", dependencies: [
      .product(name: "ArgumentParser", package: "swift-argument-parser")
    ]),

    .target(name: "BushelDataCore", dependencies: ["BushelLogging"]),
    .target(name: "BushelViewsCore", dependencies: ["BushelUT", "BushelLogging"]),

    .executableTarget(name: "bushel", dependencies: ["BushelArgs"]),
    .target(name: "BushelApp", dependencies: ["BushelViews", "BushelVirtualization", "BushelLibrary", "BushelData", "BushelMachine"]),
    .target(name: "BushelViews", dependencies: ["BushelLibraryViews", "BushelMachineViews", "BushelSettingsViews"]),
    .target(name: "BushelData", dependencies: ["BushelLibraryData", "BushelMachineData"]),

    .target(name: "BushelSettingsApp", dependencies: ["BushelSettingsViews"]),
    .target(name: "BushelSettingsViews", dependencies: ["BushelData", "BushelLocalization"]),

    .target(name: "BushelLibrary", dependencies: ["BushelLogging", "BushelCore"]),
    .target(name: "BushelLibraryData", dependencies: ["BushelLibrary", "BushelLogging", "BushelDataCore"]),
    .target(name: "BushelLibraryViews", dependencies: ["BushelLibrary", "BushelLibraryData", "BushelLogging", "BushelUT", "BushelViewsCore"]),
    .target(name: "BushelLibraryApp", dependencies: ["BushelLibraryViews", "BushelLibraryMacOS"]),

    .target(name: "BushelMachine", dependencies: ["BushelLogging", "BushelCore"]),
    .target(name: "BushelMachineData", dependencies: ["BushelMachine", "BushelLogging", "BushelDataCore"]),
    .target(name: "BushelMachineViews", dependencies: ["BushelMachine", "BushelMachineData", "BushelLogging", "BushelUT", "BushelLocalization", "BushelViewsCore"]),
    .target(name: "BushelMachineApp", dependencies: ["BushelMachineViews", "BushelMachineMacOS"]),

    .target(name: "BushelVirtualization", dependencies: ["BushelLibraryMacOS", "BushelMachineMacOS"]),

    .target(
      name: "BushelLibraryMacOS",
      dependencies: ["BushelLibrary"]
    ),
    .target(name: "BushelMachineMacOS", dependencies: ["BushelMachine"])
  ]
)

There has to be a better way that takes advantage of the DSL capabilities of Swift.

What is this?

If you have a large enough Swift Package this is ideal for you.

Setup individual targets, products, and dependencies using this DSL and create an easily organized, simplified, and easy to maintain Package for your Swift project.

How do you install it?

  1. Download this repo
  2. Create Package directory inside your Swift Package
  3. Copy Support folder over
  4. Create Package/Sources – this will contain each file for your targets, products, test targets, dependencies, etc…
  5. Create a file at root of Package which will contain your package:

 Package {
  // add products here
}
testTargets: {
  // add test targets here
}
  1. Copy the package.sh script to concatenate all files in Package to your usable Package.swift
  2. $Profit$

Here’s the structure I use for Bushel’s Swift Package:

├── Package* // new folder you create
│   ├── Sources* // all files listing your targets, dependencies, products, etc...
|   |   ├── BushelApp.swift // definition of `BushelApp` product
│   └── Support* // copied from this repo
├── Package.resolved
├── Package.swift // built by `package.sh`
├── package.sh* // copied from this repo
├── Sources // actual source code of my package targets
│   ├── BushelApp
│   ├── BushelArgs
│   ├── BushelCore
│   ├── BushelData
│   ├── BushelDataCore
│   ├── BushelLibrary
│   ├── BushelLibraryApp
│   ├── BushelLibraryData
│   ├── BushelLibraryMacOS
│   ├── BushelLibraryViews
│   ├── BushelLocalization
│   ├── BushelLogging
│   ├── BushelMacOSCore
│   ├── BushelMachine
│   ├── BushelMachineApp
│   ├── BushelMachineData
│   ├── BushelMachineMacOS
│   ├── BushelMachineViews
│   ├── BushelSettingsApp
│   ├── BushelSettingsViews
│   ├── BushelUT
│   ├── BushelViews
│   ├── BushelViewsCore
│   ├── BushelVirtualization
│   └── bushel
└── Tests // actual source code of my package test targets
    └── BushelCoreTests
* the new stuff from PackageDSL

How does it work?

The Support folder contains a group of Swift source files which define an easy DSL which translates the common parts of the PackageDescription namespace you use.

Creating a Package

A typical Package.swift might look like this:

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "BushelKit",
  ...
  products: [
    .library(
      name: "BushelApp",
      targets: ["BushelApp"]
    ),
    .library(
      name: "BushelSettingsApp",
      targets: ["BushelSettingsApp"]
    ),
    .library(
      name: "BushelLibraryApp",
      targets: ["BushelLibraryApp"]
    ),
    .library(
      name: "BushelMachineApp",
      targets: ["BushelMachineApp"]
    ),
    .executable(name: "bushel", targets: ["bushel"])
  ],
  ...
  targets: [
    ...
    .testTarget(name: "BushelCoreTests", dependencies: ["BushelCore"])
  ]
)

With PackageDSL you can create a Package simply by defining the products of your package:

import PackageDescription

let package = Package {
  BushelCommand()
  BushelLibraryApp()
  BushelMachineApp()
  BushelSettingsApp()
  BushelApp()
}
testTargets: {
  BushelCoreTests()
}

Targets and dependencies are automatically pulled from your products and added the target section of your package. So for instance with BushelApp:

struct BushelApp: Product, Target {
  var dependencies: any Dependencies {
    BushelViews()
    BushelVirtualization()
    BushelMachine()
    BushelLibrary()
    BushelData()
  }
}

The dependencies listed such as BushelViews is automatically added to the Package as:

  .target(name: "BushelViews", dependencies: ["BushelLibraryViews", "BushelMachineViews", "BushelSettingsViews"]),

Also the dependencies from BushelView are added as well:

struct BushelViews: Target {
  var dependencies: any Dependencies {
    BushelLibraryViews()
    BushelMachineViews()
    BushelSettingsViews()
  }
}

It’s recursive!!!

How about remote dependencies?

Remote dependencies (i.e. Package.Dependency) are denoted by the PackageDependency protocol and just take a standard Package.Dependency property called dependency. Here’s an example of how to add the Swift Argument Parser:

struct ArgumentParser: PackageDependency {
  var dependency: Package.Dependency {
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0")
  }
}

Then just add it as a dependency to your target:

struct BushelArgs: Target {
  var dependencies: any Dependencies {
    ArgumentParser()
    BushelCore()
  }
}

How about test targets?

You can create a test target with the TestTarget protocol:

struct BushelCoreTests: TestTarget {
  var dependencies: any Dependencies {
    BushelCore()
  }
}

Then add it to your list of test targets using the testTarget argument:

let package = Package {
  BushelCommand()
  BushelLibraryApp()
  BushelMachineApp()
  BushelSettingsApp()
  BushelApp()
}
testTargets: {
  BushelCoreTests() // right here
}

How about language and platforms?

Right now there are two modifier methods to do this. defaultLocalization which takes in a LanguageTag and supportedPlatforms which can take in a list of platforms or a PlatformSet. A PlatformSet is useful if use a to define a set of platforms for a specific year such as:

struct WWDC2023: PlatformSet {
  var body: any SupportedPlatforms {
    SupportedPlatform.macOS(.v14)
    SupportedPlatform.iOS(.v17)
    SupportedPlatform.watchOS(.v10)
    SupportedPlatform.tvOS(.v17)
  }
}

Rather then define your platforms as:

let package = Package {
  ...
}
.supportedPlatforms {
  SupportedPlatform.macOS(.v14)
  SupportedPlatform.iOS(.v17)
  SupportedPlatform.watchOS(.v10)
  SupportedPlatform.tvOS(.v17)
}

You can simplify it as:

let package = Package {
  ...
}
.supportedPlatforms {
  WWDC2023()
}

FAQ

But it doesn’t do this?!?! How about this?!?!? I don’t know how to do this?!?!

Create an issue.

Why would I do this?

If your package gets big enough, it becomes difficult to manage. This is way to do that. If your package is small enough, I don’t recommend it yet.

Thanks

GitHub

View Github