Skip to content

Commit 9b1f8a7

Browse files
committed
2 parents 0aceda4 + f865129 commit 9b1f8a7

File tree

15 files changed

+378
-27
lines changed

15 files changed

+378
-27
lines changed

Documentation/Usage.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* [Build an Executable](#build-an-executable)
1212
* [Create a Package](#create-a-package)
1313
* [Distribute a Package](#distribute-a-package)
14+
* [Handling version-specific logic](#version-specific-logic)
1415
* [Reference](Reference.md)
1516
* [Resources](Resources.md)
1617

@@ -323,3 +324,76 @@ import Foundation
323324
## Distribute a Package
324325

325326
*Content to come.*
327+
328+
## Handling version-specific logic
329+
330+
The package manager is designed to support packages which work with a variety of
331+
Swift project versions, including both the language and the package manager version.
332+
333+
In most cases, if you want to support multiple Swift versions in a package you
334+
should do so by using the language-specific version checks available in the
335+
source code itself. However, in some circumstances this may become
336+
unmanageable; in particular, when the package manifest itself cannot be written
337+
to be Swift version agnostic (for example, because it optionally adopts new
338+
package manager features not present in older versions).
339+
340+
The package manager has support for a mechanism to allow Swift version-specific
341+
customizations for the both package manifest and the package versions which will
342+
be considered.
343+
344+
### Version-specific tag selection
345+
346+
The tags which define the versions of the package available for clients to use
347+
can _optionally_ be suffixed with a marker in the form of `@swift-3`. When the
348+
package manager is determining the available tags for a repository, _if_ a
349+
version-specific marker is available which matches the current tool version,
350+
then it will *only* consider the versions which have the version-specific
351+
marker. Conversely, version-specific tags will be ignored by any non-matching
352+
tool version.
353+
354+
For example, suppose the package `Foo` has the tags
355+
`[1.0.0, 1.2.0@swift-3, 1.3.0]`. If version 3.0 of the package manager is
356+
evaluating the available versions for this repository, it will only ever
357+
consider version `1.2.0`. However, version 4.0 would consider only `1.0.0` and
358+
`1.3.0`.
359+
360+
This feature is intended for use in the following scenarios:
361+
362+
1. A package wishes to maintain support for Swift 3.0 in older versions, but
363+
newer versions of the package require Swift 4.0 for the manifest to be
364+
readable. Since Swift 3.0 will not know to ignore those versions, it would
365+
fail when performing dependency resolution on the package if no action is
366+
taken. In this case, the author can re-tag the last versions which supported
367+
Swift 3.0 appropriately.
368+
369+
2. A package wishes to maintain dual support for Swift 3.0 and Swift 4.0 at the
370+
same version numbers, but this requires substantial differences in the
371+
code. In this case, the author can maintain parallel tag sets for both
372+
versions.
373+
374+
It is *not* expected the packages would ever use this feature unless absolutely
375+
necessary to support existing clients. In particular, packages *should not*
376+
adopt this syntax for tagging versions supporting the _latest GM_ Swift version.
377+
378+
The package manager supports looking for any of the following marked tags, in
379+
order of preference:
380+
381+
1. `MAJOR.MINOR.PATCH` (e.g., `[email protected]`)
382+
2. `MAJOR.MINOR` (e.g., `[email protected]`)
383+
3. `MAJOR` (e.g., `1.2.0@swift-3`)
384+
385+
### Version-specific manifest selection
386+
387+
The package manager will additionally look for a version-specific marked
388+
manifest version when loading the particular version of a package, by searching
389+
for a manifest in the form of `[email protected]`. The set of markers looked
390+
for is the same as for version-specific tag selection.
391+
392+
This feature is intended for use in cases where a package wishes to maintain
393+
compatibility with multiple Swift project versions, but requires a substantively
394+
different manifest file for this to be viable (e.g., due to changes in the
395+
manifest API).
396+
397+
It is *not* expected the packages would ever use this feature unless absolutely
398+
necessary to support existing clients. In particular, packages *should not*
399+
adopt this syntax for tagging versions supporting the _latest GM_ Swift version.

Sources/Get/RawClone.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ class RawClone: Fetchable {
3232
if branch.hasPrefix("v") {
3333
branch = String(branch.characters.dropFirst())
3434
}
35+
if branch.contains("@") {
36+
branch = branch.components(separatedBy: "@").first!
37+
}
3538
return Version(branch)
3639
}
3740

Sources/PackageLoading/ManifestLoader.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ public final class ManifestLoader {
5858
/// - version: The version the manifest is from, if known.
5959
/// - fileSystem: If given, the file system to load from (otherwise load from the local file system).
6060
public func load(packagePath: AbsolutePath, baseURL: String, version: Version?, fileSystem: FileSystem? = nil) throws -> Manifest {
61+
// As per our versioning support, determine the appropriate manifest version to load.
62+
for versionSpecificKey in Versioning.currentVersionSpecificKeys {
63+
let versionSpecificPath = packagePath.appending(component: Manifest.basename + versionSpecificKey + ".swift")
64+
if (fileSystem ?? localFileSystem).exists(versionSpecificPath) {
65+
return try loadFile(path: versionSpecificPath, baseURL: baseURL, version: version, fileSystem: fileSystem)
66+
}
67+
}
68+
6169
return try loadFile(path: packagePath.appending(component: Manifest.filename), baseURL: baseURL, version: version, fileSystem: fileSystem)
6270
}
6371

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,33 @@ public struct PackageBuilder {
222222
// MARK: Utility Predicates
223223

224224
private func isValidSource(_ path: AbsolutePath) -> Bool {
225-
return isValidSource(path, validExtensions: SupportedLanguageExtension.validExtensions)
226-
}
227-
228-
private func isValidSource(_ path: AbsolutePath, validExtensions: Set<String>) -> Bool {
229-
if path.basename.hasPrefix(".") { return false }
230-
if path == manifest.path { return false }
231-
if excludedPaths.contains(path) { return false }
225+
// Ignore files which don't match the expected extensions.
226+
guard let ext = path.extension, SupportedLanguageExtension.validExtensions.contains(ext) else {
227+
return false
228+
}
229+
230+
// Ignore dotfiles.
231+
let basename = path.basename
232+
if basename.hasPrefix(".") { return false }
233+
234+
// Ignore symlinks to non-files.
232235
if !fileSystem.isFile(path) { return false }
233-
guard let ext = path.extension else { return false }
234-
return validExtensions.contains(ext)
236+
237+
// Ignore excluded files.
238+
if excludedPaths.contains(path) { return false }
239+
240+
// Ignore manifest files.
241+
if path.parentDirectory == packagePath {
242+
if basename == Manifest.filename { return false }
243+
244+
// Ignore version-specific manifest files.
245+
if basename.hasPrefix(Manifest.basename + "@") && basename.hasSuffix(".swift") {
246+
return false
247+
}
248+
}
249+
250+
// Otherwise, we have a valid source file.
251+
return true
235252
}
236253

237254
private func shouldConsiderDirectory(_ path: AbsolutePath) -> Bool {
@@ -414,7 +431,7 @@ public struct PackageBuilder {
414431
}
415432
}
416433

417-
/// Private function that constructs a single Module object for the moduel at `path`, having the name `name`. If `isTest` is true, the module is constructed as a test module; if false, it is a regular module.
434+
/// Private function that constructs a single Module object for the module at `path`, having the name `name`. If `isTest` is true, the module is constructed as a test module; if false, it is a regular module.
418435
private func createModule(_ path: AbsolutePath, name: String, isTest: Bool) throws -> Module {
419436

420437
// Validate the module name. This function will throw an error if it detects a problem.
@@ -424,9 +441,11 @@ public struct PackageBuilder {
424441
let walked = try walk(path, fileSystem: fileSystem, recursing: shouldConsiderDirectory).map{ $0 }
425442

426443
// Select any source files for the C-based languages and for Swift.
427-
let cSources = walked.filter{ isValidSource($0, validExtensions: SupportedLanguageExtension.cFamilyExtensions) }
428-
let swiftSources = walked.filter{ isValidSource($0, validExtensions: SupportedLanguageExtension.swiftExtensions) }
429-
444+
let sources = walked.filter(isValidSource)
445+
let cSources = sources.filter{ SupportedLanguageExtension.cFamilyExtensions.contains($0.extension!) }
446+
let swiftSources = sources.filter{ SupportedLanguageExtension.swiftExtensions.contains($0.extension!) }
447+
assert(sources.count == cSources.count + swiftSources.count)
448+
430449
// Create and return the right kind of module depending on what kind of sources we found.
431450
if cSources.isEmpty {
432451
// No C sources, so we expect to have Swift sources, and we create a Swift module.

Sources/PackageModel/Manifest.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import PackageDescription
2020
*/
2121
public struct Manifest {
2222
/// The standard filename for the manifest.
23-
public static var filename = "Package.swift"
23+
public static var filename = basename + ".swift"
24+
25+
/// The standard basename for the manifest.
26+
public static var basename = "Package"
2427

2528
/// The path of the manifest file.
2629
//

Sources/TestSupport/misc.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ public func fixture(name: String, tags: [String] = [], file: StaticString = #fil
8484

8585
/// Test-helper function that creates a new Git repository in a directory. The new repository will contain exactly one empty file, and if a tag name is provided, a tag with that name will be created.
8686
public func initGitRepo(_ dir: AbsolutePath, tag: String? = nil, file: StaticString = #file, line: UInt = #line) {
87+
initGitRepo(dir, tags: tag.flatMap{ [$0] } ?? [], file: file, line: line)
88+
}
89+
90+
public func initGitRepo(_ dir: AbsolutePath, tags: [String], file: StaticString = #file, line: UInt = #line) {
8791
do {
8892
let file = dir.appending(component: "file.swift")
8993
try systemQuietly(["touch", file.asString])
@@ -92,7 +96,7 @@ public func initGitRepo(_ dir: AbsolutePath, tag: String? = nil, file: StaticStr
9296
try systemQuietly([Git.tool, "-C", dir.asString, "config", "user.name", "Example Example"])
9397
try systemQuietly([Git.tool, "-C", dir.asString, "add", "."])
9498
try systemQuietly([Git.tool, "-C", dir.asString, "commit", "-m", "msg"])
95-
if let tag = tag {
99+
for tag in tags {
96100
try tagGitRepo(dir, tag: tag)
97101
}
98102
}
@@ -105,6 +109,18 @@ public func tagGitRepo(_ dir: AbsolutePath, tag: String) throws {
105109
try systemQuietly([Git.tool, "-C", dir.asString, "tag", tag])
106110
}
107111

112+
public func removeTagGitRepo(_ dir: AbsolutePath, tag: String) throws {
113+
try systemQuietly([Git.tool, "-C", dir.asString, "tag", "-d", tag])
114+
}
115+
116+
public func addGitRepo(_ dir: AbsolutePath, file path: RelativePath) throws {
117+
try systemQuietly([Git.tool, "-C", dir.asString, "add", path.asString])
118+
}
119+
120+
public func commitGitRepo(_ dir: AbsolutePath, message: String = "Test commit") throws {
121+
try systemQuietly([Git.tool, "-C", dir.asString, "commit", "-m", message])
122+
}
123+
108124
public enum Configuration {
109125
case Debug
110126
case Release
@@ -154,3 +170,16 @@ public func systemQuietly(_ args: [String]) throws {
154170
public func systemQuietly(_ args: String...) throws {
155171
try systemQuietly(args)
156172
}
173+
174+
public extension FileSystem {
175+
/// Write to a file from a stream producer.
176+
//
177+
// FIXME: This is copy-paste from Commands/init.swift, maybe it is reasonable to lift it to Basic?
178+
mutating func writeFileContents(_ path: AbsolutePath, body: (OutputByteStream) -> ()) throws {
179+
let contents = BufferedOutputByteStream()
180+
body(contents)
181+
try createDirectory(path.parentDirectory, recursive: true)
182+
try writeFileContents(path, bytes: contents.bytes)
183+
}
184+
}
185+

Sources/Utility/Git.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,32 @@ public class Git {
5959
let out = (try? Git.runPopen([Git.tool, "-C", repo.path.asString, "tag", "-l"])) ?? ""
6060
let tags = out.characters.split(separator: "\n").map{ String($0) }
6161

62-
// First try the plain init.
62+
// First, check if we need to restrict the tag set to version-specific tags.
6363
var knownVersions: [Version: String] = [:]
64+
for versionSpecificKey in Versioning.currentVersionSpecificKeys {
65+
for tag in tags where tag.hasSuffix(versionSpecificKey) {
66+
let specifier = String(tag.characters.dropLast(versionSpecificKey.characters.count))
67+
if let version = Version(specifier) ?? Version.vprefix(specifier) {
68+
knownVersions[version] = tag
69+
}
70+
}
71+
72+
// If we found tags at this version-specific key, we are done.
73+
if !knownVersions.isEmpty {
74+
return knownVersions
75+
}
76+
}
77+
78+
// Otherwise, look for normal tags.
6479
for tag in tags {
6580
if let version = Version(tag) {
6681
knownVersions[version] = tag
6782
}
6883
}
84+
6985
// If we didn't find any versions, look for 'v'-prefixed ones.
86+
//
87+
// FIXME: We should match both styles simultaneously.
7088
if knownVersions.isEmpty {
7189
for tag in tags {
7290
if let version = Version.vprefix(tag) {

Sources/Utility/Versioning.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ public struct SwiftVersion {
2727
/// Build information, as an unstructured string.
2828
public var buildIdentifier: String?
2929

30+
/// The major component of the version number.
31+
public var major: Int { return version.major }
32+
/// The minor component of the version number.
33+
public var minor: Int { return version.minor }
34+
/// The patch component of the version number.
35+
public var patch: Int { return version.patch }
36+
3037
/// The version as a readable string.
3138
public var displayString: String {
32-
var result = "\(version.major).\(version.minor).\(version.patch)"
39+
var result = "\(major).\(minor).\(patch)"
3340
if isDevelopment {
3441
result += "-dev"
3542
}
@@ -47,6 +54,18 @@ public struct SwiftVersion {
4754
#endif
4855
return vendorPrefix + "Swift Package Manager - Swift " + displayString
4956
}
57+
58+
/// The list of version specific identifiers to search when attempting to
59+
/// load version specific package or version information, in order of
60+
/// preference.
61+
public var versionSpecificKeys: [String] {
62+
return [
63+
"@swift-\(major).\(minor).\(patch)",
64+
"@swift-\(major).\(minor)",
65+
"@swift-\(major)"
66+
]
67+
}
68+
5069
}
5170

5271
private func getBuildIdentifier() -> String? {
@@ -64,4 +83,8 @@ public struct Versioning {
6483
version: (3, 0, 0),
6584
isDevelopment: true,
6685
buildIdentifier: getBuildIdentifier())
86+
87+
/// The list of version specific "keys" to search when attempting to load
88+
/// version specific package or version information, in order of preference.
89+
public static let currentVersionSpecificKeys = currentVersion.versionSpecificKeys
6790
}

Tests/FunctionalTests/DependencyResolutionTests.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
*/
1010

1111
import XCTest
12-
import TestSupport
12+
1313
import Basic
14+
1415
import func POSIX.popen
1516

16-
class DependencyResolutionTestCase: XCTestCase {
17+
import TestSupport
18+
19+
class DependencyResolutionTests: XCTestCase {
1720
func testInternalSimple() {
1821
fixture(name: "DependencyResolution/Internal/Simple") { prefix in
1922
XCTAssertBuilds(prefix)

Tests/FunctionalTests/ValidLayoutTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Utility
1717
import func POSIX.rename
1818
import func POSIX.popen
1919

20-
class ValidLayoutsTestCase: XCTestCase {
20+
class ValidLayoutsTests: XCTestCase {
2121

2222
func testSingleModuleLibrary() {
2323
runLayoutFixture(name: "SingleModule/Library") { prefix in
@@ -113,7 +113,7 @@ class ValidLayoutsTestCase: XCTestCase {
113113

114114
// MARK: Utility
115115

116-
extension ValidLayoutsTestCase {
116+
extension ValidLayoutsTests {
117117
func runLayoutFixture(name: String, line: UInt = #line, body: (AbsolutePath) throws -> Void) {
118118
let name = "ValidLayouts/\(name)"
119119

0 commit comments

Comments
 (0)