Improving manifest loading performance for declarative package manifests

TL;DR: we can get much faster manifest loading for packages that stick within a declarative subset of the language. Details below---I'd love your thoughts on the approach.

Swift package manifests (Package.swift) are executable Swift code written in a mostly-declarative style. The current package manifest loader calls the compiler to build the package manifest into a program, then runs that program and parses the JSON output describing the package. This allows arbitrary code execution in the package manifest, which is useful, but can be somewhat slow due to the cost of all of these steps. Times are at best in the hundreds of milliseconds, with the average across the whole of the Swift Package Index being around 1.5 seconds on my machine (the median is around 1.3 seconds). The results are cached, but the first-build experience, CI systems, and places where user interaction is involved (e.g., editing the manifest in an IDE) still can feel slow.

Many package manifests are simple and fit into a declarative style: a single Package initialization that declares the various products, dependencies, and targets. Here is a (simplified) version of the package manifest for swift-argument-parser that illustrates what many packages look like:

import PackageDescription

var package = Package(
    name: "swift-argument-parser",
    products: [
        .library(
            name: "ArgumentParser",
            targets: ["ArgumentParser"]),
    ],
    dependencies: [],
    targets: [
        // Core Library
        .target(
            name: "ArgumentParser",
            dependencies: ["ArgumentParserToolInfo"],
            exclude: ["CMakeLists.txt"]),
    ]
)

For these package manifests, we could implement a separate manifest loader that parses the Swift code and evaluates it's syntax tree, without calling the compiler or running any code. Such an implementation should be significantly faster, and is also naturally sandboxed, but is naturally limited to those package manifests that it can fully understand from looking at the syntax.

Approach

This pull request implements a new parsing manifest loader, which uses swift-syntax to parse the manifest directly, and then walks the resulting syntax tree to find the package declaration, its targets, dependencies, and so on. The result is a package manifest that is equivalent to the one produced by building and executing the manifest, but faster.

Limitations

One particularly important aspect of the parsing manifest loader is how it deals with manifests that include executable code or, more generally, code that it does not recognize. When the parsing manifest loader encounters a syntax node is does not recognize, it records a "limitation" associated with that syntax tree node. For example, an if like this

if buildDynamicLibrary {
  products = [
    .library(
      name: "_SwiftSyntaxDynamic",
      type: .dynamic,
      targets: [ /* some targets elided for brevity*/ ]
    )
  ]
} else {
  products = [
    .library(name: "SwiftBasicFormat", targets: ["SwiftBasicFormat"]),
    // more targets elided for brevity
  ]
}

will be recorded as an unrecognized syntax node on the if. The presence of any limitations while processing a manifest indicates that the result of the parsing manifest loader should not be trusted. In such cases, SwiftPM can fall back to the existing (executable) manifest loader, allowing the parsing manifest loader to be treated as an optimization.

With the implementation, one can request that the limitations, if found, are printed to the terminal using the experimental flag --experimental-show-manifest-parser-limitations. For example, the swift-syntax package itself (from which the above excerpt was taken) will report the limitation as follows:

