Skip to content

Commit 4fd4df4

Browse files
[ToolsVersion][Workspace] Abstract out the generation of Swift tools version specification (#3073)
* replace `.asJSON` with direct string interpolation As of this commit, `.asJSON` doesn't do anything but return the `description` of the variable, which is what string interpolation already does. * replace `writeToolsVersion(at:version:fs:)` with `prependToolsVersionSpecification(toDefaultManifestIn:specifying:fileSystem:)` The new function signature follows the Swift API design guidelines better. The actual functionality is delegated to a new function `prependToolsVersionSpecification(toManifestAt:specifying:fileSystem:)`, which prepends the Swift tools version specification to any given manifest file, not only the non-version-specific manifest (i.e. `Package.swift`). Additional test cases might be needed. * rename ToolsVersionWriter as ToolsVersionSpecificationPrepender The test file, class, and functions are also renamed accordingly The new name reflects the functions more accurately * add test cases for new function `prependToolsVersionSpecification(toManifestAt:specifying:fileSystem:)` Some quality-of-life refactors for existing test cases are thrown in as well. * abstract out the generation of Swift tools version specification, a common component between `ManifestSourceGeneration.swift` and `ToolsVersionSpecificationPrepender.swift` * remove implementation and tests for writing/replacing Swift tools version specification to version-specific manifests * replace "prepend" with "rewrite", with appropriate casings, and remove some remaining references to the now removed function * rename "resolution" to "rounded to least significant version" * throw a ManifestAccessError instead of crash when the manifest is inaccessible
1 parent 62c3458 commit 4fd4df4

14 files changed

+455
-212
lines changed

Sources/Commands/SwiftPackageTool.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -672,14 +672,14 @@ extension SwiftPackageTool {
672672
// FIXME: Probably lift this error defination to ToolsVersion.
673673
throw ToolsVersionLoader.Error.malformedToolsVersionSpecification(.versionSpecifier(.isMisspelt(value)))
674674
}
675-
try writeToolsVersion(at: pkg, version: toolsVersion, fs: localFileSystem)
675+
try rewriteToolsVersionSpecification(toDefaultManifestIn: pkg, specifying: toolsVersion, fileSystem: localFileSystem)
676676

677677
case .setCurrent:
678678
// Write the tools version with current version but with patch set to zero.
679679
// We do this to avoid adding unnecessary constraints to patch versions, if
680680
// the package really needs it, they can do it using --set option.
681-
try writeToolsVersion(
682-
at: pkg, version: ToolsVersion.currentToolsVersion.zeroedPatch, fs: localFileSystem)
681+
try rewriteToolsVersionSpecification(
682+
toDefaultManifestIn: pkg, specifying: ToolsVersion.currentToolsVersion.zeroedPatch, fileSystem: localFileSystem)
683683
}
684684
}
685685
}

