Home

Awesome

PackageConfig

A Swift Package that allows you to define configuration settings inside a Package.swift - this is so that tools can all keep their configs consolidated inside a single place.

Tool builders use this dependency to grab their config settings.

Package Configuration

The fastest way to insert a configuration in your Package.swift is to add PackageConfig to your dependencies

.package(url: "https://github.com/shibapm/PackageConfig.git", from: "0.13.0")

And add the configuration right at the bottom of your Package.swift

e.g.

#if canImport(PackageConfig)
    import PackageConfig

    let config = PackageConfiguration([
        "komondor": [
            "pre-push": "swift test",
            "pre-commit": [
                "swift test",
                "swift run swiftformat .",
                "swift run swiftlint autocorrect --path Sources/",
                "git add .",
            ],
        ],
        "rocket": [
            "after": [
            	"push",
            ],
        ],
    ]).write()
#endif

Custom Configuration Types

PackageConfig offers also the possibility to create your own configuration type

User writes:

Run this line to have empty source for PackageConfigs target generated for you.

swift run package-config

First time it should return an error error: no target named 'PackageConfigs'.

Now you can list all the required package configs anywhere in the list of targets in Package.swift like this.

// PackageConfig parses PackageConfigs target in Package.swift to extract list of dylibs to link when compiling Package.swift with configurations
.target(name: "PackageConfigs", dependencies: [
    "ExampleConfig" // some executable configuration definition dylib
])

At the very bottom of the Package.swift

#if canImport(ExampleConfig) // example config dynamic library
import ExampleConfig

// invoking write is mandatory, otherwise the config won't be written // thanks captain obvious
let exampleConfig = ExampleConfig(value: "example value").write()
#endif

If more than one dependency uses PackageConfig be sure to wrap each in

#if canImport(SomeLibraryConfig)
import SomeLibraryConfig

let someLibraryConfig = SomeLibraryConfig().write()
#endif

Be sure to invoke write method of the Config otherwise this won't work.

And then to use executable user would need to run this in the same directory as his/her project Package.swift

swift run package-config	# compiles PackageConfigs target, expecting to find a dylib in `.build` directory for each of the listed libraries configs
swift run example		# runs your library executable

Tool-dev writes:

For the sake of example lets assume your library is called Example then Package.swift would look like this:

let package = Package(
    name: "Example",
    products: [
        // notice that product with your library config should be dynamic library in order to produce dylib and allow PackageConfig to link it when building Package.swift
        .library(name: "ExampleConfig", type: .dynamic, targets: ["ExampleConfig"]),
        // 
        .executable(name: "example", targets: ["Example"]),
    ],
    dependencies: [
        .package(url: "https://github.com/shibapm/PackageConfig.git", from: "0.0.2"),
    ],
    targets: [
        .target(name: "ExampleConfig", dependencies: ["PackageConfig"]),
        .target(name: "Example", dependencies: ["ExampleConfig"]),
    ]
)

In your ExampleConfig target define ExampleConfig like this.

import PackageConfig

// it must be public for you to use in your executable target
// also you must conform to `Codable` and `PackageConfig`
public struct ExampleConfig: Codable, PackageConfig {

    // here can be whatever you want as long as your config can stay `Codable`
    let value: String

   	// here you must define your config fileName which will be used to write and read it to/from temporary directory
    public static var fileName: String { return "example-config.json" }

    // public init is also a requirement
    public init(value: String) {
	self.value = value
    }
}

Then for example in your main.swift in executable Example target you can load your config like this:

import ExampleConfig

do {
    let config = try ExampleConfig.load()
    print(config)
} catch {
    print(error)
}

Notes for library developers

Since YourConfig target is a dynamic library you must ensure that you have built it everytime when using either read or write methods of PackageConfig. When building from terminal this can be done by just running swift build.


Changelog

How it all works

When you invoke YourPackage.load() it will compile the Package.swift in the current directory using swiftc.

While compiling it will try to link list of dynamic libraries listed in PackageConfigs target.

When it is compiled, PackageConfig will run and when YourPackage.write() is called your package configuration json will be written to temporary directory.

After that it will try to read the json and decode it as if it was YourPackage type, providing it back to where you have invoked load method.

Debugging

How to see the JSON from a Package.swift file

Use SPM with verbose mode:

~/d/p/o/i/s/PackageConfig  $ swift build --verbose

And grab the bit out after the first sandbox. I then changed the final arg to -fileno 1 and it printed the JSON.

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc --driver-mode=swift -L /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4_2 -lPackageDescription -suppress-warnings -swift-version 4.2 -I /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk /Users/ortatherox/dev/projects/orta/ios/spm/PackageConfig/Package.swift -fileno 1

{"errors": [], "package": {"cLanguageStandard": null, "cxxLanguageStandard": null, "dependencies": [], "name": "PackageConfig", "products": [{"name": "PackageConfig", "product_type": "library", "targets": ["PackageConfig"], "type": null}], "targets": [{"dependencies": [], "exclude": [], "name": "PackageConfig", "path": null, "publicHeadersPath": null, "sources": null, "type": "regular"}, {"dependencies": [{"name": "PackageConfig", "type": "byname"}], "exclude": [], "name": "PackageConfigTests", "path": null, "publicHeadersPath": null, "sources": null, "type": "test"}]}}

How I verify this works

I run this command:

swift build; env DEBUG="*" swift run package-config-example

if you don't use fish:

swift build; DEBUG="*" swift run package-config-example