Skip to content
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

Pass compiler flags to alllow use of @main in single-source-file executable modules #3410

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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Swift v.Next

Swift 5.5
-----------
* [3410]
In a package that specifies a minimum tools version of 5.5, `@main` can now be used in a single-source file executable as long as the name of the source file isn't `main.swift`. To work around special compiler semantics with single-file modules, SwiftPM now passes `-parse-as-library` when compiling an executable module that contains a single Swift source file whose name is not `main.swift`.

* [#3310]
Adding a dependency requirement can now be done with the convenience initializer `.package(url: String, revision: String)`.

Expand Down
31 changes: 24 additions & 7 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,27 @@ public final class SwiftTargetBuildDescription {

/// True if this is the test discovery target.
public let testDiscoveryTarget: Bool

/// True if this module needs to be parsed as a library based on the target type and the configuration
/// of the source code (for example because it has a single source file whose name isn't "main.swift").
/// This deactivates heuristics in the Swift compiler that treats single-file modules and source files
/// named "main.swift" specially w.r.t. whether they can have an entry point.
///
/// See https://bugs.swift.org/browse/SR-14488 for discussion about improvements so that SwiftPM can
/// convey the intent to build an executable module to the compiler regardless of the number of files
/// in the module or their names.
var needsToBeParsedAsLibrary: Bool {
switch target.type {
case .library, .test:
return true
case .executable:
guard toolsVersion >= .v5_5 else { return false }
let sources = self.sources
return sources.count == 1 && sources.first?.basename != "main.swift"
default:
return false
}
}

/// The filesystem to operate on.
let fs: FileSystem
Expand Down Expand Up @@ -773,12 +794,8 @@ public final class SwiftTargetBuildDescription {
// FIXME: Eliminate side effect.
result.append(try writeOutputFileMap().pathString)

switch target.type {
case .library, .test:
if self.needsToBeParsedAsLibrary {
result.append("-parse-as-library")

case .executable, .systemModule, .binary, .plugin:
do { }
}

if buildParameters.useWholeModuleOptimization {
Expand Down Expand Up @@ -817,7 +834,7 @@ public final class SwiftTargetBuildDescription {
result.append("-experimental-skip-non-inlinable-function-bodies")
result.append("-force-single-frontend-invocation")

if target.type == .library || target.type == .test {
if self.needsToBeParsedAsLibrary {
result.append("-parse-as-library")
}

Expand Down Expand Up @@ -864,7 +881,7 @@ public final class SwiftTargetBuildDescription {
// FIXME: Eliminate side effect.
result.append(try writeOutputFileMap().pathString)

if target.type == .library || target.type == .test {
if self.needsToBeParsedAsLibrary {
result.append("-parse-as-library")
}
// FIXME: Handle WMO
Expand Down
52 changes: 52 additions & 0 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,58 @@ final class BuildPlanTests: XCTestCase {
#endif
}

func testParseAsLibraryFlagForExe() throws {
let fs = InMemoryFileSystem(emptyFiles:
// First executable has a single source file not named `main.swift`.
"/Pkg/Sources/exe1/foo.swift",
// Second executable has a single source file named `main.swift`.
"/Pkg/Sources/exe2/main.swift",
// Third executable has multiple source files.
"/Pkg/Sources/exe3/bar.swift",
"/Pkg/Sources/exe3/main.swift"
)

let diagnostics = DiagnosticsEngine()
let graph = try loadPackageGraph(fs: fs, diagnostics: diagnostics,
manifests: [
Manifest.createV4Manifest(
name: "Pkg",
path: "/Pkg",
packageKind: .root,
packageLocation: "/Pkg",
toolsVersion: .v5_5,
targets: [
TargetDescription(name: "exe1", type: .executable),
TargetDescription(name: "exe2", type: .executable),
TargetDescription(name: "exe3", type: .executable),
]),
]
)
XCTAssertNoDiagnostics(diagnostics)

let result = BuildPlanResult(plan: try BuildPlan(
buildParameters: mockBuildParameters(shouldLinkStaticSwiftStdlib: true),
graph: graph, diagnostics: diagnostics, fileSystem: fs)
)

result.checkProductsCount(3)
result.checkTargetsCount(3)

XCTAssertNoDiagnostics(diagnostics)

// Check that the first target (single source file not named main) has -parse-as-library.
let exe1 = try result.target(for: "exe1").swiftTarget().emitCommandLine()
XCTAssertMatch(exe1, ["-parse-as-library", .anySequence])

// Check that the second target (single source file named main) does not have -parse-as-library.
let exe2 = try result.target(for: "exe2").swiftTarget().emitCommandLine()
XCTAssertNoMatch(exe2, ["-parse-as-library", .anySequence])

// Check that the third target (multiple source files) does not have -parse-as-library.
let exe3 = try result.target(for: "exe3").swiftTarget().emitCommandLine()
XCTAssertNoMatch(exe3, ["-parse-as-library", .anySequence])
}

func testCModule() throws {
let fs = InMemoryFileSystem(emptyFiles:
"/Pkg/Sources/exe/main.swift",
Expand Down