Using SwiftPM 5.6 and supporting older versions

I have packages that I would like to use some features included in Swift 5.6, but while retaining backwards compatibility.

To do this I declare swift-tools-version:5.2 in the main Package.swift file and I've then added a Package@swift-5.6.swift file.

When performing package actions (such as update or resolve) with Swift 5.6 it replaces the Package.resolved file with a "version 2" file that is not compatible with Swift versions prior to 5.6.

Is it possible to have Swift 5.6 not update the version of the Package.resolved file? I have to ensure not to commit the changes that are automatically made or remember to use an older version of Swift whenever dealing with this package.

4 Likes

For clients depend on your package, it seems like they will not use your package’s Package.resolved file.

And for those developing your package, they can just delete Package.resolved and Xcode will generate a new one according to their local env.

If you do not want to update the Package.resolved to a ā€œv2 versionā€, you can use the older Xcode version.(NOT RECOMMENDED)

My recommendation is to add Package.resolved to .gitignore file.

That's a bad idea, at least if you work with anyone else. Resolved files keep groups in sync, so it's important not to ignore it.

@josephduffy Can you explain why you need to be careful here? Consumers of the package won't care about your resolved file, so do you have to support many Swift versions for local development of the package?

2 Likes

At the minimum it's very important to be able to test a package with all versions of Swift that you claim to support. IME it's very easy to write code which accidentally won't work in older versions.

@Jon_Shier Thomas has explained it perfectly. I run tests via GitHub actions against old Xcode versions and older Swift versions on Linux. This would require deleting the Package.resolved file before testing or ensuring the version 2 migration is never committed.

1 Like

I mean, you can just make sure not to commit the v2 resolved file from Swift 5.6. 5.6 can use the v1 manifest just fine, just don't use it to update dependencies. It is unfortunate that SPM shipped a breaking manifest change as part of a minor release. @NeoNacho Is there any backward compatibility affordance here? Can we version the resolved files? Can we tell 5.6 to use the older manifest format?

This also means making any changes to dependencies (e.g. adding a new one, changing the version) in an older version of Swift.

This isn't a huge deal at all, but it would be great if this were easier!

The only backwards compatibility we currently have here is that if you are using a pre 5.6 tools-version for your manifest, you will get the v1 resolved file format.

I think it might make sense to extend that such that as long as there is at least one pre-5.6 manifest present, we use the v1 format. @tomerd wdyt?

related: Package.resolved should go in the .gitignore

1 Like

some thoughts:

  1. v2 kicks in only when the manifest has declared tools-version 5.6, which one only needs to do when using manifest 5.6 features.

  2. iirc, we only write package.resolve if there are changes detected, so a 5.6 and 5.2 manifest with the same dependencies versions should not cause a new resolved file to be written

  3. we could potentially add a flag to "force v1 format" to help the transition

As I recall, it's also used to choose the major Swift version when consuming packages. While we aren't yet at the Swift 6 stage, the Swift 4 to 5 transition had bugs when packages with v4 package files were used in a v5 package. Also the annoyance of Xcode telling you to undergo the Swift 5 update when one of your packages was still on v4. So I'm pretty sure there are other impacts to not using updated versions of the package.

As I recall, it's also used to choose the major Swift version when consuming packages.

yes it can be used this way too

I believe this should be the solution for the exact case.

Forcing the v1 format when an older package file is found sounds ideal. Another use case for this is adding or updating a dependency, which ideally would be done using the current version of Swift (soon to be 5.6). As it stands we'll need to switch to an older version when making these changes.

Hi

Any news on this topic?

I switched from XCode 13.2.1 to XCode 13.3.1

In the past, the package.resolved was changed to version 2

Now it hasn't happened, why?

How can I upgrade to version 2?

Is it importato doing it? and why?

Thanks

If an existing .resolved file is still logically valid, SwiftPM will not bother to overwrite it. So no matter what, it will not ā€œupgradeā€ until you actually change some dependency constraints.

When it does have to write out a new file, it will try to use the version corresponding to the tools version and any versioned manifests present. That way it still writes the pins in a format that any older toolchains supported by the package can still read. (Without this behaviour, the older toolchain would then fail to read the file and resolve anew instead, resulting in it ping‐ponging back and forth as you switched between toolchains.)

Once your package actually updates the tools version in the manifest, then SwifPM will start writing the pins file in the new format. At this point you may be using features the old format cannot express. And since old toolchains cannot load the manifest anymore anyway, there are no compatibility concerns left.

It will happen automatically whenever it makes sense. There is no benefit to be had from trying to force it to update.

1 Like

This change breaks CI testing that relies on older Xcode/iOS simulators. Is there any plan to address this with a flag to opt-in for v1 manifest format instead of v2? All I ever asked for is to have a git endpoint and commit hash. How hard can that be without breaking all CI/dev environments around the globe with no added benefit? Sigh...

Here is my little script for CI that converts manifest file from v2 to v1 while preserving revisions which is enough to pull the specific commit from remote:

jq '{
  "object": {
    "pins": .pins | map({
      "package": .identity,
      "repositoryURL": .location,
      "state": .state
    })
  },
  "version": 1
}' Package.resolved > Package.resolved.out
mv Package.resolved.out Package.resolved

That sounds very fragile. If you want to ensure specific versions of dependencies, you should do that in the package definition. Also consider cloning the dependencies into your own git server and only resolving from there. That will protect you against people doing stupid things like moving tags or deleting the repo altogether Ć  la npm left pad fiasco.

In practice it's not fragile at all. The goal isn't to lock to a single version but to keep everyone in sync with the version used by the project. This allows the dependencies to be updated regularly while keeping team members in sync with the latest building code. And in 10 years I think I've seen a grand total of one iOS dependency lost because it was deleted from git. There are a variety of strategies to deal with that but it's a non issue in the Swift ecosystem right now.

1 Like

Because nobody would ever push a broken commit to a Swift package?