Skip to content

Commit ad90c17

Browse files
authored
improve heuristics for @main support (#5678) (#5680)
motivation: heuristics was based on file name, which is not accurate enough and did not include files like LinuxMain.swift changes: * change heuristics to look into the ocntent of the source file and check for the @main annotation and decide on that * update tests
1 parent 29c0c5a commit ad90c17

File tree

2 files changed

+64
-23
lines changed

2 files changed

+64
-23
lines changed

Sources/Build/BuildPlan.swift

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -626,21 +626,26 @@ public final class SwiftTargetBuildDescription {
626626
public let isTestDiscoveryTarget: Bool
627627

628628
/// True if this module needs to be parsed as a library based on the target type and the configuration
629-
/// of the source code (for example because it has a single source file whose name isn't "main.swift").
630-
/// This deactivates heuristics in the Swift compiler that treats single-file modules and source files
631-
/// named "main.swift" specially w.r.t. whether they can have an entry point.
632-
///
633-
/// See https://bugs.swift.org/browse/SR-14488 for discussion about improvements so that SwiftPM can
634-
/// convey the intent to build an executable module to the compiler regardless of the number of files
635-
/// in the module or their names.
629+
/// of the source code
636630
var needsToBeParsedAsLibrary: Bool {
637-
switch target.type {
631+
switch self.target.type {
638632
case .library, .test:
639633
return true
640634
case .executable:
641-
guard toolsVersion >= .v5_5 else { return false }
642-
let sources = self.sources
643-
return sources.count == 1 && sources.first?.basename != "main.swift"
635+
// This deactivates heuristics in the Swift compiler that treats single-file modules and source files
636+
// named "main.swift" specially w.r.t. whether they can have an entry point.
637+
//
638+
// See https://bugs.swift.org/browse/SR-14488 for discussion about improvements so that SwiftPM can
639+
// convey the intent to build an executable module to the compiler regardless of the number of files
640+
// in the module or their names.
641+
if self.toolsVersion < .v5_5 || self.sources.count != 1 {
642+
return false
643+
}
644+
// looking into the file content to see if it is using the @main annotation which requires parse-as-library
645+
// this is not bullet-proof since theoretically the file can contain the @main string for other reasons
646+
// but it is the closest to accurate we can do at this point
647+
let content: String = self.sources.first.flatMap({ try? self.fileSystem.readFileContents($0) }) ?? ""
648+
return content.contains("@main")
644649
default:
645650
return false
646651
}

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,15 +1014,41 @@ final class BuildPlanTests: XCTestCase {
10141014

10151015
func testParseAsLibraryFlagForExe() throws {
10161016
let fs = InMemoryFileSystem(emptyFiles:
1017-
// First executable has a single source file not named `main.swift`.
1017+
// executable has a single source file not named `main.swift`, without @main.
10181018
"/Pkg/Sources/exe1/foo.swift",
1019-
// Second executable has a single source file named `main.swift`.
1019+
// executable has a single source file named `main.swift`, without @main.
10201020
"/Pkg/Sources/exe2/main.swift",
1021-
// Third executable has multiple source files.
1022-
"/Pkg/Sources/exe3/bar.swift",
1023-
"/Pkg/Sources/exe3/main.swift"
1021+
// executable has a single source file not named `main.swift`, with @main.
1022+
"/Pkg/Sources/exe3/foo.swift",
1023+
// executable has a single source file named `main.swift`, with @main
1024+
"/Pkg/Sources/exe4/main.swift",
1025+
// executable has multiple source files.
1026+
"/Pkg/Sources/exe5/bar.swift",
1027+
"/Pkg/Sources/exe5/main.swift"
10241028
)
10251029

1030+
try fs.writeFileContents(AbsolutePath("/Pkg/Sources/exe3/foo.swift")) {
1031+
"""
1032+
@main
1033+
struct Runner {
1034+
static func main() {
1035+
print("hello world")
1036+
}
1037+
}
1038+
"""
1039+
}
1040+
1041+
try fs.writeFileContents(AbsolutePath("/Pkg/Sources/exe4/main.swift")) {
1042+
"""
1043+
@main
1044+
struct Runner {
1045+
static func main() {
1046+
print("hello world")
1047+
}
1048+
}
1049+
"""
1050+
}
1051+
10261052
let observability = ObservabilitySystem.makeForTesting()
10271053
let graph = try loadPackageGraph(
10281054
fileSystem: fs,
@@ -1035,6 +1061,8 @@ final class BuildPlanTests: XCTestCase {
10351061
TargetDescription(name: "exe1", type: .executable),
10361062
TargetDescription(name: "exe2", type: .executable),
10371063
TargetDescription(name: "exe3", type: .executable),
1064+
TargetDescription(name: "exe4", type: .executable),
1065+
TargetDescription(name: "exe5", type: .executable),
10381066
]),
10391067
],
10401068
observabilityScope: observability.topScope
@@ -1048,22 +1076,30 @@ final class BuildPlanTests: XCTestCase {
10481076
observabilityScope: observability.topScope
10491077
))
10501078

1051-
result.checkProductsCount(3)
1052-
result.checkTargetsCount(3)
1079+
result.checkProductsCount(5)
1080+
result.checkTargetsCount(5)
10531081

10541082
XCTAssertNoDiagnostics(observability.diagnostics)
10551083

1056-
// Check that the first target (single source file not named main) has -parse-as-library.
1084+
// single source file not named main, and without @main should not have -parse-as-library.
10571085
let exe1 = try result.target(for: "exe1").swiftTarget().emitCommandLine()
1058-
XCTAssertMatch(exe1, ["-parse-as-library"])
1086+
XCTAssertNoMatch(exe1, ["-parse-as-library"])
10591087

1060-
// Check that the second target (single source file named main) does not have -parse-as-library.
1088+
// single source file named main, and without @main should not have -parse-as-library.
10611089
let exe2 = try result.target(for: "exe2").swiftTarget().emitCommandLine()
10621090
XCTAssertNoMatch(exe2, ["-parse-as-library"])
10631091

1064-
// Check that the third target (multiple source files) does not have -parse-as-library.
1092+
// single source file not named main, with @main should have -parse-as-library.
10651093
let exe3 = try result.target(for: "exe3").swiftTarget().emitCommandLine()
1066-
XCTAssertNoMatch(exe3, ["-parse-as-library"])
1094+
XCTAssertMatch(exe3, ["-parse-as-library"])
1095+
1096+
// single source file named main, with @main should have -parse-as-library.
1097+
let exe4 = try result.target(for: "exe4").swiftTarget().emitCommandLine()
1098+
XCTAssertMatch(exe4, ["-parse-as-library"])
1099+
1100+
// multiple source files should not have -parse-as-library.
1101+
let exe5 = try result.target(for: "exe5").swiftTarget().emitCommandLine()
1102+
XCTAssertNoMatch(exe5, ["-parse-as-library"])
10671103
}
10681104

10691105
func testCModule() throws {

0 commit comments

Comments
 (0)