Skip to content

Commit e333dbe

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, meaning that packages will build the same in SwiftPM and in Xcode. Note that this still does not allow use of `@main` in source files named `main.swift`, but that will require compiler support to address. Since this has the potential to break existing packages that use top-level code in a single source file that isn't named `main.swift`, this behavior is gated by a 5.5 tools version. 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. rdar://76746150
1 parent 9650406 commit e333dbe

File tree

3 files changed

+79
-7
lines changed

3 files changed

+79
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Swift v.Next
77

88
Swift 5.5
99
-----------
10+
* [3410]
11+
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`.
12+
1013
* [#3310]
1114
Adding a dependency requirement can now be done with the convenience initializer `.package(url: String, revision: String)`.
1215

Sources/Build/BuildPlan.swift

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,27 @@ 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 target type and the configuration
569+
/// of the source code (for example because it has a single source file whose name isn't "main.swift").
570+
/// This deactivates heuristics in the Swift compiler that treats single-file modules and source files
571+
/// named "main.swift" specially w.r.t. whether they can have an entry point.
572+
///
573+
/// See https://bugs.swift.org/browse/SR-14488 for discussion about improvements so that SwiftPM can
574+
/// convey the intent to build an executable module to the compiler regardless of the number of files
575+
/// in the module or their names.
576+
var needsToBeParsedAsLibrary: Bool {
577+
switch target.type {
578+
case .library, .test:
579+
return true
580+
case .executable:
581+
guard toolsVersion >= .v5_5 else { return false }
582+
let sources = self.sources
583+
return sources.count == 1 && sources.first?.basename != "main.swift"
584+
default:
585+
return false
586+
}
587+
}
567588

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

776-
switch target.type {
777-
case .library, .test:
797+
if self.needsToBeParsedAsLibrary {
778798
result.append("-parse-as-library")
779-
780-
case .executable, .systemModule, .binary, .plugin:
781-
do { }
782799
}
783800

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

820-
if target.type == .library || target.type == .test {
837+
if self.needsToBeParsedAsLibrary {
821838
result.append("-parse-as-library")
822839
}
823840

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

867-
if target.type == .library || target.type == .test {
884+
if self.needsToBeParsedAsLibrary {
868885
result.append("-parse-as-library")
869886
}
870887
// FIXME: Handle WMO

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,58 @@ 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+
toolsVersion: .v5_5,
973+
targets: [
974+
TargetDescription(name: "exe1", type: .executable),
975+
TargetDescription(name: "exe2", type: .executable),
976+
TargetDescription(name: "exe3", type: .executable),
977+
]),
978+
]
979+
)
980+
XCTAssertNoDiagnostics(diagnostics)
981+
982+
let result = BuildPlanResult(plan: try BuildPlan(
983+
buildParameters: mockBuildParameters(shouldLinkStaticSwiftStdlib: true),
984+
graph: graph, diagnostics: diagnostics, fileSystem: fs)
985+
)
986+
987+
result.checkProductsCount(3)
988+
result.checkTargetsCount(3)
989+
990+
XCTAssertNoDiagnostics(diagnostics)
991+
992+
// Check that the first target (single source file not named main) has -parse-as-library.
993+
let exe1 = try result.target(for: "exe1").swiftTarget().emitCommandLine()
994+
XCTAssertMatch(exe1, ["-parse-as-library", .anySequence])
995+
996+
// Check that the second target (single source file named main) does not have -parse-as-library.
997+
let exe2 = try result.target(for: "exe2").swiftTarget().emitCommandLine()
998+
XCTAssertNoMatch(exe2, ["-parse-as-library", .anySequence])
999+
1000+
// Check that the third target (multiple source files) does not have -parse-as-library.
1001+
let exe3 = try result.target(for: "exe3").swiftTarget().emitCommandLine()
1002+
XCTAssertNoMatch(exe3, ["-parse-as-library", .anySequence])
1003+
}
1004+
9531005
func testCModule() throws {
9541006
let fs = InMemoryFileSystem(emptyFiles:
9551007
"/Pkg/Sources/exe/main.swift",

0 commit comments

Comments
 (0)