From e333dbeeb8bb1c116cdb912b7e96ef92fbf97bf2 Mon Sep 17 00:00:00 2001 From: Anders Bertelrud Date: Fri, 16 Apr 2021 10:30:23 -0700 Subject: [PATCH] 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 --- CHANGELOG.md | 3 ++ Sources/Build/BuildPlan.swift | 31 ++++++++++++---- Tests/BuildTests/BuildPlanTests.swift | 52 +++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d30f053ad75..1507f79e8a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Swift v.Next Swift 5.5 ----------- +* [3410] + 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`. + * [#3310] Adding a dependency requirement can now be done with the convenience initializer `.package(url: String, revision: String)`. diff --git a/Sources/Build/BuildPlan.swift b/Sources/Build/BuildPlan.swift index 95d244f65aa..e2a8ee9bffb 100644 --- a/Sources/Build/BuildPlan.swift +++ b/Sources/Build/BuildPlan.swift @@ -564,6 +564,27 @@ 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 target type and the configuration + /// of the source code (for example because it has a single source file whose name isn't "main.swift"). + /// This deactivates heuristics in the Swift compiler that treats single-file modules and source files + /// named "main.swift" specially w.r.t. whether they can have an entry point. + /// + /// 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. + var needsToBeParsedAsLibrary: Bool { + switch target.type { + case .library, .test: + return true + case .executable: + guard toolsVersion >= .v5_5 else { return false } + let sources = self.sources + return sources.count == 1 && sources.first?.basename != "main.swift" + default: + return false + } + } /// The filesystem to operate on. let fs: FileSystem @@ -773,12 +794,8 @@ public final class SwiftTargetBuildDescription { // FIXME: Eliminate side effect. result.append(try writeOutputFileMap().pathString) - switch target.type { - case .library, .test: + if self.needsToBeParsedAsLibrary { result.append("-parse-as-library") - - case .executable, .systemModule, .binary, .plugin: - do { } } if buildParameters.useWholeModuleOptimization { @@ -817,7 +834,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 self.needsToBeParsedAsLibrary { result.append("-parse-as-library") } @@ -864,7 +881,7 @@ public final class SwiftTargetBuildDescription { // FIXME: Eliminate side effect. result.append(try writeOutputFileMap().pathString) - if target.type == .library || target.type == .test { + if self.needsToBeParsedAsLibrary { result.append("-parse-as-library") } // FIXME: Handle WMO diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index 060bbe290ef..3510c46d12d 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -950,6 +950,58 @@ 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", + toolsVersion: .v5_5, + 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",