[Pre-pitch] SwiftPM Manifest based on Result Builders

Thank you so much @tomerd for writing this pre-pitch. I can definitely feel the key issues you mentioned every time I work with manifest files. As I care about this topic as well, I have been thinking about same problems for some time now, and I came up with a couple of ideas.

I would like to propose few changes to your proposal that I think could make the package manifest even simpler. While doing about this, I have put focus on the following areas:

  • Eliminating internal string references. I think this is a very important topic, and I don't believe that we should have a major manifest redesign without addressing this issue.
  • Following the principles of progressive disclosure. A simple package should be easy to declare, while more advance features should be disclosed when needed.
  • Trying to make changes a sugar on top of the existing implementation. I didn't stick strongly to this one, but it might be close.

Here it goes.

Firstly, I think the Package type initializer should be a result builder where we define public package products. Secondly, I follow your idea of merging products and targets into a single entity.

With that, the simplest package could look like this:

Package {
    Library("MyLibrary")
}

Next, products often have dependencies. The Library type gets an initializer to declare dependencies through a result builder. Additionally, we allow declaring products within declarations of other products. Those products are dependencies of the parent product and are private by default, but can be made public, package, products if needed.

Package {
    Library("MyLibrary") {
        Library("MyUtilities")
        Library("MyDataModels", public: true)
    }
}

We also allow declaring external dependencies within declarations of other products. External packages get an API to reference their products.

Package {
    Library("MyLibrary") {
        Library("MyUtilities")
        Library("MyDataModels")
        ExternalPackage(at: "https://git-service.com/foo/some-package", upToNextMajor: "1.0.0")
            .library("SomeLibrary")
    }
}

When external packages are dependencies of multiple products, then they should be pulled out into a variable.

Package {

    let someLibrary = ExternalPackage(at: "https://git-service.com/foo/some-package", upToNextMajor: "1.0.0")
        .library("SomeLibrary")

    Library("MyLibrary") {
        Library("MyUtilities")
        Library("MyDataModels") {
            someLibrary
        }
        someLibrary
    }
}

I believe this is very important as it eliminates the need for inventing external packages name that can be used as string references, which is confusing for users and I've noticed it made a mess in the SwiftPM implementation.

The same should be done for package products that are dependencies of multiple other products.

Package {

    let myLowLevelLibrary = Library("MyLowLevelLibrary")

    Library("MyLibrary") {
        Library("MyUtilities") {
            myLowLevelLibrary
        }
        Library("MyDataModels") {
            myLowLevelLibrary
        }
    }
}

Saving an instance into a variable, instead of using string references, is a natural thing for a developer to do when passing the same thing to multiple functions. It eliminates the possibility of making a typo in one place and allows the compiler to statically evaluate correctness.

Finally, we can declare test products within products they are supposed to test, through a second result builder closure, eliminating the need for referencing the target product, which becomes an implicit dependency. Loving the application of SE-0279 here!

Package {
    Library("MyLibrary") {
        Library("MyUtilities")
        Library("MyDataModels")
    } tests {
        Tests("MyLibraryTests")
    }
}

Of course, we allow multiple test targets, as well as test targets having their own dependencies.

Package {
    Library("MyLibrary") {
        Library("MyUtilities")
        Library("MyDataModels")
    } tests {
        Tests("MyLibraryTests")
        Tests("MyLibraryOtherTests") {
            Library("MyTestUtilities")
        }
    }
}

Your full example:

import PackageDescription

let someLibrary = ExternalPackage(at: "https://git-service.com/foo/some-package", upToNextMajor: "1.0.0")
    .library("SomeModule")
let someOtherLibrary = ExternalPackage(at: "https://git-service.com/foo/some-other-package", upToNextMajor: "1.0.0")
    .library("SomeOtherModule")
let myDataModelLibrary = Library("MyDataModel", path: "custom/path", swiftSettings: "swiftSettings")

Package {
    Executable("MyExecutable") {
        myDataModelLibrary
        someLibrary
        Library("MyUtilities")
    } tests {
        Tests("MyExecutableTests") {
            Library("MyTestUtilities")
        }
    }
    Library("MyLibrary") {
        myDataModelLibrary
        someOtherLibrary
    } tests {
        Tests("MyLibrary")
    }
}
2 Likes