More Build Adventures
April 24, 2018

After my last blog post back in March about the Swift Package Manager (SPM) and some experiments I was layering on top of it, I actually continued with a bit more work, but didn’t get around to blogging about it.

Cleaner Example

One of the things I did was to split the example out into a separate repo.

Hopefully this makes it a little bit easier to see how, if you use Builder in a project, you are able to pull in all of your tool dependencies (including builder itself), just with a single line of bootstrapping using SPM.

Better Configuration

I also worked a bit on a cleaner way to specify settings and schemes/actions.

Using SPM (and Builder) encourages you to break things up into small modules, and so to do this I added a BuilderConfiguration package that you can pull in to your Configure target, and which lets you specify both the configuration and the build settings using a similar style of syntax to SPM’s own Packages.swift file.

To use this, you just include it in the dependencies section of your Packages.swift file:

    dependencies: [
      .package(url: "https://github.com/elegantchaos/Builder.git", from: "1.0.3"),
      .package(url: "https://github.com/elegantchaos/BuilderConfiguration.git", from: "1.1.2"),
    ],

and then list it as a dependency for your Configure target:

    .target(
        name: "Configure",
      dependencies: ["BuilderConfiguration"]),

This allows you to import BuilderConfiguration from within your Configure target, which lets you write a nice clean configuration like so:


import BuilderConfiguration

let settings = /* see below for more details... */

let configuration = Configuration(
    settings: settings,
    actions: [
        .action(name:"build", phases:[
            .buildPhase(name:"Building", target:"Example"),
            .toolPhase(name:"Packaging", tool: "Packatron", arguments:["application", "com.elegantchaos.example"]),
            ]),
        .action(name:"test", phases:[
            .testPhase(name:"Testing", target:"Example"),
            ]),
        .action(name:"run", phases:[
            .actionPhase(name:"Building", action: "build"),
            .toolPhase(name:"Running", tool: "run", arguments:["Example"]),
            ]),
    ]
)

For simplicity I’ve glossed over the settings part for now, but you can see how the configuration itself can be specified as some settings and some actions.

Actions represent discrete tasks you’ll want to perform. They will typically be things like build, run, test - but they aren’t limited to a particular set of names, you can add as many as you need.

Each action has a name, and some phases.

Each phase represents a “command” that builder knows how to perform. The current set is:

The most interesting of these is the toolPhase, which you use to run other tools. This is what allows you to call out to perform custom build steps.

Where do these tools come from?

They’re SPM packages of course. You just add them in as dependencies, and they are fetched and built along with everything else. For example, above we’re calling out to an imaginary tool called “Packatron” to bundle up the application.

For this to work, we amend the dependencies like so:

    dependencies: [
      .package(url: "https://github.com/elegantchaos/Builder.git", from: "1.0.3"),
      .package(url: "https://github.com/elegantchaos/BuilderConfiguration.git", from: "1.1.2"),
      .package(url: "https://github.com/elegantchaos/Packatron.git", from: "1.0.6"),
    ],

and add the tool to the target dependencies:

    .target(
        name: "Configure",
      dependencies: ["BuilderConfiguration", "Packatron"]),

With these steps done, the Packatron tool will be fetched and built when our project is first built, and can then be used from toolPhase.

Settings

Earlier I glossed over how you specify settings.

This is done by defining “schemes”1.

Schemes are hierarchical, in the sense that one scheme can inherit values from others.

This allows you to split up your settings in a logical way. For example if you have multiple products that share some settings, but have other settings that vary, you can make a scheme for each one and another for the common settings.

You can also make the scheme inheritance conditional, based on a filter. This allows you to effectively say “include this scheme if you’re on this platform”, or “include this scheme if you’re building this configuration”.

This results in quite a flexible system which hopefully allows settings to be specified in a clean, logical manner:

let settings = Settings(schemes: [
    .baseScheme(
        swift: ["Dexample"],
        inherits: [
            .scheme(name: "mac", filter: ["macOS"]),
            .scheme(name: "debug", filter: ["debug"])
        ]
    ),
    .scheme(
        name: "mac",
        swift: ["target", "x86_64-apple-macosx10.12"]
    ),
    .scheme(
        name: "debug",
        swift: ["Onone"]
    )
    ]
)

The settings for each scheme are specified as a list of strings.

There are currently four categories of setting that you can specify:

Behind the scenes these values get converted into flags which get passed to the relevant compiler when SPM is invoked.

So in the example above:

swift: ["target", "x86_64-apple-macosx10.12"]

will ultimately be passed to swift build as -Xswiftc "target" -Xswiftx "x86_64-apple-macosx10.12"

Future Enhancements

In the long run the Swift team are working on some proposals to enhance SPM, which will probably make Builder obsolete.

The approach that Builder is taking is currently not really focussed on performance, and is more about experimenting with finding a good syntax for some of this stuff, and just exploring the problem space.

Whilst the SPM solution hasn’t arrived, therefore, I will probably continue to tinker.

The next things I’m looking at are:

More on that in future blog posts…

  1. I think this is probably a bad name, since it will be confusing to people used to Xcode Schemes. I think “specifications” (or “specs”) may end up being a better name.