forked from swiftlang/swift-package-manager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathToolsVersionLoader.swift
184 lines (159 loc) · 8.89 KB
/
ToolsVersionLoader.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/*
This source file is part of the Swift.org open source project
Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See http://swift.org/LICENSE.txt for license information
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
import TSCBasic
import PackageModel
import TSCUtility
import Foundation
/// Protocol for the manifest loader interface.
public protocol ToolsVersionLoaderProtocol {
/// Load the tools version at the give package path.
///
/// - Parameters:
/// - path: The path to the package.
/// - fileSystem: The file system to use to read the file which contains tools version.
/// - Returns: The tools version.
/// - Throws: ToolsVersion.Error
func load(at path: AbsolutePath, fileSystem: FileSystem) throws -> ToolsVersion
}
extension Manifest {
/// Returns the manifest at the given package path.
///
/// Version specific manifest is chosen if present, otherwise path to regular
/// manfiest is returned.
public static func path(
atPackagePath packagePath: AbsolutePath,
currentToolsVersion: ToolsVersion = .currentToolsVersion,
fileSystem: FileSystem
) throws -> AbsolutePath {
// Look for a version-specific manifest.
for versionSpecificKey in Versioning.currentVersionSpecificKeys {
let versionSpecificPath = packagePath.appending(component: Manifest.basename + versionSpecificKey + ".swift")
if fileSystem.isFile(versionSpecificPath) {
return versionSpecificPath
}
}
// Otherwise, check if there is a version-specific manifest that has
// a higher tools version than the main Package.swift file.
let contents: [String]
do { contents = try fileSystem.getDirectoryContents(packagePath) } catch {
throw ToolsVersionLoader.Error.inaccessiblePackage(path: packagePath, reason: String(describing: error))
}
let regex = try! RegEx(pattern: "^Package@swift-(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?.swift$")
// Collect all version-specific manifests at the given package path.
let versionSpecificManifests = Dictionary(contents.compactMap{ file -> (ToolsVersion, String)? in
let parsedVersion = regex.matchGroups(in: file)
guard parsedVersion.count == 1, parsedVersion[0].count == 3 else {
return nil
}
let major = Int(parsedVersion[0][0])!
let minor = parsedVersion[0][1].isEmpty ? 0 : Int(parsedVersion[0][1])!
let patch = parsedVersion[0][2].isEmpty ? 0 : Int(parsedVersion[0][2])!
return (ToolsVersion(version: Version(major, minor, patch)), file)
}, uniquingKeysWith: { $1 })
let regularManifest = packagePath.appending(component: filename)
let toolsVersionLoader = ToolsVersionLoader(currentToolsVersion: currentToolsVersion)
// Find the version-specific manifest that statisfies the current tools version.
if let versionSpecificCandidate = versionSpecificManifests.keys.sorted(by: >).first(where: { $0 <= currentToolsVersion }) {
let versionSpecificManifest = packagePath.appending(component: versionSpecificManifests[versionSpecificCandidate]!)
// Compare the tools version of this manifest with the regular
// manifest and use the version-specific manifest if it has
// a greater tools version.
let versionSpecificManifestToolsVersion = try toolsVersionLoader.load(file: versionSpecificManifest, fileSystem: fileSystem)
let regularManifestToolsVersion = try toolsVersionLoader.load(file: regularManifest, fileSystem: fileSystem)
if versionSpecificManifestToolsVersion > regularManifestToolsVersion {
return versionSpecificManifest
}
}
return regularManifest
}
}
public class ToolsVersionLoader: ToolsVersionLoaderProtocol {
let currentToolsVersion: ToolsVersion
public init(currentToolsVersion: ToolsVersion = .currentToolsVersion) {
self.currentToolsVersion = currentToolsVersion
}
public enum Error: Swift.Error, CustomStringConvertible {
/// Package directory is inaccessible (missing, unreadable, etc).
case inaccessiblePackage(path: AbsolutePath, reason: String)
/// Package manifest file is inaccessible (missing, unreadable, etc).
case inaccessibleManifest(path: AbsolutePath, reason: String)
/// Malformed tools version specifier
case malformedToolsVersion(specifier: String, currentToolsVersion: ToolsVersion)
public var description: String {
switch self {
case .inaccessiblePackage(let packageDir, let reason):
return "the package at '\(packageDir)' cannot be accessed (\(reason))"
case .inaccessibleManifest(let manifestFile, let reason):
return "the package manifest at '\(manifestFile)' cannot be accessed (\(reason))"
case .malformedToolsVersion(let versionSpecifier, let currentToolsVersion):
return "the tools version '\(versionSpecifier)' is not valid; consider using '// swift-tools-version:\(currentToolsVersion.major).\(currentToolsVersion.minor)' to specify the current tools version"
}
}
}
public func load(at path: AbsolutePath, fileSystem: FileSystem) throws -> ToolsVersion {
// The file which contains the tools version.
let file = try Manifest.path(atPackagePath: path, currentToolsVersion: currentToolsVersion, fileSystem: fileSystem)
guard fileSystem.isFile(file) else {
// FIXME: We should return an error from here but Workspace tests rely on this in order to work.
// This doesn't really cause issues (yet) in practice though.
return ToolsVersion.currentToolsVersion
}
return try load(file: file, fileSystem: fileSystem)
}
fileprivate func load(file: AbsolutePath, fileSystem: FileSystem) throws -> ToolsVersion {
// FIXME: We don't need the entire file, just the first line.
let contents: ByteString
do { contents = try fileSystem.readFileContents(file) } catch {
throw Error.inaccessibleManifest(path: file, reason: String(describing: error))
}
// Get the version specifier string from tools version file.
guard let versionSpecifier = ToolsVersionLoader.split(contents).versionSpecifier else {
// Try to diagnose if there is a misspelling of the swift-tools-version comment.
let splitted = contents.contents.split(
separator: UInt8(ascii: "\n"),
maxSplits: 1,
omittingEmptySubsequences: false)
let misspellings = ["swift-tool", "tool-version"]
if let firstLine = ByteString(splitted[0]).validDescription,
misspellings.first(where: firstLine.lowercased().contains) != nil {
throw Error.malformedToolsVersion(specifier: firstLine, currentToolsVersion: currentToolsVersion)
}
// Otherwise assume the default to be v3.
return .v3
}
// Ensure we can construct the version from the specifier.
guard let version = ToolsVersion(string: versionSpecifier) else {
throw Error.malformedToolsVersion(specifier: versionSpecifier, currentToolsVersion: currentToolsVersion)
}
return version
}
/// Splits the bytes to the version specifier (if present) and rest of the contents.
public static func split(_ bytes: ByteString) -> (versionSpecifier: String?, rest: [UInt8]) {
let splitted = bytes.contents.split(
separator: UInt8(ascii: "\n"),
maxSplits: 1,
omittingEmptySubsequences: false)
// Try to match our regex and see if a valid specifier line.
guard let firstLine = ByteString(splitted[0]).validDescription,
let match = ToolsVersionLoader.regex.firstMatch(
in: firstLine, options: [], range: NSRange(location: 0, length: firstLine.count)),
match.numberOfRanges >= 2 else {
return (nil, bytes.contents)
}
let versionSpecifier = NSString(string: firstLine).substring(with: match.range(at: 1))
// FIXME: We can probably optimize here and return array slice.
return (versionSpecifier, splitted.count == 1 ? [] : Array(splitted[1]))
}
// The regex to match swift tools version specification:
// * It should start with `//` followed by any amount of whitespace.
// * Following that it should contain the case insensitive string `swift-tools-version:`.
// * The text between the above string and `;` or string end becomes the tools version specifier.
static let regex = try! NSRegularExpression(
pattern: "^// swift-tools-version:(.*?)(?:;.*|$)",
options: [.caseInsensitive])
}