From 577208056c9176ff8ef78952d5b54d8dcdf22bda Mon Sep 17 00:00:00 2001 From: Boris Buegling Date: Mon, 30 Oct 2023 19:37:01 -0600 Subject: [PATCH] Generate Swift module maps during the build This moves generation of module maps for Swift targets into the build process. rdar://89082987 --- .../SwiftTargetBuildDescription.swift | 42 +++++----------- .../LLBuildManifestBuilder+Swift.swift | 20 +++++++- Sources/LLBuildManifest/LLBuildManifest.swift | 48 +++++++++++++++++++ Tests/BuildTests/BuildPlanTests.swift | 6 +-- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift b/Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift index b8c248ded53..5aa30a85a5b 100644 --- a/Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift +++ b/Sources/Build/BuildDescription/SwiftTargetBuildDescription.swift @@ -220,7 +220,13 @@ public final class SwiftTargetBuildDescription { let fileSystem: FileSystem /// The modulemap file for this target, if any. - private(set) var moduleMap: AbsolutePath? + var moduleMap: AbsolutePath? { + if self.shouldGenerateModuleMap { + return self.tempsPath.appending(component: moduleMapFilename) + } else { + return nil + } + } /// The results of applying any build tool plugins to this target. public let buildToolPluginInvocationResults: [BuildToolPluginInvocationResult] @@ -237,6 +243,11 @@ public final class SwiftTargetBuildDescription { /// Whether or not to generate code for test observation. private let shouldGenerateTestObservation: Bool + /// Whether or not to generate a module map for this target. + var shouldGenerateModuleMap: Bool { + return self.shouldEmitObjCCompatibilityHeader + } + /// Create a new target description with target and build parameters. init( package: ResolvedPackage, @@ -286,10 +297,6 @@ public final class SwiftTargetBuildDescription { observabilityScope: observabilityScope ) - if self.shouldEmitObjCCompatibilityHeader { - self.moduleMap = try self.generateModuleMap() - } - // Do nothing if we're not generating a bundle. if self.bundlePath != nil { try self.generateResourceAccessor() @@ -723,31 +730,6 @@ public final class SwiftTargetBuildDescription { return path } - /// Generates the module map for the Swift target and returns its path. - private func generateModuleMap() throws -> AbsolutePath { - let path = self.tempsPath.appending(component: moduleMapFilename) - - let bytes = ByteString( - #""" - module \#(self.target.c99name) { - header "\#(self.objCompatibilityHeaderPath.pathString)" - requires objc - } - - """#.utf8 - ) - - // Return early if the contents are identical. - if self.fileSystem.isFile(path), try self.fileSystem.readFileContents(path) == bytes { - return path - } - - try self.fileSystem.createDirectory(path.parentDirectory, recursive: true) - try self.fileSystem.writeFileContents(path, bytes: bytes) - - return path - } - /// Returns the path to the ObjC compatibility header for this Swift target. var objCompatibilityHeaderPath: AbsolutePath { self.tempsPath.appending("\(self.target.name)-Swift.h") diff --git a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift index 6adcc8d62a7..1d8a1427b51 100644 --- a/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift +++ b/Sources/Build/BuildManifest/LLBuildManifestBuilder+Swift.swift @@ -37,8 +37,26 @@ extension LLBuildManifestBuilder { func createSwiftCompileCommand( _ target: SwiftTargetBuildDescription ) throws { + var modulesReadyInputs = [Node]() + + // If the given target needs a generated module map, set up the dependency and required task to write out the module map. + if target.shouldGenerateModuleMap, let moduleMapPath = target.moduleMap { + modulesReadyInputs.append(.file(moduleMapPath)) + + self.manifest.addWriteSwiftModuleMapCommand( + moduleName: target.target.c99name, + objCompatibilityHeaderPath: target.objCompatibilityHeaderPath, + outputPath: moduleMapPath + ) + } + + let modulesReady = self.addPhonyCommand( + targetName: target.target.getLLBuildModulesReadyCmdName(config: self.buildConfig), + inputs: modulesReadyInputs + ) + // Inputs. - let inputs = try self.computeSwiftCompileCmdInputs(target) + let inputs = try self.computeSwiftCompileCmdInputs(target) + [modulesReady] // Outputs. let objectNodes = try target.objects.map(Node.file) diff --git a/Sources/LLBuildManifest/LLBuildManifest.swift b/Sources/LLBuildManifest/LLBuildManifest.swift index 5f67a12faad..2c30a0fcae2 100644 --- a/Sources/LLBuildManifest/LLBuildManifest.swift +++ b/Sources/LLBuildManifest/LLBuildManifest.swift @@ -29,6 +29,7 @@ public enum WriteAuxiliary { LinkFileList.self, SourcesFileList.self, SwiftGetVersion.self, + SwiftModuleMap.self, XCTestInfoPlist.self ] @@ -56,6 +57,39 @@ public enum WriteAuxiliary { } } + public struct SwiftModuleMap: AuxiliaryFileType { + public static let name = "swift-modulemap" + + public static func computeInputs( + moduleName: String, + objCompatibilityHeaderPath: AbsolutePath + ) -> [Node] { + return [ + .virtual(Self.name), + .virtual(moduleName), + // this can't be a path since we don't create any rule to generate this file, it's a byproduct of the compilation + .virtual(objCompatibilityHeaderPath.pathString) + ] + } + + public static func getFileContents(inputs: [Node]) throws -> String { + guard inputs.count == 2 else { + throw StringError("invalid module map generation task, inputs: \(inputs)") + } + + let moduleName = inputs[0].extractedVirtualNodeName + let objCompatibilityHeaderPath = try AbsolutePath(validating: inputs[1].extractedVirtualNodeName) + + return #""" + module \#(moduleName) { + header "\#(objCompatibilityHeaderPath.pathString)" + requires objc + } + + """# + } + } + public struct ClangModuleMap: AuxiliaryFileType { public static let name = "modulemap" @@ -334,6 +368,20 @@ public struct LLBuildManifest { commands[name] = Command(name: name, tool: tool) } + public mutating func addWriteSwiftModuleMapCommand( + moduleName: String, + objCompatibilityHeaderPath: AbsolutePath, + outputPath: AbsolutePath + ) { + let inputs = WriteAuxiliary.SwiftModuleMap.computeInputs( + moduleName: moduleName, + objCompatibilityHeaderPath: objCompatibilityHeaderPath + ) + let tool = WriteAuxiliaryFile(inputs: inputs, outputFilePath: outputPath) + let name = outputPath.pathString + commands[name] = Command(name: name, tool: tool) + } + public mutating func addWriteClangModuleMapCommand( targetName: String, moduleName: String, diff --git a/Tests/BuildTests/BuildPlanTests.swift b/Tests/BuildTests/BuildPlanTests.swift index 43c05cfeda9..9ffe55d3540 100644 --- a/Tests/BuildTests/BuildPlanTests.swift +++ b/Tests/BuildTests/BuildPlanTests.swift @@ -874,7 +874,7 @@ final class BuildPlanTests: XCTestCase { let contents: String = try fs.readFileContents(yaml) let swiftGetVersionFilePath = try XCTUnwrap(llbuild.swiftGetVersionFiles.first?.value) XCTAssertMatch(contents, .contains(""" - inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString)","\(swiftGetVersionFilePath.escapedPathString)","\(buildPath.appending(components: "PkgLib.swiftmodule").escapedPathString)","\(buildPath.appending(components: "exe.build", "sources").escapedPathString)"] + inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString)","\(swiftGetVersionFilePath.escapedPathString)","\(buildPath.appending(components: "PkgLib.swiftmodule").escapedPathString)","","\(buildPath.appending(components: "exe.build", "sources").escapedPathString)"] """)) } @@ -904,7 +904,7 @@ final class BuildPlanTests: XCTestCase { let buildPath = plan.buildParameters.dataPath.appending(components: "debug") let swiftGetVersionFilePath = try XCTUnwrap(llbuild.swiftGetVersionFiles.first?.value) XCTAssertMatch(contents, .contains(""" - inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString)","\(swiftGetVersionFilePath.escapedPathString)","\(buildPath.appending(components: "exe.build", "sources").escapedPathString)"] + inputs: ["\(Pkg.appending(components: "Sources", "exe", "main.swift").escapedPathString)","\(swiftGetVersionFilePath.escapedPathString)","","\(buildPath.appending(components: "exe.build", "sources").escapedPathString)"] """)) } } @@ -3972,7 +3972,7 @@ final class BuildPlanTests: XCTestCase { let suffix = "" #endif XCTAssertMatch(contents, .contains(""" - inputs: ["\(PkgA.appending(components: "Sources", "swiftlib", "lib.swift").escapedPathString)","\(swiftGetVersionFilePath.escapedPathString)","\(buildPath.appending(components: "exe\(suffix)").escapedPathString)","\(buildPath.appending(components: "swiftlib.build", "sources").escapedPathString)"] + inputs: ["\(PkgA.appending(components: "Sources", "swiftlib", "lib.swift").escapedPathString)","\(swiftGetVersionFilePath.escapedPathString)","\(buildPath.appending(components: "exe\(suffix)").escapedPathString)","","\(buildPath.appending(components: "swiftlib.build", "sources").escapedPathString)"] outputs: ["\(buildPath.appending(components: "swiftlib.build", "lib.swift.o").escapedPathString)","\(buildPath.escapedPathString) """)) }