Hive Hive
Sign in

swifterpm lowercases GitHub locations and removes .git suffixes in Package.resolved

GitHub issue · Open

Metadata
Source
tuist/tuist #11813
Updated
Jul 13, 2026
Domains
Generated projects
Details

What happened?

With Tuist 4.201.0, tuist install produces different Package.resolved files depending on whether TUIST_USE_SWIFTERPM is disabled or enabled.

The minimal DemoKit project below intentionally declares:

  • https://github.com/apple/swift-atomics.git to test preservation of the .git suffix
  • https://github.com/Kyle-Ye/ScreenShieldKit to test preservation of GitHub owner/repository casing

SwiftPM preserves the source-control location spelling from the manifest, including casing and whether the URL has a .git suffix. swifterpm instead lowercases the GitHub owner/repository path and removes the .git suffix.

Dependency TUIST_USE_SWIFTERPM=0 TUIST_USE_SWIFTERPM=1
ScreenShieldKit https://github.com/Kyle-Ye/ScreenShieldKit https://github.com/kyle-ye/screenshieldkit
swift-atomics https://github.com/apple/swift-atomics.git https://github.com/apple/swift-atomics

Both resolver modes produce the same package identities, versions, revisions, and originHash. Both commands also complete successfully, but switching resolver modes dirties the tracked lockfile even though the dependency graph has not changed.

I also verified the same output with Tuist 4.202.2 by passing the SwiftPM-generated lockfile to TUIST_USE_SWIFTERPM=1 tuist install.

Expected behavior

swifterpm should preserve source-control location strings already present in Package.resolved, or serialize them in the same form as SwiftPM, so switching resolver modes does not create unrelated lockfile changes.

How do we reproduce it?

Create the following DemoKit project.

DemoKit
├── mise.toml
├── Project.swift
├── Sources
│ └── DemoKit.swift
└── Tuist
└── Package.swift

mise.toml:

[tools]
tuist = "4.201.0"

Project.swift:

import ProjectDescription
let project = Project(
name: "DemoKit",
targets: [
.target(
name: "DemoKit",
destinations: .macOS,
product: .framework,
bundleId: "dev.tuist.DemoKit",
sources: ["Sources/**"],
dependencies: [
.external(name: "Atomics"),
.external(name: "ScreenShieldKit"),
]
),
]
)

Sources/DemoKit.swift:

public enum DemoKit {}

Tuist/Package.swift:

// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "DemoKitDependencies",
dependencies: [
.package(
url: "https://github.com/apple/swift-atomics.git",
exact: "1.2.0"
),
.package(
url: "https://github.com/Kyle-Ye/ScreenShieldKit",
exact: "0.1.1"
),
]
)
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings()
#endif

Run both resolver modes from clean project-local state:

mise trust
mise install
rm -rf Tuist/.build
rm -f Tuist/Package.resolved
TUIST_USE_SWIFTERPM=0 mise exec -- tuist install
cp Tuist/Package.resolved /tmp/Package.resolved.swiftpm
rm -rf Tuist/.build
rm -f Tuist/Package.resolved
TUIST_USE_SWIFTERPM=1 mise exec -- tuist install
diff -u /tmp/Package.resolved.swiftpm Tuist/Package.resolved

The resulting diff is:

"identity" : "screenshieldkit",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/Kyle-Ye/ScreenShieldKit",
+ "location" : "https://github.com/kyle-ye/screenshieldkit",
"state" : {
"identity" : "swift-atomics",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/apple/swift-atomics.git",
+ "location" : "https://github.com/apple/swift-atomics",
"state" : {

Error log

There is no process error. Both tuist install commands exit successfully. The unexpected output is the Package.resolved diff shown above.

Environment

  • macOS 26.5.2
  • Xcode 26.3 (17C529)
  • Tuist 4.201.0
  • Also reproduced with Tuist 4.202.2
  • swifterpm 0.9.0
Flights

Investigate, reproduce, or fix this item in an isolated repository. Each Flight preserves its outcome and agent session.

New Flights are paused Configure model inference, GitHub, and a sandbox provider to start another Flight. Existing results remain available below.
No Flights yet

Start a Flight and preserve its objective, outcome, and session here.

Comments
D
dosubot[bot] Jul 13, 2026

This is intentional behavior in swifterpm mode. The .git suffix stripping was introduced deliberately to reduce lockfile churn and improve cache reuse when teams use equivalent URLs with different spellings [1].

The canonicalization is provider-aware:

  • GitHub/GitLab URLs: .git suffix is stripped and paths are lowercased
  • Generic Git hosts: .git suffix and path casing are preserved (to avoid breaking private servers)

The rationale is that SwiftPM internally compares dependencies through canonical package locations, but when writing Package.resolved it serializes the “active spelling” — which can vary depending on how different team members or CI systems declared the dependency. Normalizing at the serialization boundary ensures consistent lockfiles regardless of input spelling [2].

That said, the consequence you’re observing — that switching between TUIST_USE_SWIFTERPM=0 and TUIST_USE_SWIFTERPM=1 produces a one-time diff — is a valid ergonomic concern if your team needs to toggle between modes. Once you commit the swifterpm-produced lockfile, subsequent resolves in swifterpm mode should be stable. If you’d like to discuss whether a compatibility option or different canonicalization strategy would be appropriate, I’d recommend opening a thread on the community forum under the “Help” category.

To reply, just mention @dosu.


Share context across your team and agents. Try Dosu.

Leave Feedback Learn about tuist with Dosu Add Dosu to your team

KY
Kyle-Ye Jul 14, 2026

This is intentional behavior in swifterpm mode. The .git suffix stripping was introduced deliberately to reduce lockfile churn and improve cache reuse when teams use equivalent URLs with different spellings https://github.com/tuist/tuist/pull/11400.

Got it. Since this is an AI-generated response, could you confirm whether this behavior is intentional and desirable, @pepicrft?

I agree with the goal behind “normalizing at the serialization boundary ensures consistent lockfiles regardless of input spelling.” However, could this normalization remain internal to swifterpm instead of changing the externally visible Package.resolved?

Package.resolved is a SwiftPM interoperability artifact. Rewriting its location spelling means the same dependency graph produces different lockfiles depending on which resolver writes the file.

Could swifterpm preserve the original location when reading and writing Package.resolved, while computing a canonical location—lowercasing provider paths and removing the .git suffix—when mapping resolved pins into its internal model? That canonical value could then be used for identity comparison, deduplication, and cache keys without changing SwiftPM-compatible output.

This would retain the internal consistency and cache benefits while keeping Package.resolved interchangeable between SwiftPM and swifterpm.

A
alexanderwe Jul 16, 2026

I stumbled over this issue while looking for something similar. From my perspective I agree to the statement from @Kyle-Ye. It would be awesome if tuist can preserve the same writing of Package.resolved as SPM. Diverging between the two “formats” could potentially hinder interchangeability between SwifterPM and SPM.