Skip to content

Commit 667a006

Browse files
neonichuabertelrud
andauthored
Improve version-specific manifest selection (#6801)
Currently, we only pick a non-exact match for version-specific manifests if its tools-version is *higher* than the regular one. That seems like an edge case only and we should *also* pick it if the regular manifest is incompatible. This both matches intuition as well as our current documentation on the topic. It should also be backwards-compatible with the current behavior since the new behavior only comes into play if we would have previously failed to load a manifest at all. Co-authored-by: Anders Bertelrud <[email protected]>
1 parent e08363e commit 667a006

File tree

3 files changed

+84
-40
lines changed

3 files changed

+84
-40
lines changed

Sources/PackageLoading/ToolsVersionParser.swift

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -583,8 +583,6 @@ extension ManifestLoader {
583583
}
584584
}
585585

586-
// Otherwise, check if there is a version-specific manifest that has
587-
// a higher tools version than the main Package.swift file.
588586
let contents: [String]
589587
do { contents = try fileSystem.getDirectoryContents(packagePath) } catch {
590588
throw ToolsVersionParser.Error.inaccessiblePackage(path: packagePath, reason: String(describing: error))
@@ -607,39 +605,47 @@ extension ManifestLoader {
607605

608606
let regularManifest = packagePath.appending(component: Manifest.filename)
609607

610-
// Find the newest version-specific manifest that is compatible with the the current tools version.
611-
if let versionSpecificCandidate = versionSpecificManifests.keys.sorted(by: >).first(where: { $0 <= currentToolsVersion }) {
608+
// Try to get the tools version of the regular manifest. As the comment marker is missing, we default to
609+
// tools version 3.1.0 (as documented).
610+
let regularManifestToolsVersion: ToolsVersion
611+
do {
612+
regularManifestToolsVersion = try ToolsVersionParser.parse(manifestPath: regularManifest, fileSystem: fileSystem)
613+
}
614+
catch let error as UnsupportedToolsVersion where error.packageToolsVersion == .v3 {
615+
regularManifestToolsVersion = .v3
616+
}
612617

613-
let versionSpecificManifest = packagePath.appending(component: versionSpecificManifests[versionSpecificCandidate]!)
618+
// Find the newest version-specific manifest that is compatible with the current tools version.
619+
guard let versionSpecificCandidate = versionSpecificManifests.keys.sorted(by: >).first(where: { $0 <= currentToolsVersion }) else {
620+
// Otherwise, return the regular manifest.
621+
return regularManifest
622+
}
614623

615-
// SwiftPM 4 introduced tools-version designations; earlier packages default to tools version 3.1.0.
616-
// See https://swift.org/blog/swift-package-manager-manifest-api-redesign.
617-
let versionSpecificManifestToolsVersion: ToolsVersion
618-
if versionSpecificCandidate < .v4 {
619-
versionSpecificManifestToolsVersion = .v3
620-
}
621-
else {
622-
versionSpecificManifestToolsVersion = try ToolsVersionParser.parse(manifestPath: versionSpecificManifest, fileSystem: fileSystem)
623-
}
624+
let versionSpecificManifest = packagePath.appending(component: versionSpecificManifests[versionSpecificCandidate]!)
624625

625-
// Try to get the tools version of the regular manifest. At the comment marker is missing, we default to
626-
// tools version 3.1.0 (as documented).
627-
let regularManifestToolsVersion: ToolsVersion
628-
do {
629-
regularManifestToolsVersion = try ToolsVersionParser.parse(manifestPath: regularManifest, fileSystem: fileSystem)
630-
}
631-
catch let error as UnsupportedToolsVersion where error.packageToolsVersion == .v3 {
632-
regularManifestToolsVersion = .v3
633-
}
626+
// SwiftPM 4 introduced tools-version designations; earlier packages default to tools version 3.1.0.
627+
// See https://swift.org/blog/swift-package-manager-manifest-api-redesign.
628+
let versionSpecificManifestToolsVersion: ToolsVersion
629+
if versionSpecificCandidate < .v4 {
630+
versionSpecificManifestToolsVersion = .v3
631+
}
632+
else {
633+
versionSpecificManifestToolsVersion = try ToolsVersionParser.parse(manifestPath: versionSpecificManifest, fileSystem: fileSystem)
634+
}
634635

635-
// Compare the tools version of this manifest with the regular
636-
// manifest and use the version-specific manifest if it has
637-
// a greater tools version.
638-
if versionSpecificManifestToolsVersion > regularManifestToolsVersion {
636+
// Compare the tools version of this manifest with the regular
637+
// manifest and use the version-specific manifest if it has
638+
// a greater tools version.
639+
if versionSpecificManifestToolsVersion > regularManifestToolsVersion {
640+
return versionSpecificManifest
641+
} else {
642+
// If there's no primary candidate, validate the regular manifest.
643+
if regularManifestToolsVersion.validateToolsVersion(currentToolsVersion) {
644+
return regularManifest
645+
} else {
646+
// If that's incompatible, use the closest version-specific manifest we got.
639647
return versionSpecificManifest
640648
}
641649
}
642-
643-
return regularManifest
644650
}
645651
}

Sources/PackageModel/ToolsVersion.swift

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,30 +105,53 @@ public struct ToolsVersion: Equatable, Hashable, Codable, Sendable {
105105
_version = version
106106
}
107107

108+
private enum ValidationResult {
109+
case valid
110+
case unsupportedToolsVersion
111+
case requireNewerTools
112+
}
113+
114+
private func _validateToolsVersion(_ currentToolsVersion: ToolsVersion) -> ValidationResult {
115+
// We don't want to throw any error when using the special vNext version.
116+
if SwiftVersion.current.isDevelopment && self == .vNext {
117+
return .valid
118+
}
119+
120+
// Make sure the package has the right minimum tools version.
121+
guard self >= .minimumRequired else {
122+
return .unsupportedToolsVersion
123+
}
124+
125+
// Make sure the package isn't newer than the current tools version.
126+
guard currentToolsVersion >= self else {
127+
return .requireNewerTools
128+
}
129+
130+
return .valid
131+
}
132+
108133
/// Returns true if the tools version is valid and can be used by this
109134
/// version of the package manager.
135+
public func validateToolsVersion(_ currentToolsVersion: ToolsVersion) -> Bool {
136+
return self._validateToolsVersion(currentToolsVersion) == .valid
137+
}
138+
110139
public func validateToolsVersion(
111140
_ currentToolsVersion: ToolsVersion,
112141
packageIdentity: PackageIdentity,
113142
packageVersion: String? = .none
114143
) throws {
115-
// We don't want to throw any error when using the special vNext version.
116-
if SwiftVersion.current.isDevelopment && self == .vNext {
117-
return
118-
}
119-
120-
// Make sure the package has the right minimum tools version.
121-
guard self >= .minimumRequired else {
144+
switch self._validateToolsVersion(currentToolsVersion) {
145+
case .valid:
146+
break
147+
case .unsupportedToolsVersion:
122148
throw UnsupportedToolsVersion(
123149
packageIdentity: packageIdentity,
124150
packageVersion: packageVersion,
125151
currentToolsVersion: currentToolsVersion,
126152
packageToolsVersion: self
127153
)
128-
}
129-
130-
// Make sure the package isn't newer than the current tools version.
131-
guard currentToolsVersion >= self else {
154+
case .requireNewerTools:
132155
throw RequireNewerTools(
133156
packageIdentity: packageIdentity,
134157
packageVersion: packageVersion,

Tests/PackageLoadingTests/ToolsVersionParserTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,4 +764,19 @@ class ToolsVersionParserTests: XCTestCase {
764764
}
765765
}
766766

767+
func testVersionSpecificManifestMostCompatibleIfLower() throws {
768+
let fs = InMemoryFileSystem(emptyFiles:
769+
"/pkg/foo"
770+
)
771+
let root = AbsolutePath("/pkg")
772+
773+
try fs.writeFileContents(root.appending("Package.swift"), string: "// swift-tools-version:6.0.0\n")
774+
try fs.writeFileContents(root.appending("[email protected]"), string: "// swift-tools-version:5.0.0\n")
775+
776+
let currentToolsVersion = ToolsVersion(version: "5.5.0")
777+
let manifestPath = try ManifestLoader.findManifest(packagePath: root, fileSystem: fs, currentToolsVersion: currentToolsVersion)
778+
let version = try ToolsVersionParser.parse(manifestPath: manifestPath, fileSystem: fs)
779+
try version.validateToolsVersion(currentToolsVersion, packageIdentity: .plain("lunch"))
780+
XCTAssertEqual(version.description, "5.0.0")
781+
}
767782
}

0 commit comments

Comments
 (0)