Skip to content

Commit

Permalink
Pass -parse-as-library when compiling an executable module that has…
Browse files Browse the repository at this point in the history
… a single source file that isn't named `main.swift`

The Swift compiler has certain special behaviors regarding main source files:

- if a module has just a single source file of any name, it's treated as the main source file
- if a module has a source file named `main.swift`, it's treated as the main source file

If a source file is considered the main source file, it can have top level code.  But a source file that has top level code can't also have `@main`.

This means that a single source file executable module can't use `@main`, regardless of the name of that source file.  A second empty source file can be added as a workaround, but we can employ some countermeasures in SwiftPM.

Specifically, if the executable module consists of a single source file and it is not named `main.swift`, we pass `-parse-as-library` so that a single-source file module will work.  This matches what can be seen in the build logs in Xcode.

This does not allow use of `@main` in source files named `main.swift`, but that will require compiler support to address.

rdar://76746150
  • Loading branch information
abertelrud committed Apr 16, 2021
1 parent ce50cb0 commit 219dd49
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 7 deletions.
17 changes: 10 additions & 7 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,13 @@ 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 configuration of the source code
/// (for example because it has a single source file whose name isn't "main.swift").
var needsToBeParsedAsLibrary: Bool {
let sources = self.sources
return sources.count == 1 && sources.first?.basename != "main.swift"
}

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

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

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

if buildParameters.useWholeModuleOptimization {
Expand Down Expand Up @@ -817,7 +820,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 target.type == .library || target.type == .test || (target.type == .executable && self.needsToBeParsedAsLibrary) {
result.append("-parse-as-library")
}

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

if target.type == .library || target.type == .test {
if target.type == .library || target.type == .test || (target.type == .executable && self.needsToBeParsedAsLibrary) {
result.append("-parse-as-library")
}
// FIXME: Handle WMO
Expand Down
51 changes: 51 additions & 0 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,57 @@ 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",
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

0 comments on commit 219dd49

Please sign in to comment.