I have a few mono-repospackages and I agree with both of your approaches, combining custom paths and prefix-oriented naming (for discoverability in directories and in content-assist).
What makes it manageable for me in Package.swift
is to consolidate naming, location, and resource logic in code that works from a specification for each node, and then consolidate project-specific conventions in some local builder functions. This makes the package declaration not only more writable/readable, but also changeable: I've altered directory conventions without changing the package declaration itself, which makes experimenting easier. The PackageSpec ends up embodying whatever conventions are commonly adopted.
Stepwise, when building a package, I declare and use a struct PackageSpec
for each dependency node, to consolidate logic (and minimally to avoid duplicating names in strings).
Then in a package builder factory/method, for per-package behavior I implement local factory methods closing over common nodes, and finally declare the package.
Thus, overall Package.swift
top-level structure and factory method organization looks like...
let package = P.factory()
fileprivate enum P {
static func factory() -> Package {
// 1. Nodes as PackageSpec instances...
// 2. Local factory functions wrapping common/core nodes...
// 3. Build Package using Package API's, PackageSpec, and local functions
}
}
/// Manages conventions, common declarations...
fileprivate struct PackageSpec {
typealias PackageDep = PackageDescription.Package.Dependency
typealias TargetDep = Target.Dependency
static let ours = "github-username"
let user: String
let package: String
let product: String
let version: Version
let urlAccess: UrlAccess // ssh or https
let current: Bool // being declared in this package (vs external)
// PackageSpec factories, utilities common to all projects
func asLibrary() .. {}
func asPackDepWith() _ others: [PackageSpec]) -> [PackageDep] {..}
func asTargDepWith(_ others: [PackageSpec]) -> [TargetDep] {
func testName() -> String {...}
...
}
The Package factory function below shows the three phases:
- Common nodes
- Local functions closing over the common nodes
- The package declaration
static func makePackage() -> Package {
// 1. Current package and its products
let bird = PackageSpec("Bird", .init(0,0,1), current: true)
let birdNest = bird.product("BirdNest")
let demo = bird.product("Demo")
let localTree = bird.product("LocalTree")
// External dependencies:
let log = PackageSpec(
package: "swift-log",
"Logging",
by: "apple",
Version(1, 5, 2))
// 2. project-specific logic closing over common nodes
// Declare target that only depends on Bird (in Sources/\(spec.product)/)
func birdLibTarget(_ spec: PackageSpec) -> Target {
PackageDescription.Target.target(
name: spec.product,
dependencies: [bird.asTargetDep()]
)
}
// Declare test target that depends on its peer module and the "fix" Test library
func fixTestTarget(_ spec: PackageSpec, _ otherDeps: [PackageSpec] = []) -> Target {...}
/// 3. Then building Package is concise if lengthy:
let package = Package(
name: bird.package,
platforms: [...],
products: [
bird.asLibrary(),
birdNest.asLibrary(),
places.asExecutable(),
..
],
dependencies: log.asPackDepWith([atomics, markdown, docc]),
targets: [
// MARK: library targets
.target(name: places.product), // direct API
birdLibTarget(birdBits), // method-local API
// MARK: test targets
fixTestTarget(badBird),
...
return package
}
I re-use PackageSpec
by copying across projects; common conventions are implemented with common PackageSpec
, and those projects kinda look like each other. I use one in particular enough that I have a script complain when they differ (for lack of a true common dependency mechanism).