Skip to content

Allow sources anywhere in ./Sources when only one target is present #6294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Swift Next
Swift 5.9
-----------

* [#6294]

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.

* [#6114]

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.
Expand Down
10 changes: 8 additions & 2 deletions Sources/PackageLoading/Diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ import PackageModel
import TSCBasic

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

static func targetNameHasIncorrectCase(target: String) -> Self {
Expand Down
46 changes: 40 additions & 6 deletions Sources/PackageLoading/PackageBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public enum ModuleError: Swift.Error {
case duplicateModule(String, [String])

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

/// The artifact for the binary target could not be found.
case artifactNotFound(targetName: String, expectedArtifactName: String)
Expand Down Expand Up @@ -87,9 +87,14 @@ extension ModuleError: CustomStringConvertible {
case .duplicateModule(let name, let packages):
let packages = packages.joined(separator: "', '")
return "multiple targets named '\(name)' in: '\(packages)'; consider using the `moduleAliases` parameter in manifest to provide unique names"
case .moduleNotFound(let target, let type):
case .moduleNotFound(let target, let type, let shouldSuggestRelaxedSourceDir):
let folderName = (type == .test) ? "Tests" : (type == .plugin) ? "Plugins" : "Sources"
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"
var clauses = ["Source files for target \(target) should be located under '\(folderName)/\(target)'"]
if shouldSuggestRelaxedSourceDir {
clauses.append("'\(folderName)'")
}
clauses.append("or a custom sources path can be set with the 'path' property in Package.swift")
return clauses.joined(separator: ", ")
case .artifactNotFound(let targetName, let expectedArtifactName):
return "binary target '\(targetName)' could not be mapped to an artifact with expected name '\(expectedArtifactName)'"
case .invalidModuleAlias(let originalName, let newName):
Expand Down Expand Up @@ -526,12 +531,23 @@ public final class PackageBuilder {
return path
}

let commonTargetsOfSimilarType = self.manifest.targetsWithCommonSourceRoot(type: target.type).count
// If there is only one target defined, it may be allowed to occupy the
// entire predefined target directory.
if self.manifest.toolsVersion >= .v5_9 {
if commonTargetsOfSimilarType == 1 {
return predefinedDir.path
}
}

// Otherwise, if the path "exists" then the case in manifest differs from the case on the file system.
if fileSystem.isDirectory(path) {
self.observabilityScope.emit(.targetNameHasIncorrectCase(target: target.name))
return path
}
throw ModuleError.moduleNotFound(target.name, target.type)
throw ModuleError.moduleNotFound(target.name,
target.type,
shouldSuggestRelaxedSourceDir: self.manifest.shouldSuggestRelaxedSourceDir(type: target.type))
}

// Create potential targets.
Expand Down Expand Up @@ -568,7 +584,10 @@ public final class PackageBuilder {
let missingModuleNames = allVisibleModuleNames.subtracting(potentialModulesName)
if let missingModuleName = missingModuleNames.first {
let type = potentialModules.first(where: { $0.name == missingModuleName })?.type ?? .regular
throw ModuleError.moduleNotFound(missingModuleName, type)
throw ModuleError.moduleNotFound(missingModuleName,
type,
shouldSuggestRelaxedSourceDir: self.manifest.shouldSuggestRelaxedSourceDir(type: type)
)
}

let products = Dictionary(manifest.products.map({ ($0.name, $0) }), uniquingKeysWith: { $1 })
Expand Down Expand Up @@ -707,7 +726,9 @@ public final class PackageBuilder {
targets[createdTarget.name] = createdTarget
} else {
emptyModules.insert(potentialModule.name)
self.observabilityScope.emit(.targetHasNoSources(targetPath: potentialModule.path.pathString, target: potentialModule.name))
self.observabilityScope.emit(.targetHasNoSources(name: potentialModule.name,
type: potentialModule.type,
shouldSuggestRelaxedSourceDir: manifest.shouldSuggestRelaxedSourceDir(type: potentialModule.type)))
}
}

Expand Down Expand Up @@ -1391,6 +1412,19 @@ public final class PackageBuilder {
}
return true
}

/// Returns the first suggested predefined source directory for a given target type.
public static func suggestedPredefinedSourceDirectory(type: TargetDescription.TargetType) -> String {
// These are static constants, safe to access by index; the first choice is preferred.
switch type {
case .test:
return predefinedTestDirectories[0]
case .plugin:
return predefinedPluginDirectories[0]
default:
return predefinedSourceDirectories[0]
}
}
}

extension PackageBuilder {
Expand Down
20 changes: 20 additions & 0 deletions Sources/PackageModel/Manifest/Manifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,26 @@ public final class Manifest: Sendable {
return false
}
}

/// Returns a list of target descriptions whose root source directory is the same as that for the given type.
public func targetsWithCommonSourceRoot(type: TargetDescription.TargetType) -> [TargetDescription] {
switch type {
case .test:
return targets.filter { $0.type == .test }
case .plugin:
return targets.filter { $0.type == .plugin }
default:
return targets.filter { $0.type != .test && $0.type != .plugin }
}
}

/// Returns true if the tools version is >= 5.9 and the number of targets with a common source root is 1.
public func shouldSuggestRelaxedSourceDir(type: TargetDescription.TargetType) -> Bool {
guard toolsVersion >= .v5_9 else {
return false
}
return targetsWithCommonSourceRoot(type: type).count == 1
}
}

extension Manifest: Hashable {
Expand Down
16 changes: 9 additions & 7 deletions Sources/Workspace/InitPackage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,7 @@ public final class InitPackage {
if packageType == .executable {
param += """
.executableTarget(
name: "\(pkgname)",
path: "Sources"),
name: "\(pkgname)")
]
"""
} else if packageType == .tool {
Expand All @@ -267,8 +266,7 @@ public final class InitPackage {
name: "\(pkgname)",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
path: "Sources"),
]),
]
"""
} else if packageType == .macro {
Expand Down Expand Up @@ -364,9 +362,13 @@ public final class InitPackage {
progressReporter?("Creating \(sources.relative(to: destinationPath))/")
try makeDirectories(sources)

let moduleDir = packageType == .executable || packageType == .tool
? sources
: sources.appending("\(pkgname)")
let moduleDir: AbsolutePath
switch packageType {
case .executable, .tool:
moduleDir = sources
default:
moduleDir = sources.appending("\(pkgname)")
}
try makeDirectories(moduleDir)

let sourceFileName: String
Expand Down
2 changes: 1 addition & 1 deletion Tests/PackageGraphTests/PackageGraphTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ class PackageGraphTests: XCTestCase {

testDiagnostics(observability.diagnostics) { result in
result.check(
diagnostic: "Source files for target Bar should be located under \(Bar.appending(components: "Sources", "Bar"))",
diagnostic: .contains("Source files for target Bar should be located under 'Sources/Bar'"),
severity: .warning
)
result.check(
Expand Down
Loading