swift-syntax/Package.swift:6:24: error: Unsupported syntax 'expressionStmt' in package manifest [#PackageManifest]
  6 | let products: [Product]
  7 | 
  8 | if buildDynamicLibrary {
    | `- error: Unsupported syntax 'expressionStmt' in package manifest [#PackageManifest]
  9 |   products = [
 10 |     .library(

This flag can be used to determine why a particular package manifest isn't handled by the parsing manifest loader. For package authors, this can be useful to ensure that your particular manifest takes the fast path. For developers of SwiftPM itself, it can be used to identify places where the parsing manifest loader could be extended to support more common patterns, thereby making the parsing manifest loader apply to more manifests.

Validation

The parsing manifest loader is a second implementation of a manifest loader. As such, the primary way to validate it is to ensure that it produces exactly the same results as the existing (executing) manifest loader. Fortunately, it is easy to get a complete view of the package manifest, because SwiftPM can already dump it as JSON with the swift package dump-package operation. The parsing manifest loader therefore has two possible modes of success for any given package manifest:

  1. It can produce identical JSON to what the executing manifest loader produces, or
  2. It can record one or more limitations, indicating that it does not support this manifest.

Either result is correct. The first result is more desirable, because it indicates that we could skip the executing manifest loader entirely. If the parsing manifest loader is sufficiently fast, and enough packages produce the first result, then the parsing manifest loader works as an optimization.

The implementation of the parsing manifest loader adds an experimental command-line option, --experimental-manifest-processing-mode, that can enable the parsing manifest loader. It must be provided with one of the following options:

  • only-parsed: only use the parsing loader. This is not correct, because some manifests require execution, but can be used for testing.
  • only-executed: only use the executing loader. This matches SwiftPM's current behavior, and remains the default at this point.
  • parsed-with-fallback: try using the parsing loader. If it encounters limitations, fall back to the executing loader without failing. This option is what can provide build-time performance improvements when the parsing manifest loader takes over.
  • crosscheck: try both the parsing and the executing loaders and compare the results to ensure they match. If the parsing loader encounters limitations, no further checking will be done. On success, it will print the relative times. If the results differ, it will indicate that the parsing manifest loader has a bug and print the JSON form of the manifest from each parser for future.

Additional benefits

The parsing manifest loader uses swift-syntax, and therefore can benefit from other tools based on swift-syntax. For example, if a manifest declares two targets with the same name, SwiftPM will provide an error message like this:

error: 'pkg': duplicate target named 'pkg'

The parsing manifest loader has complete source location information in the syntax tree and access to the same diagnostics printing machinery that the Swift compiler uses, so it could provide a proper diagnostic pointing at the source location of both target definitions. We could also leverage tools such as Fix-Its to help make the experience of writing package manifests smoother.

As noted earlier, the parsing manifest loader is naturally sandboxed, because it isn't running code at all. This also means that its evaluation of a manifest isn't tied to the host where SwiftPM runs: SwiftPM running on Windows could process a package manifest as-if it were on macOS (e.g., so #if os(macOS) code is included), or by emulating a different set of environment variables.

Risks

The primary risk of the parsing manifest loader is that, as a second implementation, it gets out-of-sync with the executable manifest loader. At worst, it would accept a manifest (without recording any limitations) but produce a different, incorrect result, causing incorrect package builds. The parsing manifest loader is architected to avoid this, always matching what syntax it understands and recording limitations along the failure paths. However, it's ~3,000 lines of code and it's possible that bugs along these lines still exist.

A softer failure condition is that the parsing manifest loader doesn't keep up with changes to the manifest format, so fewer packages can take advantage of the performance improvements from using the parsing manifest loader. This won't be seen as a hard failure, but as a slow degradation in aggregate manifest loading performance over time.

There is also the risk that package manifests only work with the parsing manifest loader, because they take advantage of the fact that it doesn't validate everything about a package manifest that the compiler would. For example, it doesn't check that the arguments to a Package initializer or the target function are provided in the right order. A package that depends on the parsing manifest loader being more lax would not work with the executing loader, for example in an older version of SwiftPM. This would also be annoying when moving from a declarative to an executable package manifest: your first if statement could cause you to need to reshuffle your package manifest a bit to meet the compiler's stricter constraints. Any single problem like this can be addressed by making the parsing manifest loader more strict, but doing so adds complexity and creeps the parsing manifest loader closer to becoming a real compiler and interpreter, possibly undermining its performance advantage.

Results

Assuming that the parsing manifest loader implementation is correct, weighing the benefits and risks comes down to two basic numbers:

  • How fast is the parsing manifest loader?
  • How often do package manifests fit within the limitations of the parsing manifest loader?

Across all of the packages in the Swift Package Index, the parsing manifest loader processes manifests in an average of ~1.36ms, with a median of ~1.28ms. That's about three orders of magnitude faster than the executing manifest loader, which makes this an excellent optimization when it kicks in. Moreover, it's fast enough that it is reasonable to run the parsing manifest loader for every manifest as a silent optimization: if it encounters limitations, we've only wasted a millisecond to find out and can silently fall back. This is the parsed-with-fallback strategy mentioned earlier.

In its current form, the parsing manifest loader successfully handles ~87% of the manifests in the Swift Package Index. The remaining package manifests encounter limitations, so they would still need to be processed using the executing manifest loader. These measurements were taken by performing a swift package describe on every manifest using the crosscheck mode mentioned above. I've fixed all of the cross-checking failures encountered in the SPI, which doesn't guarantee that the parsing manifest loader is correct, but is a large-enough corpus that we're probably close.

Together, these numbers mean that we can get a 1000x speedup for manifest loading for ~87% of the packages in the wild, and we can do so as an optimization---without users having to do anything. The main cost is in maintaining this second implementation of a manifest loader over time.

Possible improvements

The 87% of packages that already work with the parsing manifest loader is very good, but improving that number makes this optimization more beneficial. There are two complementary approaches to improving on that number.

Make the parsing manifest loader smarter

The parsing manifest loader is, in essence, a greatly simplified version of the Swift compiler that cuts out most of the checking. It can be extended to process additional aspects of the Swift language. For example, it's fairly common for a package to pull out common settings into a separate global variable, like this example inspired by the swift-collections package:

let extraSettings: [SwiftSetting] = [
  .enableUpcomingFeature("MemberImportVisibility"),
  .strictMemorySafety(),
  .enableExperimentalFeature("Lifetimes"),
]

and then use those global variables as part of the package manifest definition, e.g.

.target(
    kind: .exported,
    name: "_RopeModule",
    settings: extraSettings + [.swiftLanguageMode(.v5)])
)

The parsing manifest loader could keep track of these global variables and perform the substitution when processing the target. It's still effectively declarative, but allows natural de-duplication.

We could also handle simple mutation patterns that aren't really declarative, but are nonetheless easy to model. For example, swift-argument-parser adds some targets using append(contentsOf:) :

#if os(macOS)
package.targets.append(contentsOf: [
    // Examples
    .executableTarget(
        name: "count-lines",
        dependencies: ["ArgumentParser"],
        path: "Examples/count-lines"),

    // Tools
    .executableTarget(
        name: "changelog-authors",
        dependencies: ["ArgumentParser"],
        path: "Tools/changelog-authors"),
    ])
#endif

The parsing manifest loader already handles the #if using the SwiftIfConfig library. However, it could recognize the append(contentsOf:) call to add these new targets.

These two improvements have been implemented in a separate pull request, because they've shown up fairly often in packages. However, there is a potentially infinite number of such changes we could make to improve the percentage of packages that are handled by the parsing manifest loader. The only cost is complexity in the implementation.

Make the ecosystem more declarative

The performance benefits of the parsing manifest loader as an optimization is likely to push more packages to stay within the limitations of the parsing manifest loader. We could extend the package manifest format to formalize this notion a bit. For example, we could have a syntax to request that the manifest only be used with the parsing manifest loader, e.g., by stating that the manifest is meant to be declarative:

// swift-tools-version: 6.5; (declarative)

In this case, the parsing manifest loader will always be used with this manifest. Any limitations will be treated as user errors, and SwiftPM will not implicitly fall back to the executing manifest loader. Along with this, we could also make it possible for the manifest to state that it is executable, skipping the parsing manifest loader.

// swift-tools-version: 6.5; (executable)

This would be useful for avoiding bugs in the parsing manifest loader, or simply documenting the intent that this work with the executable loader. Looking forward, a future version of SwiftPM could make declarative the default behavior. Users could opt in to executable manifests, but the defaults would strongly nudge them toward staying within the limitations of the parsing manifest loader to keep more of the ecosystem's manifests declarative and fast.

Doug

31 Likes

yes, please!

I can't even begin to wrap my head around how many compute and "developer sitting around slightly annoyed" hours this would safe. I think this alone easily justifies the "risks" column.

it all reads really well thought out, no notes - just joy.

7 Likes

This sounds like an excellent optimization! Dropping from ~1.3 seconds to ~1.3 ms seems like a big enough improvement to warrant adding the additional implementation.

With the two possible improvements that you list, global variables and simple mutations, do you have any data as to what percentage of additional SPM packages would be able to use this fast path for each improvement?

Similarly, do you have any data on how much performance overhead each improvement would add?

Anecdotally, I often use the ā€˜common settings’ pattern in my package manifests and would love to see that supported.

But overall I imagine the trade-offs for improvements would be the number of additional manifests enabled vs the additional overhead added vs complexity/maintainability of the added improvement.

3 Likes

The two together got about 2.5% more packages in the SPI, bringing us up to %89.6 in my latest run.

None that was measurable. I have done absolutely zero work to optimize the parsing manifest loader itself. It doesn't even bail out on encountering the first limitation. If it did, maybe we would have seen an effect because we'd get farther in some unhandled manifests before bailing out.

I think there's a philosophical difference as well. We could say that the append and += support isn't really "declarative", so we shouldn't support it.

Doug

With near-90% adoption, is it perhaps better for the ecosystem if the next major swift-tools version goes all-in on declarative manifests?

It’s worth noting that the Python community had a long history of using an executable setup.py to vend package dependency information as well as perform actual package installation, but chose to pivot from that approach to a declarative syntax. While pip retains compatibility with executable setup.py files, as is being pitched here, I’ve heard a lot of buzz about the uv package- and virtual-environment manager, which exclusively supports declarative manifests. I’m not plugged into the professional Python development community, which has certainly had its issues with breaking changes, but I’m curious whether that buzz represents serious momentum and/or broad adoption of declarative manifests.

Unlike Python, Swift is still in the very early days of its package management story, and might have a chance to execute a more forceful break.

4 Likes

This is a very complicated approach compared to just allowing Package.json (in the current output format of Package.swift). Yes, that'd take a minute to propagate into the ecosystem, but it surely solves most of the technical concerns above in the long run?

It would even allow Package.json and Package.swift to coexist, with Package.json being used for remote package references and maybe auto-generated, and Package.swift allowing local checkouts to have additional customization or flexibility.

1 Like

I would feel that it would be a regression to use .json in comparison to leveraging the swift toolchain and proper strict parsing of the manifest.

At Ordo, we even have built the configuration files for our system as Swift source code, to ensure strict type checking, ability to build tests to verify configurations, etc. It's nice to leverage the compiler really, it is liberating when you get rid of json/yml/toml/xxx configuration files with all the issues associated with that.

It would also bifurcate and require that people learn two formats for the manifest.

Kudos to @Douglas_Gregor - this is a very pragmatic and nice optimisations that just kicks in and helps in a lot of places without changes.

If we want to move to an explicit declarative tools manifest in the future, that is probably not bad, but slightly different follow up perhaps.

6 Likes

Great!
I'm trying to test how much of an improvement this gives on swift package update command on a large repository; but see a lot of errors on standard packages:

swift-package update --experimental-manifest-processing-mode parsed-with-fallback
[...]
Manifest loading encountered limitations for '/Package.swift':
  - Parsing took nanoseconds(296833)
  - Executing took nanoseconds(623625291)
Computed https://github.com/apple/swift-http-types.git at 1.5.1 (1.56s)
Computing version for https://github.com/apple/swift-http-structured-headers.git
Manifest loading encountered limitations for '/Package.swift':
  - Parsing took nanoseconds(311167)
  - Executing took nanoseconds(895018875)
Computed https://github.com/apple/swift-http-structured-headers.git at 1.6.0 (0.93s)
Computing version for https://github.com/apple/swift-metrics
Manifest loading encountered limitations for '/Package.swift':
  - Parsing took nanoseconds(294292)
  - Executing took nanoseconds(614039000)
Computed https://github.com/apple/swift-metrics at 2.10.0 (0.65s)
Computing version for https://github.com/apple/swift-protobuf.git
Manifest loading encountered limitations for '/Package.swift':
  - Parsing took nanoseconds(988250)
  - Executing took nanoseconds(558588625)
Computed https://github.com/apple/swift-protobuf.git at 1.36.1 (0.61s)
Computing version for https://github.com/apple/swift-argument-parser
Manifest loading encountered limitations for '/Package@swift-5.8.swift':
  - Parsing took nanoseconds(526625)
  - Executing took nanoseconds(519931333)
Computed https://github.com/apple/swift-argument-parser at 1.7.1 (0.57s)
Computing version for https://github.com/swift-server/swift-kafka-client
Manifest loading encountered limitations for '/Package.swift':
  - Parsing took nanoseconds(741291)
  - Executing took nanoseconds(518650500)

The limitations are logged 174 times.

Not sure what all-in means here. Personally, I prefer the idea at the end of my original post, where we add "declarative" and "executable" directives to the swift-tools-version, and have some future swift-tools-version default to "declarative". That gives preference for declarative manifests without forcing anything to change.

I really don't want us to do a hard break on the package ecosystem unless we absolutely have to. I went down this path specifically to see if we can get these performance improvements without the hard break.

I wouldn't choose JSON because it's an awful format for humans to read or write, but yes, we could introduce a different, simpler declarative format. TOML is used in other package managers, for example, so let's assume that's the primary alternative.

I would still like that Swift package manifests are Swift code. Swift is quite good for expressing declarative DSLs, and that's what package manifests (generally) are. If you're using SwiftPM you already know or will be learning Swift, so it's a familiar format. With swift-syntax, we have source tools already that help with mechanical edits to package manifests. With the declarative-by-default nudge, I think it also fits the ideas of progressive disclosure pretty well: you stick with declarative as long as you can, and then if you need to do something fancy, you switch to executable.

From an implementation standpoint, doing a new format with something like TOML isn't likely to be that much simpler. You need to pull in the TOML parser and reimplement the mapping from the existing manifests, similar to what I had to do in my implementation. To get any sizable fraction of the package ecosystem to move in a timely manner, you need automatic translation tools to the new format---which would basically be the code in my PR sliced a little differently. You also need those translation tools to validate that the capabilities of the two formats line up, which I get for free. It's plausible that a TOML parser would be slightly faster than what this implementation is doing, but when we're in the millisecond range it won't matter.

The costs of introducing a large-scale transition for the ecosystem are always significant. It's something new for everyone to learn, to plan a transition, to ensure that transition goes smoothly while maintaining compatibility. And if we went that route, we wouldn't see the benefits of the improvements until a critical mass of the ecosystem has moved over.

If this experiment of mine had failed---the new approach wasn't that much faster, or it handled relatively few packages---then I might have a different view (or would have given up).

Just to be very clear, a "limitation" is not an "error". It means the optimization cannot be applied, but it's still correct.

If you want to see the limitations that are preventing the parsing manifest loader from being used on the packages, pass --experimental-show-manifest-parser-limitations. From the output, it also looks like you used my base implementation. The extended implementation pull request handles global variables and appending, which does help a number of packages.

Doug

5 Likes

I mean making the decision that if a package.swift declares a swift-tools version >= X,SwiftPM will never execute it. Packages with earlier swift-tools versions will continue to be (optionally?) executed.

I’m advocating for slightly less than a hard break. Older packages would continue to work. Newer packages will opt into declarative-only packaging by default, and newer Package features will only be accessible to those packages which have opted into declarative packaging.

A true hard break would be to decide that Swift toolchains after a certain version will not support executing project.swift. That would render some legacy packages uninstallable. This is the approach the uv project took, but is likely a step too far for the Swift toolchain’s built-in package infrastructure.

That said, having to execute code to determine dependencies makes server-side dependency resolution and meta-packaging much harder to secure. Supply chain attacks are real, and packaging infrastructure is a typical attack vector. Maybe down the road a hard break will be the ideal option after all.

2 Likes

If the full parse is not cached, that seems like a good optimization to try (first?).

Package.swift changes rarely (possibly once per thousand builds), and it’s the sole source of truth for a given compiler, so it’s a good candidate. The cache format would be specific to a compiler, so there are no version-following issues. That would avoid a host of possible format-following issues with the two implementations, and a whole series of PR’s to support preferred declarations.

1 Like

Those sound very nice but I think what counts at the end is there real world benefit for projects. Not all packages in the package index are equally often used. Some are orders of magnitude more frequently used than others. Those also tend to have more complex package manifest like swift-nio, swift-systems and swift-collections that have for loops and usage of collection algorithms.

It would be nice to see this taken into the account e.g by looking at the number of git clones to get a sense of what the average project will see in performance improvements.

Also, are or can package manifest be resolved in parallel in some cases? If yes, this complicates the math further and one would need to look a the critical path of dependency graphs.

3 Likes

as far as i am concerned declarative manifests are very, very low on my wishlist.

i want a SwiftPM that is stable, that does not periodically vomit build intermediates to the package root. i want to be able to enable LTO in a toolset without thousands of .bc files popping up in the package root. i want to be able to move a git tag without causing SwiftPM to panic everywhere and get stuck in an unrecoverable state. i want to be able to rename a GitHub repo without having to spend an hour hunting down every cache location on macOS because error: Git command config --get remote.origin.url' failed: fatal: cannot change to file or directory won’t go away. i never want to see Another instance of SwiftPM is already running using '/Users/diana/blah/.build' when there is actually no conflicting process ever again.

2 Likes

I had tried on apple/container and still not very many manifests get the fast path.

I was hoping to test this out with Speed up git dependency resolution by 1.5x by vsarunas Ā· Pull Request #9942 Ā· swiftlang/swift-package-manager Ā· GitHub to reduce the ~100ms gaps between requests that are very visible on when a particular project is being fetched out especially in bottom graph:

2 Likes

I guess I feel quite differently about this.

SPM should never have used executable manifests; by the time SPM was being brought up, Python had already switched to toml. Homebrew had been feeling the pain. Cocoapods had switched to declarative podspecs a year earlier. Using Swift as the package manifest format was clearly a mistake before it was made.

Creating an ad-hoc SwiftSyntax based parser does fix some of the problems with an executable manifest format, but as the original post describes, it does so with quite significant costs. In my mind, the most significant is the inevitable incompatibilities between the two parsing modes. It seems the current implementation of the declarative parser doesn't validate argument order, for example — that means that any situation where a package can be published after only being validated by the declarative parser, it may no longer be processable by the executable parser.

Switching to a wholly new format (whether that's JSON or something else) would take longer to percolate through the ecosystem. But at least it wouldn't have the problem of the same data having two blessed-but-conflicting interpretations. And if JSON specifically were chosen, it could be generated directly with existing tooling by executing the existing Package.swift files & checking in the output. Like Cocoapods' solution, writing Swift would still be the primary way of creating the manifest; sufficiently simple packages updating a Package.json would be an optional optimization.

7 Likes

Would Apple’s pkl be a better format for declarative manifests than JSON, YAML, etc?

1 Like

I suspect that this is mostly due to this, below.

Yes, they do use these features, although they probably don't have to. Given an end-user benefit to sticking to the declarative syntax, I suspect these projects that are commonly used would move to a declarative syntax (whatever it is).

They can be; you need to resolve a manifest to find its dependencies, but those can be resolved in parallel. SwiftPM parallelizes the first level of dependencies but not the rest. I don't know why and it would be better to not do that if we can avoid it.

Doug

3 Likes

I understand your viewpoint here.

Assuming the declarative manifest becomes a real thing (e.g., a manifest says whether it is declarative or executable), I don't think this is a significant issue. You might need to do a little more work when moving from declarative to executable if the arguments are out of order, but it's not a big deal. And it's very easy to check if a manifest works with both, e.g., to make sure it works with SwiftPM before this change.

No, but it does push the problem of having two different copies of the data on to package authors for the transition period. That's a cost born by the ecosystem. Transitions are hard, and we should do them when it's

This doesn't actually work in practice. Executable manifests can be environment-sensitive and produce different results, e.g., on different hosts, and the JSON dumped for the package doesn't capture that. You could use something like the parsing manifest loader, of course, which at least knows where it has environment sensitivity.

The key decision point is whether the existing package manifest format (as Swift code) is the path forward, with the parsing manifest loader I'm proposing to make it fast. Or whether to move the ecosystem toward a second, fully-declarative format based on something else, as Keith is proposing. The specific implementation technology for the purely-declarative format (JSON, TOML, pkl, YAML, whatever) is a detail of the second option.

Doug

3 Likes

I'm pro declarative formats in general for Package.swift but I don't see why that discussion has to hold up a clear performance win.

I do, however, think that every effort should be made to treat it as a bug if the declarative parser accepts a manifest which the compiler would reject (e.g. argument order). This makes it a much clearer win & reduces sharp edges where seemingly unrelated changes could cause errors by forcing SwiftPM to fall back to the compiler.


On another note: all this discussion of handling array concatenation suggests to me that PackageDescription is missing a feature - we need dependency & setting groups so these common settings can be applied declaratively by name. This would help manage the sprawl without resorting to array operations. uv's pyproject.toml, for example, has a similar feature for its dependency groups called include-group.

E.g. We should be able to write:

import PackageDescription

var package = Package(
  name: "FooBar",
  dependencies: [
    .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.24.0"),
    .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"5.0.0"),
  ],
  dependencyGroups: [
    .group(
      name: "Common",
      dependencies: [
        .product(name: "AsyncHTTPClient", package: "async-http-client"),
        .product(name: "Crypto", package: "swift-crypto"),
      ]
    )
  ],
  targets: [
    .target(
      name: "FooCore",
      dependencies: [.group("Common")],
      swiftSettings: [.group("Features")]
    ),
    .target(
      name: "BarCore",
      swiftSettings: [.group("Features")]
    ),
    .target(
      name: "Bar",
      dependencies: ["BarCore", .group("Common")],
      swiftSettings: [.group("Features"), .enableUpcomingFeature("ImmutableWeakCaptures")]
    ),
  ],
  swiftSettingsGroups: [
    .group(
      name: "Features",
      swiftSettings: [.enableUpcomingFeature("NonisolatedNonsendingByDefault")]
    )
  ]
)

3 Likes

I love the idea of a truly declarative package manifest. It’s a common pitfall when we’re adding packages to the Swift Package Index that we’re running the validation (essentially swift package dump-package) on Linux where some packages fail due to some platform specific assumptions or because Package.swift files are ā€œtoo cleverā€ and do things that just don’t work except on the developer’s machine or CI environment.

I’m curious about the performance improvement angle, though. Three orders or magnitude is great but is package manifest parsing actually too slow in practise?

Any package operations I’m used to doing are absolutely dominated by package resolution which can take tens of seconds or more. So a second saved wouldn’t really be noticeable and to me the real benefit is the simplification of the manifest by making it truly declarative.

It feels like that’s what this change should lean into.

6 Likes