Skip to content

Commit 219dd49

Browse files
committed
Pass -parse-as-library when compiling an executable module that has 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
1 parent ce50cb0 commit 219dd49

File tree

2 files changed

+61
-7
lines changed

2 files changed

+61
-7
lines changed

Sources/Build/BuildPlan.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,13 @@ public final class SwiftTargetBuildDescription {
564564

565565
/// True if this is the test discovery target.
566566
public let testDiscoveryTarget: Bool
567+
568+
/// True if this module needs to be parsed as a library based on the configuration of the source code
569+
/// (for example because it has a single source file whose name isn't "main.swift").
570+
var needsToBeParsedAsLibrary: Bool {
571+
let sources = self.sources
572+
return sources.count == 1 && sources.first?.basename != "main.swift"
573+
}
567574

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

776-
switch target.type {
777-
case .library, .test:
783+
if target.type == .library || target.type == .test || (target.type == .executable && self.needsToBeParsedAsLibrary) {
778784
result.append("-parse-as-library")
779-
780-
case .executable, .systemModule, .binary, .plugin:
781-
do { }
782785
}
783786

784787
if buildParameters.useWholeModuleOptimization {
@@ -817,7 +820,7 @@ public final class SwiftTargetBuildDescription {
817820
result.append("-experimental-skip-non-inlinable-function-bodies")
818821
result.append("-force-single-frontend-invocation")
819822

820-
if target.type == .library || target.type == .test {
823+
if target.type == .library || target.type == .test || (target.type == .executable && self.needsToBeParsedAsLibrary) {
821824
result.append("-parse-as-library")
822825
}
823826

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

867-
if target.type == .library || target.type == .test {
870+
if target.type == .library || target.type == .test || (target.type == .executable && self.needsToBeParsedAsLibrary) {
868871
result.append("-parse-as-library")
869872
}
870873
// FIXME: Handle WMO

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,57 @@ final class BuildPlanTests: XCTestCase {
950950
#endif
951951
}
952952

953+
func testParseAsLibraryFlagForExe() throws {
954+
let fs = InMemoryFileSystem(emptyFiles:
955+
// First executable has a single source file not named `main.swift`.
956+
"/Pkg/Sources/exe1/foo.swift",
957+
// Second executable has a single source file named `main.swift`.
958+
"/Pkg/Sources/exe2/main.swift",
959+
// Third executable has multiple source files.
960+
"/Pkg/Sources/exe3/bar.swift",
961+
"/Pkg/Sources/exe3/main.swift"
962+
)
963+
964+
let diagnostics = DiagnosticsEngine()
965+
let graph = try loadPackageGraph(fs: fs, diagnostics: diagnostics,
966+
manifests: [
967+
Manifest.createV4Manifest(
968+
name: "Pkg",
969+
path: "/Pkg",
970+
packageKind: .root,
971+
packageLocation: "/Pkg",
972+
targets: [
973+
TargetDescription(name: "exe1", type: .executable),
974+
TargetDescription(name: "exe2", type: .executable),
975+
TargetDescription(name: "exe3", type: .executable),
976+
]),
977+
]
978+
)
979+
XCTAssertNoDiagnostics(diagnostics)
980+
981+
let result = BuildPlanResult(plan: try BuildPlan(
982+
buildParameters: mockBuildParameters(shouldLinkStaticSwiftStdlib: true),
983+
graph: graph, diagnostics: diagnostics, fileSystem: fs)
984+
)
985+
986+
result.checkProductsCount(3)
987+
result.checkTargetsCount(3)
988+
989+
XCTAssertNoDiagnostics(diagnostics)
990+
991+
// Check that the first target (single source file not named main) has -parse-as-library.
992+
let exe1 = try result.target(for: "exe1").swiftTarget().emitCommandLine()
993+
XCTAssertMatch(exe1, ["-parse-as-library", .anySequence])
994+
995+
// Check that the second target (single source file named main) does not have -parse-as-library.
996+
let exe2 = try result.target(for: "exe2").swiftTarget().emitCommandLine()
997+
XCTAssertNoMatch(exe2, ["-parse-as-library", .anySequence])
998+
999+
// Check that the third target (multiple source files) does not have -parse-as-library.
1000+
let exe3 = try result.target(for: "exe3").swiftTarget().emitCommandLine()
1001+
XCTAssertNoMatch(exe3, ["-parse-as-library", .anySequence])
1002+
}
1003+
9531004
func testCModule() throws {
9541005
let fs = InMemoryFileSystem(emptyFiles:
9551006
"/Pkg/Sources/exe/main.swift",

0 commit comments

Comments
 (0)