Sources/PackageModel/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ add_library(PackageModel
3131
SupportedLanguageExtension.swift
3232
SwiftLanguageVersion.swift
3333
Target.swift
34-
ToolsVersion.swift)
34+
ToolsVersion.swift
35+
ToolsVersionSpecificationGeneration.swift)
3536
target_link_libraries(PackageModel PUBLIC
3637
TSCBasic
3738
TSCUtility

Sources/PackageModel/ManifestSourceGeneration.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension Manifest {
2727
/// tools version, since the patch version doesn't change semantics.
2828
/// We leave out the spacer if the tools version doesn't support it.
2929
return """
30-
// swift-tools-version:\(toolsVersion < .v5_4 ? "" : " ")\(toolsVersion.major).\(toolsVersion.minor)
30+
\(toolsVersion.specification(roundedTo: .minor))
3131
import PackageDescription
3232
3333
let package = \(SourceCodeFragment(from: self).generateSourceCode())
@@ -131,14 +131,14 @@ fileprivate extension SourceCodeFragment {
131131
case .scm(let data):
132132
params.append(SourceCodeFragment(key: "url", string: data.location))
133133
switch data.requirement {
134-
case .exact(let version):
134+
case .exact(let version):
135135
params.append(SourceCodeFragment(enum: "exact", string: "\(version)"))
136-
case .range(let range):
136+
case .range(let range):
137137
params.append(SourceCodeFragment("\"\(range.lowerBound)\"..<\"\(range.upperBound)\""))
138-
case .revision(let revision):
139-
params.append(SourceCodeFragment(enum: "revision", string: revision))
140-
case .branch(let branch):
141-
params.append(SourceCodeFragment(enum: "branch", string: branch))
138+
case .revision(let revision):
139+
params.append(SourceCodeFragment(enum: "revision", string: revision))
140+
case .branch(let branch):
141+
params.append(SourceCodeFragment(enum: "branch", string: branch))
142142
}
143143
}
144144
self.init(enum: "package", subnodes: params)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// PackageModel/ToolsVersionSpecificationGeneration.swift
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
// -----------------------------------------------------------------------------
12+
///
13+
/// This file contains an extension to `ToolsVersion` that implements the generation of a Swift tools version specification from a `ToolsVersion` instance.
14+
///
15+
// -----------------------------------------------------------------------------
16+
17+
extension ToolsVersion {
18+
// TODO: Add options for whitespace styles.
19+
/// Returns a Swift tools version specification specifying the version to the given precision.
20+
/// - Parameter leastSignificantVersion: The precision to which the version specifier follows the version.
21+
/// - Returns: A Swift tools version specification specifying the version to the given precision.
22+
public func specification(roundedTo leastSignificantVersion: LeastSignificantVersion = .automatic) -> String {
23+
var versionSpecifier = "\(major).\(minor)"
24+
switch leastSignificantVersion {
25+
case .automatic:
26+
// If the patch version is not zero, then it's included in the Swift tools version specification.
27+
if patch != 0 { fallthrough }
28+
case .patch:
29+
versionSpecifier = "\(versionSpecifier).\(patch)"
30+
case .minor:
31+
break
32+
}
33+
return "// swift-tools-version:\(self < .v5_4 ? "" : " ")\(versionSpecifier)"
34+
}
35+
36+
/// The least significant version to round to.
37+
public enum LeastSignificantVersion {
38+
/// The patch version is the least significant if and only if it's not zero. Otherwise, the minor version is the least significant.
39+
case automatic
40+
/// The minor version is the least significant.
41+
case minor
42+
/// The patch version is the least significant.
43+
case patch
44+
// Although `ToolsVersion` uses `Version` as its backing store, it discards all pre-release and build metadata.
45+
// The versioning information ends at the patch version.
46+
}
47+
}

Sources/SPMTestSupport/MockWorkspace.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -114,7 +114,7 @@ public final class MockWorkspace {
114114
let toolsVersion = package.toolsVersion ?? .currentToolsVersion
115115
let repoManifestPath = AbsolutePath.root.appending(component: Manifest.filename)
116116
try repo.writeFileContents(repoManifestPath, bytes: "")
117-
try writeToolsVersion(at: .root, version: toolsVersion, fs: repo)
117+
try rewriteToolsVersionSpecification(toDefaultManifestIn: .root, specifying: toolsVersion, fileSystem: repo)
118118
try repo.commit()
119119

120120
let versions: [String?] = packageKind == .remote ? package.versions : [nil]

Sources/Workspace/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ add_library(Workspace
1616
ManagedDependency.swift
1717
ResolvedFileWatcher.swift
1818
ResolverPrecomputationProvider.swift
19-
ToolsVersionWriter.swift
19+
ToolsVersionSpecificationRewriter.swift
2020
UserToolchain.swift
2121
Workspace.swift
2222
WorkspaceConfiguration.swift

Sources/Workspace/InitPackage.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See http://swift.org/LICENSE.txt for license information
@@ -226,8 +226,8 @@ public final class InitPackage {
226226
let version = InitPackage.newPackageToolsVersion.zeroedPatch
227227

228228
// Write the current tools version.
229-
try writeToolsVersion(
230-
at: manifest.parentDirectory, version: version, fs: localFileSystem)
229+
try rewriteToolsVersionSpecification(
230+
toDefaultManifestIn: manifest.parentDirectory, specifying: version, fileSystem: localFileSystem)
231231
}
232232

233233
private func writeREADMEFile() throws {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Workspace/ToolsVersionSpecificationRewriter.swift - Prepends/replaces Swift tools version specifications in manifest files.
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
// -----------------------------------------------------------------------------
12+
///
13+
/// This file implements a global function that rewrite the Swift tools version specification of a manifest file.
14+
///
15+
// -----------------------------------------------------------------------------
16+
17+
import TSCBasic
18+
import PackageModel
19+
import PackageLoading
20+
import TSCUtility
21+
22+
/// An error that causes the access to a manifest to fails.
23+
public struct ManifestAccessError: Error, CustomStringConvertible {
24+
public init(_ kind: Kind, at path: AbsolutePath) {
25+
self.kind = kind
26+
self.path = path
27+
}
28+
29+
/// The kind of the error being raised.
30+
public enum Kind: Equatable {
31+
/// A component of a specified pathname did not exist, or the pathname was an empty string.
32+
///
33+
/// This error is equivalent to `TSCBasic.FileSystemError.Kind.noEntry` and corresponds to the POSIX ENOENT error code, but is specialised for manifest access.
34+
case noSuchFileOrDirectory
35+
/// The path points to a directory.
36+
///
37+
/// This error is equivalent to `TSCBasic.FileSystemError.Kind.isDirectory` and corresponds to rhe POSIX EISDIR error code, but is specialised for manifest access.
38+
case isADirectory
39+
/// The manifest cannot be accessed for an unknown reason.
40+
case unknown
41+
}
42+
43+
/// The kind of the error being raised.
44+
public let kind: Kind
45+
46+
/// The absolute path where the error occurred.
47+
public let path: AbsolutePath
48+
49+
public var description: String {
50+
var reason: String {
51+
switch kind {
52+
case .noSuchFileOrDirectory:
53+
return "a component of the path does not exist, or the path is an empty string"
54+
case .isADirectory:
55+
return "the path is a directory; a file is expected"
56+
case .unknown:
57+
return "an unknown error occurred"
58+
}
59+
}
60+
return "no accessible Swift Package Manager manifest file found at '\(path)'; \(reason)"
61+
}
62+
}
63+
64+
/// Rewrites Swift tools version specification to the non-version-specific manifest file (`Package.swift`) in the given directory.
65+
///
66+
/// If the main manifest file already contains a valid tools version specification (ignoring the validity of the version specifier and that of everything following it), then the existing specification is replaced by this new one.
67+
///
68+
/// The version specifier in the specification does not contain any build metadata or pre-release identifier. The patch version is included if and only if it's not zero.
69+
///
70+
/// A `FileSystemError` is thrown if the manifest file is unable to be read from or written to.
71+
///
72+
/// - Precondition: `manifestDirectoryPath` must be a valid path to a directory that contains a `Package.swift` file.
73+
///
74+
/// - Parameters:
75+
/// - manifestDirectoryPath: The absolute path to the given directory.
76+
/// - toolsVersion: The Swift tools version to specify as the lowest supported version.
77+
/// - fileSystem: The filesystem to read/write the manifest file on.
78+
public func rewriteToolsVersionSpecification(toDefaultManifestIn manifestDirectoryPath: AbsolutePath, specifying toolsVersion: ToolsVersion, fileSystem: FileSystem) throws {
79+
let manifestFilePath = manifestDirectoryPath.appending(component: Manifest.filename)
80+
81+
guard fileSystem.isFile(manifestFilePath) else {
82+
guard fileSystem.exists(manifestFilePath) else {
83+
throw ManifestAccessError(.noSuchFileOrDirectory, at: manifestFilePath)
84+
}
85+
guard !fileSystem.isDirectory(manifestFilePath) else {
86+
throw ManifestAccessError(.isADirectory, at: manifestFilePath)
87+
}
88+
throw ManifestAccessError(.unknown, at: manifestFilePath)
89+
}
90+
91+
/// The current contents of the file.
92+
let contents = try fileSystem.readFileContents(manifestFilePath)
93+
94+
let stream = BufferedOutputByteStream()
95+
// Write out the tools version specification, including the patch version if and only if it's not zero.
96+
stream <<< toolsVersion.specification(roundedTo: .automatic) <<< "\n"
97+
98+
// The following lines up to line 77 append the file contents except for the Swift tools version specification line.
99+
100+
guard let contentsDecodedWithUTF8 = contents.validDescription else {
101+
throw ToolsVersionLoader.Error.nonUTF8EncodedManifest(path: manifestFilePath)
102+
}
103+
104+
let manifestComponents = ToolsVersionLoader.split(contentsDecodedWithUTF8)
105+
106+
let toolsVersionSpecificationComponents = manifestComponents.toolsVersionSpecificationComponents
107+
108+
// Replace the Swift tools version specification line if and only if it's well-formed up to the version specifier.
109+
// This matches the behaviour of the old (now removed) [`ToolsVersionLoader.split(:_)`](https://github.com/WowbaggersLiquidLunch/swift-package-manager/blob/49cfc46bc5defd3ce8e0c0261e3e2cb475bcdb91/Sources/PackageLoading/ToolsVersionLoader.swift#L160).
110+
if toolsVersionSpecificationComponents.everythingUpToVersionSpecifierIsWellFormed {
111+
stream <<< ByteString(encodingAsUTF8: String(manifestComponents.contentsAfterToolsVersionSpecification))
112+
} else {
113+
stream <<< contents
114+
}
115+
116+
try fileSystem.writeFileContents(manifestFilePath, bytes: stream.bytes)
117+
}

Sources/Workspace/ToolsVersionWriter.swift

Lines changed: 0 additions & 56 deletions
This file was deleted.

Tests/CommandsTests/PackageToolTests.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,9 @@ final class PackageToolTests: XCTestCase {
528528

529529
let manifest = path.appending(component: "Package.swift")
530530
let contents = try localFileSystem.readFileContents(manifest).description
531-
let version = "\(InitPackage.newPackageToolsVersion.major).\(InitPackage.newPackageToolsVersion.minor)"
532-
XCTAssertTrue(contents.hasPrefix("// swift-tools-version:\(version)\n"))
531+
let version = InitPackage.newPackageToolsVersion
532+
let versionSpecifier = "\(version.major).\(version.minor)"
533+
XCTAssertTrue(contents.hasPrefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n"))
533534

534535
XCTAssertTrue(fs.exists(manifest))
535536
XCTAssertEqual(try fs.getDirectoryContents(path.appending(component: "Sources").appending(component: "Foo")), ["main.swift"])
@@ -563,8 +564,9 @@ final class PackageToolTests: XCTestCase {
563564

564565
let manifest = path.appending(component: "Package.swift")
565566
let contents = try localFileSystem.readFileContents(manifest).description
566-
let version = "\(InitPackage.newPackageToolsVersion.major).\(InitPackage.newPackageToolsVersion.minor)"
567-
XCTAssertTrue(contents.hasPrefix("// swift-tools-version:\(version)\n"))
567+
let version = InitPackage.newPackageToolsVersion
568+
let versionSpecifier = "\(version.major).\(version.minor)"
569+
XCTAssertTrue(contents.hasPrefix("// swift-tools-version:\(version < .v5_4 ? "" : " ")\(versionSpecifier)\n"))
568570

569571
XCTAssertTrue(fs.exists(manifest))
570572
XCTAssertEqual(try fs.getDirectoryContents(path.appending(component: "Sources").appending(component: "CustomName")), ["main.swift"])

0 commit comments

Comments
 (0)