Tuist Registry RFC

Need/problem

We’ve been working on a Tuist implementation of the Package Registry to improve the resolve times. This RFC is focused on the integration at the Tuist CLI and the Xcode level with the already implemented Tuist Package Registry server.

Motivation

Package Registry is a Swift standard to allow downloading source archives for a specific dependency version instead of having deep clones.

We conform to the API contract on our server. There are a couple of things that need to happen to connect to the registry.

Detailed design

The integration looks a bit different based on the type of project at hand, so I’m dividing the detailed design into two sections.

Vanilla SwiftPM projects and Tuist projects

Since we use a vanilla Package.swift for package resolution for Tuist projects with the XcodeProj-based integration, the setup is the same for both.

The configuration of the Package Registry is done in the .swiftpm/configuration/registries.json (or Tuist/.swiftpm/configuration/registries.json).

The JSON needs to roughly look like this:

{
  "security": {
    "default": {
      "signing": {
        "onUnsigned": "silentAllow",
    }
  },
  "authentication": {
    "tuist.dev": {
      "loginAPIPath": "/api/accounts/{account-handle}/registry/swift/login",
      "type": "token"
    }
  },
  "registries": {
    "[default]": {
      "supportsAvailability": false,
      "url": "https://tuist.dev/api/accounts/{account-handle}/registry/swift"
    }
  },
  "version": 1
}

Note the path is dependent on the account that you want to authenticate with.

I’m proposing to add a new command tuist registry setup <account-handle> that will generate the JSON in the right path for the right account. This command would be run once for a given project and then checked in with .git to share the configuration with the rest of the team.

Before resolving packages, developers need to authenticate with the registry. The swift command for this is swift package-registry login <url> --token "Your token". The token is then stored in the user’s keychain. We can also specify the --netrc file, but as per this that can’t be used when Xcode resolves the packages. I’d try to use the keychain but using a keychain could be problematic on the CI as discussed here. If that turns out to be the case, we’d only store the credentials in the keychain for vanilla SwiftPM and Xcode projects, but not for Tuist projects with the XcodeProj-based integration.

Either way, I’m proposing to add a new command tuist registry login that would run the swift package-registry command under the hood with the right URL and the right options. The command would also get a long-lived account access token that can be passed to the swift package-registry login command. On the CI, the token would be equal to the project token stored in the TUIST_CONFIG_TOKEN variable. The token needs to be long-lived as swift CLI doesn’t have a built-in way to refresh an access token (as far as I’m aware).

Last but not least, we need to specify to SwiftPM that it should use the registry instead of the source control when the dependencies are available in the registry. This is done by running swift package --replace-scm-with-registry resolve. We can pass that flag ourselves when tuist install is invoked and we recognize that a registry is set up. We’d provide docs for vanilla SwiftPM projects.

Final flow overview

As a summary, the usage would look as following:

  • tuist registry setup <account-handle> – generates a configuration.json at the right location. <account-handle> is optional – if not passed, we derive it from the Tuist.swift (if possible). Needs to be run once per project.
  • tuist registry login <account-handle> – authenticates with the registry. This will need to be run once by every developer in the team.
  • tuist registry logout <account-handle> – logs out of a given registry. Runs swift package-registry logout <url>.
  • tuist install – augments the swift package resolve with the --replace-scm-with-registry flag.

I don’t think we need a command to remove the registry configuration, documenting how to do that in the docs should suffice.

Xcode projects with the vanilla SwiftPM integration

Currently, the Xcode registry integration is somewhat lacking – there’s no way, afaik, to permanently configure the swizzling between source control and registry resolution like with the --replace-scm-with-registry swift flag.

The configuration and logging in to the registry is the same as for Tuist and SwiftPM projects – with the extra requirement that the credentials to the registry need to be stored in the keychain. Additionally, the path to configure the registry is slightly different and is placed in {name}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/configuration/registries.json, so we would need to cater for that when tuist registry setup is run.

What’s different is that developers need to explicitly specify which dependencies should be used from the registry.

For this, we can use Package Collections. Package collections are added through the Xcode Packages UI. Once the collection is added, Xcode will automatically recognize that a dependency was found in the registry:

You can then choose whether you want to download the dependency from the registry or from source control:
image

The registry identifier is then encoded in the .pbxproj:

/* Begin XCRemoteSwiftPackageReference section */
		F8410D022D0B3F810052EC6E /* XCRemoteSwiftPackageReference "Alamofire" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = Alamofire.Alamofire;
			requirement = {
				kind = upToNextMajorVersion;
				minimumVersion = 5.10.2;
			};
		};
/* End XCRemoteSwiftPackageReference section */

Note the repositoryURL is equal to the registry identifier. For source control, you would see https://github.com/Alamofire/Alamofire.git instead.

To sum up, developers would need to:

  • Set up and login to registry the same way as you would in vanilla SwiftPM or Tuist projects.
  • Add a Tuist Registry collection in their Xcode projects. We’d provide the URL in our docs.
  • Replace already integrated packages with the registry counterparts
  • Profit

Drawbacks

Some of the commands are convenience wrappers for the swift CLI. Developers could instead use the swift CLI directly with the right options that would be specified in the docs.

However, I believe the tuist commands would provide enough of a convenience to justify their cost.

Alternatives

Instead of tuist registry setup, we could add new options in the Tuist.swift file such as:

import ProjectDescription

let tuist = Tuist(
  fullHandle: "tuist/tuist",
  registryEnabled: true
)

We would then generate the configuration.json on every tuist install invocation. However, for vanilla Xcode projects, this is not an option as the packages are resolved directly through Xcode. I believe having the configuration.json generated once and then tracked via .git will lead to a better experience.

Adoption strategy

The registry is opt-in – developers need to run tuist registry setup to start using it. We’ll provide docs explaining the how and why.

Unresolved questions

What do you think about the proposed CLI interface? Any other concerns that you think should be answered here?

I’m very onboard with the plan.

I think wrapping the SPM CLI to streamline those workflows is a good idea. We can make those workflows interactive and take the developer through authentication and account creation/selection if needed.

The part of package collections is a bit annoying, but that’s all we can do. I’d consider creating a discussion/issue if you haven’t done already in the Swift community forum, for Apple to consider introducing a similar fallback mechanism. Once they address it, we can adjust this approach.

This is exciting :rocket:

Hi! And will it be possible to make the registry be deployed/hosted locally? As I understand it, the idea is to integrate with the Tuist Cloud infrastructure, but it would be nice to be able to save time resolving dependencies locally too.

Hi @Ernest0N

SPM already caches incremental resolutions.

Clean resolutions, which this solution tries to solve, requires the mirroring of open source packages in a remote storage that complies with the package registry spec and therefore a server is required.