Skip to content

Commit ea24981

Browse files
authored
improve heuristics for @main support (#5678)
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 d869960 commit ea24981

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
@@ -1206,15 +1206,41 @@ final class BuildPlanTests: XCTestCase {
12061206

12071207
func testParseAsLibraryFlagForExe() throws {
12081208
let fs = InMemoryFileSystem(emptyFiles:
1209-
// First executable has a single source file not named `main.swift`.
1209+
// executable has a single source file not named `main.swift`, without @main.
12101210
"/Pkg/Sources/exe1/foo.swift",
1211-
// Second executable has a single source file named `main.swift`.
1211+
// executable has a single source file named `main.swift`, without @main.
12121212
"/Pkg/Sources/exe2/main.swift",
1213-
// Third executable has multiple source files.
1214-
"/Pkg/Sources/exe3/bar.swift",
1215-
"/Pkg/Sources/exe3/main.swift"
1213+
// executable has a single source file not named `main.swift`, with @main.
1214+
"/Pkg/Sources/exe3/foo.swift",
1215+
// executable has a single source file named `main.swift`, with @main
1216+
"/Pkg/Sources/exe4/main.swift",
1217+
// executable has multiple source files.
1218+
"/Pkg/Sources/exe5/bar.swift",
1219+
"/Pkg/Sources/exe5/main.swift"
12161220
)
12171221

1222+
try fs.writeFileContents(AbsolutePath("/Pkg/Sources/exe3/foo.swift")) {
1223+
"""
1224+
@main
1225+
struct Runner {
1226+
static func main() {
1227+
print("hello world")
1228+
}
1229+
}
1230+
"""
1231+
}
1232+
1233+
try fs.writeFileContents(AbsolutePath("/Pkg/Sources/exe4/main.swift")) {
1234+
"""
1235+
@main
1236+
struct Runner {
1237+
static func main() {
1238+
print("hello world")
1239+
}
1240+
}
1241+
"""
1242+
}
1243+
12181244
let observability = ObservabilitySystem.makeForTesting()
12191245
let graph = try loadPackageGraph(
12201246
fileSystem: fs,
@@ -1227,6 +1253,8 @@ final class BuildPlanTests: XCTestCase {
12271253
TargetDescription(name: "exe1", type: .executable),
12281254
TargetDescription(name: "exe2", type: .executable),
12291255
TargetDescription(name: "exe3", type: .executable),
1256+
TargetDescription(name: "exe4", type: .executable),
1257+
TargetDescription(name: "exe5", type: .executable),
12301258
]),
12311259
],
12321260
observabilityScope: observability.topScope
@@ -1240,22 +1268,30 @@ final class BuildPlanTests: XCTestCase {
12401268
observabilityScope: observability.topScope
12411269
))
12421270

1243-
result.checkProductsCount(3)
1244-
result.checkTargetsCount(3)
1271+
result.checkProductsCount(5)
1272+
result.checkTargetsCount(5)
12451273

12461274
XCTAssertNoDiagnostics(observability.diagnostics)
12471275

1248-
// Check that the first target (single source file not named main) has -parse-as-library.
1276+
// single source file not named main, and without @main should not have -parse-as-library.
12491277
let exe1 = try result.target(for: "exe1").swiftTarget().emitCommandLine()
1250-
XCTAssertMatch(exe1, ["-parse-as-library"])
1278+
XCTAssertNoMatch(exe1, ["-parse-as-library"])
12511279

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

1256-
// Check that the third target (multiple source files) does not have -parse-as-library.
1284+
// single source file not named main, with @main should have -parse-as-library.
12571285
let exe3 = try result.target(for: "exe3").swiftTarget().emitCommandLine()
1258-
XCTAssertNoMatch(exe3, ["-parse-as-library"])
1286+
XCTAssertMatch(exe3, ["-parse-as-library"])
1287+
1288+
// single source file named main, with @main should have -parse-as-library.
1289+
let exe4 = try result.target(for: "exe4").swiftTarget().emitCommandLine()
1290+
XCTAssertMatch(exe4, ["-parse-as-library"])
1291+
1292+
// multiple source files should not have -parse-as-library.
1293+
let exe5 = try result.target(for: "exe5").swiftTarget().emitCommandLine()
1294+
XCTAssertNoMatch(exe5, ["-parse-as-library"])
12591295
}
12601296

12611297
func testCModule() throws {

0 commit comments

Comments
 (0)