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?
- Download this repo
- Create Package directory inside your Swift Package
- Copy
Support
folder over - Create
Package/Sources
– this will contain each file for your targets, products, test targets, dependencies, etc… - Create a file at root of
Package
which will contain your package:
Package {
// add products here
}
testTargets: {
// add test targets here
}
- Copy the
package.sh
script to concatenate all files inPackage
to your usablePackage.swift
- $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?!?!
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.