Skip to content

Commit 44b00a1

Browse files
committed
Allow sources anywhere in ./Sources when only one target is present
After getting some feedback about the added `path` argument for executable packages generated with `swift package init` (#6144), this change allows a target's sources to occupy the entire "predefined sources directory" (e.g. `Sources`, `Tests`, or `Plugins`) when there is only one target of a specific kind occupying one of those directories in the package. All package types can benefit from this. For example, if there is only one library target and one test target, one can put sources directly in `./Sources` and `./Tests`. Adding another library means that one has to put the sources for TargetA in `./Sources/TargetA` and the sources for TargetB in `./Sources/TargetB`, and so on. When there is more than one target of a type in a package, the existing requirements for target sources still apply. This change should be compatible with existing layouts as well. If there is only a single target in a package, then sources can of course continue to exist exclusively in `./Sources/<target>`, or `./Tests/<testTarget>`, etc. Amend the `executable` and `tool` template's generated manifest to not include the `path` argument anymore as it is no longer needed. With this change, the `library` template type can also put its sources and tests directly in the `./Sources` and `./Tests` directories respectively. All of the above only takes place when the tools version is set to 5.9 or higher in the manifest. rdar://106829666
1 parent 57d829a commit 44b00a1

File tree

7 files changed

+703
-36
lines changed

7 files changed

+703
-36
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ Swift Next
2020
Swift 5.9
2121
-----------
2222

23+
* [#6294]
24+
25+
When a package contains a single target, sources may be distributed anywhere within the `./Sources` directory. If sources are placed in a subdirectory under `./Sources/<target>`, or there is more than one target, the existing expectation for sources apply.
26+
2327
* [#6114]
2428

2529
Added a new `allowNetworkConnections(scope:reason:)` for giving a command plugin permissions to access the network. Permissions can be scoped to Unix domain sockets in general or specifically for Docker, as well as local or remote IP connections which can be limited by port. For non-interactive use cases, there is also a `--allow-network-connections` commandline flag to allow network connections for a particular scope.

Sources/PackageLoading/Diagnostics.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ import PackageModel
1515
import TSCBasic
1616

1717
extension Basics.Diagnostic {
18-
static func targetHasNoSources(targetPath: String, target: String) -> Self {
19-
.warning("Source files for target \(target) should be located under \(targetPath)")
18+
static func targetHasNoSources(name: String, type: TargetDescription.TargetType, shouldSuggestRelaxedSourceDir: Bool) -> Self {
19+
let folderName = PackageBuilder.suggestedPredefinedSourceDirectory(type: type)
20+
var clauses = ["Source files for target \(name) should be located under '\(folderName)/\(name)'"]
21+
if shouldSuggestRelaxedSourceDir {
22+
clauses.append("'\(folderName)'")
23+
}
24+
clauses.append("or a custom sources path can be set with the 'path' property in Package.swift")
25+
return .warning(clauses.joined(separator: ", "))
2026
}
2127

2228
static func targetNameHasIncorrectCase(target: String) -> Self {

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public enum ModuleError: Swift.Error {
3030
case duplicateModule(String, [String])
3131

3232
/// The referenced target could not be found.
33-
case moduleNotFound(String, TargetDescription.TargetType)
33+
case moduleNotFound(String, TargetDescription.TargetType, shouldSuggestRelaxedSourceDir: Bool)
3434

3535
/// The artifact for the binary target could not be found.
3636
case artifactNotFound(targetName: String, expectedArtifactName: String)
@@ -87,9 +87,14 @@ extension ModuleError: CustomStringConvertible {
8787
case .duplicateModule(let name, let packages):
8888
let packages = packages.joined(separator: "', '")
8989
return "multiple targets named '\(name)' in: '\(packages)'; consider using the `moduleAliases` parameter in manifest to provide unique names"
90-
case .moduleNotFound(let target, let type):
90+
case .moduleNotFound(let target, let type, let shouldSuggestRelaxedSourceDir):
9191
let folderName = (type == .test) ? "Tests" : (type == .plugin) ? "Plugins" : "Sources"
92-
return "Source files for target \(target) should be located under '\(folderName)/\(target)', or a custom sources path can be set with the 'path' property in Package.swift"
92+
var clauses = ["Source files for target \(target) should be located under '\(folderName)/\(target)'"]
93+
if shouldSuggestRelaxedSourceDir {
94+
clauses.append("'\(folderName)'")
95+
}
96+
clauses.append("or a custom sources path can be set with the 'path' property in Package.swift")
97+
return clauses.joined(separator: ", ")
9398
case .artifactNotFound(let targetName, let expectedArtifactName):
9499
return "binary target '\(targetName)' could not be mapped to an artifact with expected name '\(expectedArtifactName)'"
95100
case .invalidModuleAlias(let originalName, let newName):
@@ -526,12 +531,23 @@ public final class PackageBuilder {
526531
return path
527532
}
528533

534+
let commonTargetsOfSimilarType = self.manifest.targetsWithCommonSourceRoot(type: target.type).count
535+
// If there is only one target defined, it may be allowed to occupy the
536+
// entire predefined target directory.
537+
if self.manifest.toolsVersion >= .v5_9 {
538+
if commonTargetsOfSimilarType == 1 {
539+
return predefinedDir.path
540+
}
541+
}
542+
529543
// Otherwise, if the path "exists" then the case in manifest differs from the case on the file system.
530544
if fileSystem.isDirectory(path) {
531545
self.observabilityScope.emit(.targetNameHasIncorrectCase(target: target.name))
532546
return path
533547
}
534-
throw ModuleError.moduleNotFound(target.name, target.type)
548+
throw ModuleError.moduleNotFound(target.name,
549+
target.type,
550+
shouldSuggestRelaxedSourceDir: self.manifest.shouldSuggestRelaxedSourceDir(type: target.type))
535551
}
536552

537553
// Create potential targets.
@@ -568,7 +584,10 @@ public final class PackageBuilder {
568584
let missingModuleNames = allVisibleModuleNames.subtracting(potentialModulesName)
569585
if let missingModuleName = missingModuleNames.first {
570586
let type = potentialModules.first(where: { $0.name == missingModuleName })?.type ?? .regular
571-
throw ModuleError.moduleNotFound(missingModuleName, type)
587+
throw ModuleError.moduleNotFound(missingModuleName,
588+
type,
589+
shouldSuggestRelaxedSourceDir: self.manifest.shouldSuggestRelaxedSourceDir(type: type)
590+
)
572591
}
573592

574593
let products = Dictionary(manifest.products.map({ ($0.name, $0) }), uniquingKeysWith: { $1 })
@@ -707,7 +726,9 @@ public final class PackageBuilder {
707726
targets[createdTarget.name] = createdTarget
708727
} else {
709728
emptyModules.insert(potentialModule.name)
710-
self.observabilityScope.emit(.targetHasNoSources(targetPath: potentialModule.path.pathString, target: potentialModule.name))
729+
self.observabilityScope.emit(.targetHasNoSources(name: potentialModule.name,
730+
type: potentialModule.type,
731+
shouldSuggestRelaxedSourceDir: manifest.shouldSuggestRelaxedSourceDir(type: potentialModule.type)))
711732
}
712733
}
713734

@@ -1391,6 +1412,18 @@ public final class PackageBuilder {
13911412
}
13921413
return true
13931414
}
1415+
1416+
/// Returns the first suggested predefined source directory for a given target type.
1417+
public static func suggestedPredefinedSourceDirectory(type: TargetDescription.TargetType) -> String {
1418+
switch type {
1419+
case .test:
1420+
return predefinedTestDirectories[0]
1421+
case .plugin:
1422+
return predefinedPluginDirectories[0]
1423+
default:
1424+
return predefinedSourceDirectories[0]
1425+
}
1426+
}
13941427
}
13951428

13961429
extension PackageBuilder {

Sources/PackageModel/Manifest/Manifest.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,26 @@ public final class Manifest: Sendable {
454454
return false
455455
}
456456
}
457+
458+
/// Returns a list of target descriptions whose root source directory is the same as that for the given type.
459+
public func targetsWithCommonSourceRoot(type: TargetDescription.TargetType) -> [TargetDescription] {
460+
switch type {
461+
case .test:
462+
return targets.filter { $0.type == .test }
463+
case .plugin:
464+
return targets.filter { $0.type == .plugin }
465+
default:
466+
return targets.filter { $0.type != .test && $0.type != .plugin }
467+
}
468+
}
469+
470+
/// Returns true if the tools version is >= 5.9 and the number of targets with a common source root is 1.
471+
public func shouldSuggestRelaxedSourceDir(type: TargetDescription.TargetType) -> Bool {
472+
guard toolsVersion >= .v5_9 else {
473+
return false
474+
}
475+
return targetsWithCommonSourceRoot(type: type).count == 1
476+
}
457477
}
458478

459479
extension Manifest: Hashable {

Sources/Workspace/InitPackage.swift

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,7 @@ public final class InitPackage {
257257
if packageType == .executable {
258258
param += """
259259
.executableTarget(
260-
name: "\(pkgname)",
261-
path: "Sources"),
260+
name: "\(pkgname)")
262261
]
263262
"""
264263
} else if packageType == .tool {
@@ -267,8 +266,7 @@ public final class InitPackage {
267266
name: "\(pkgname)",
268267
dependencies: [
269268
.product(name: "ArgumentParser", package: "swift-argument-parser"),
270-
],
271-
path: "Sources"),
269+
]),
272270
]
273271
"""
274272
} else if packageType == .macro {
@@ -364,9 +362,13 @@ public final class InitPackage {
364362
progressReporter?("Creating \(sources.relative(to: destinationPath))/")
365363
try makeDirectories(sources)
366364

367-
let moduleDir = packageType == .executable || packageType == .tool
368-
? sources
369-
: sources.appending("\(pkgname)")
365+
let moduleDir: AbsolutePath
366+
switch packageType {
367+
case .executable, .tool:
368+
moduleDir = sources
369+
default:
370+
moduleDir = sources.appending("\(pkgname)")
371+
}
370372
try makeDirectories(moduleDir)
371373

372374
let sourceFileName: String

Tests/PackageGraphTests/PackageGraphTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ class PackageGraphTests: XCTestCase {
516516

517517
testDiagnostics(observability.diagnostics) { result in
518518
result.check(
519-
diagnostic: "Source files for target Bar should be located under \(Bar.appending(components: "Sources", "Bar"))",
519+
diagnostic: .contains("Source files for target Bar should be located under 'Sources/Bar'"),
520520
severity: .warning
521521
)
522522
result.check(

0 commit comments

Comments
 (0)