Skip to content

Commit 60ffec7

Browse files
authored
Merge pull request swiftlang#2838 from abertelrud/eng/SR-13235-libswiftpm-should-vend-modulemap-type
[SR-13235] libSwiftPM should vend a ClangTarget's module map generation information to clients
2 parents 35df0c6 + ee1ab4c commit 60ffec7

File tree

8 files changed

+304
-135
lines changed

8 files changed

+304
-135
lines changed

Sources/Build/BuildPlan.swift

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,18 @@ public final class ClangTargetBuildDescription {
253253

254254
// Try computing modulemap path for a C library. This also creates the file in the file system, if needed.
255255
if target.type == .library {
256-
self.moduleMap = try computeModulemapPath()
256+
// If there's a custom module map, use it as given.
257+
if case .custom(let path) = clangTarget.moduleMapType {
258+
self.moduleMap = path
259+
}
260+
// If a generated module map is needed, generate one now in our temporary directory.
261+
else if let generatedModuleMapType = clangTarget.moduleMapType.generatedModuleMapType {
262+
let path = tempsPath.appending(component: moduleMapFilename)
263+
let moduleMapGenerator = ModuleMapGenerator(targetName: clangTarget.name, moduleName: clangTarget.c99name, publicHeadersDir: clangTarget.includeDir, fileSystem: fileSystem)
264+
try moduleMapGenerator.generateModuleMap(type: generatedModuleMapType, at: path)
265+
self.moduleMap = path
266+
}
267+
// Otherwise there is no module map, and we leave `moduleMap` unset.
257268
}
258269

259270
// Do nothing if we're not generating a bundle.
@@ -399,24 +410,6 @@ public final class ClangTargetBuildDescription {
399410
return compilationConditions
400411
}
401412

402-
403-
/// Helper function to compute the modulemap path.
404-
///
405-
/// This function either returns path to user provided modulemap or tries to automatically generates it.
406-
private func computeModulemapPath() throws -> AbsolutePath {
407-
// If user provided the modulemap, we're done.
408-
if fileSystem.isFile(clangTarget.moduleMapPath) {
409-
return clangTarget.moduleMapPath
410-
} else {
411-
// Otherwise try to generate one.
412-
var moduleMapGenerator = ModuleMapGenerator(for: clangTarget, fileSystem: fileSystem, diagnostics: diagnostics)
413-
// FIXME: We should probably only warn if we're unable to generate the modulemap
414-
// because the clang target is still a valid, it just can't be imported from Swift targets.
415-
try moduleMapGenerator.generateModuleMap(inDir: tempsPath)
416-
return tempsPath.appending(component: moduleMapFilename)
417-
}
418-
}
419-
420413
/// Module cache arguments.
421414
private var moduleCacheArgs: [String] {
422415
return ["-fmodules-cache-path=\(buildParameters.moduleCache.pathString)"]

Sources/PackageLoading/ModuleMapGenerator.swift

Lines changed: 158 additions & 95 deletions
Large diffs are not rendered by default.

Sources/PackageLoading/PackageBuilder.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,19 @@ public final class PackageBuilder {
742742
buildSettings: buildSettings
743743
)
744744
} else {
745+
// It's not a Swift target, so it's a Clang target (those are the only two types of source target currently supported).
746+
747+
// First determine the type of module map that will be appropriate for the target based on its header layout.
748+
// FIXME: We should really be checking the target type to see whether it is one that can vend headers, not just check for the existence of the public headers path. But right now we have now way of distinguishing between, for example, a library and an executable. The semantics here should be to only try to detect the header layout of targets that can vend public headers.
749+
let moduleMapType: ModuleMapType
750+
if fileSystem.exists(publicHeadersPath) {
751+
let moduleMapGenerator = ModuleMapGenerator(targetName: potentialModule.name, moduleName: potentialModule.name.spm_mangledToC99ExtendedIdentifier(), publicHeadersDir: publicHeadersPath, fileSystem: fileSystem)
752+
moduleMapType = moduleMapGenerator.determineModuleMapType(diagnostics: diagnostics)
753+
}
754+
else {
755+
moduleMapType = .none
756+
}
757+
745758
return ClangTarget(
746759
name: potentialModule.name,
747760
bundleName: bundleName,
@@ -750,6 +763,7 @@ public final class PackageBuilder {
750763
cLanguageStandard: manifest.cLanguageStandard,
751764
cxxLanguageStandard: manifest.cxxLanguageStandard,
752765
includeDir: publicHeadersPath,
766+
moduleMapType: moduleMapType,
753767
headers: headers,
754768
isTest: potentialModule.isTest,
755769
sources: sources,

Sources/PackageModel/Target.swift

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,9 @@ public class ClangTarget: Target {
356356

357357
/// The path to include directory.
358358
public let includeDir: AbsolutePath
359+
360+
/// The target's module map type, which determines whether this target vends a custom module map, a generated module map, or no module map at all.
361+
public let moduleMapType: ModuleMapType
359362

360363
/// The headers present in the target.
361364
///
@@ -379,6 +382,7 @@ public class ClangTarget: Target {
379382
cLanguageStandard: String?,
380383
cxxLanguageStandard: String?,
381384
includeDir: AbsolutePath,
385+
moduleMapType: ModuleMapType,
382386
headers: [AbsolutePath] = [],
383387
isTest: Bool = false,
384388
sources: Sources,
@@ -392,6 +396,7 @@ public class ClangTarget: Target {
392396
self.cLanguageStandard = cLanguageStandard
393397
self.cxxLanguageStandard = cxxLanguageStandard
394398
self.includeDir = includeDir
399+
self.moduleMapType = moduleMapType
395400
self.headers = headers
396401
super.init(
397402
name: name,
@@ -407,12 +412,13 @@ public class ClangTarget: Target {
407412
}
408413

409414
private enum CodingKeys: String, CodingKey {
410-
case includeDir, headers, isCXX, cLanguageStandard, cxxLanguageStandard
415+
case includeDir, moduleMapType, headers, isCXX, cLanguageStandard, cxxLanguageStandard
411416
}
412417

413418
public override func encode(to encoder: Encoder) throws {
414419
var container = encoder.container(keyedBy: CodingKeys.self)
415420
try container.encode(includeDir, forKey: .includeDir)
421+
try container.encode(moduleMapType, forKey: .moduleMapType)
416422
try container.encode(headers, forKey: .headers)
417423
try container.encode(isCXX, forKey: .isCXX)
418424
try container.encode(cLanguageStandard, forKey: .cLanguageStandard)
@@ -423,6 +429,7 @@ public class ClangTarget: Target {
423429
required public init(from decoder: Decoder) throws {
424430
let container = try decoder.container(keyedBy: CodingKeys.self)
425431
self.includeDir = try container.decode(AbsolutePath.self, forKey: .includeDir)
432+
self.moduleMapType = try container.decode(ModuleMapType.self, forKey: .moduleMapType)
426433
self.headers = try container.decode([AbsolutePath].self, forKey: .headers)
427434
self.isCXX = try container.decode(Bool.self, forKey: .isCXX)
428435
self.cLanguageStandard = try container.decodeIfPresent(String.self, forKey: .cLanguageStandard)
@@ -486,6 +493,52 @@ public class BinaryTarget: Target {
486493
}
487494
}
488495

496+
/// A type of module map layout. Contains all the information needed to generate or use a module map for a target that can have C-style headers.
497+
public enum ModuleMapType: Equatable, Codable {
498+
/// No module map file.
499+
case none
500+
/// A custom module map file.
501+
case custom(AbsolutePath)
502+
/// An umbrella header included by a generated module map file.
503+
case umbrellaHeader(AbsolutePath)
504+
/// An umbrella directory included by a generated module map file.
505+
case umbrellaDirectory(AbsolutePath)
506+
507+
private enum CodingKeys: String, CodingKey {
508+
case none, custom, umbrellaHeader, umbrellaDirectory
509+
}
510+
511+
public init(from decoder: Decoder) throws {
512+
let container = try decoder.container(keyedBy: CodingKeys.self)
513+
if let path = try container.decodeIfPresent(AbsolutePath.self, forKey: .custom) {
514+
self = .custom(path)
515+
}
516+
else if let path = try container.decodeIfPresent(AbsolutePath.self, forKey: .umbrellaHeader) {
517+
self = .umbrellaHeader(path)
518+
}
519+
else if let path = try container.decodeIfPresent(AbsolutePath.self, forKey: .umbrellaDirectory) {
520+
self = .umbrellaDirectory(path)
521+
}
522+
else {
523+
self = .none
524+
}
525+
}
526+
527+
public func encode(to encoder: Encoder) throws {
528+
var container = encoder.container(keyedBy: CodingKeys.self)
529+
switch self {
530+
case .none:
531+
break
532+
case .custom(let path):
533+
try container.encode(path, forKey: .custom)
534+
case .umbrellaHeader(let path):
535+
try container.encode(path, forKey: .umbrellaHeader)
536+
case .umbrellaDirectory(let path):
537+
try container.encode(path, forKey: .umbrellaDirectory)
538+
}
539+
}
540+
}
541+
489542
extension Target: CustomStringConvertible {
490543
public var description: String {
491544
return "<\(Swift.type(of: self)): \(name)>"

Sources/Workspace/Diagnostics.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,28 @@ extension Diagnostic.Message {
153153
.error("downloaded archive of binary target '\(targetName)' does not contain expected binary artifact '\(artifactName)'")
154154
}
155155
}
156+
157+
158+
extension FileSystemError: CustomStringConvertible {
159+
160+
public var description: String {
161+
switch self {
162+
case .invalidAccess:
163+
return "invalid access"
164+
case .ioError:
165+
return "encountered I/O error"
166+
case .isDirectory:
167+
return "is a directory"
168+
case .noEntry:
169+
return "doesn't exist in file system"
170+
case .notDirectory:
171+
return "is not a directory"
172+
case .unsupported:
173+
return "unsupported operation"
174+
case .unknownOSError:
175+
return "unknown system error"
176+
case .alreadyExistsAtDestination:
177+
return "already exists in file system"
178+
}
179+
}
180+
}

Sources/Xcodeproj/pbxproj.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -588,8 +588,8 @@ public func xcodeProject(
588588
var isGenerated = false
589589

590590
// If user provided the modulemap no need to generate.
591-
if fileSystem.isFile(clangTarget.moduleMapPath) {
592-
moduleMapPath = clangTarget.moduleMapPath
591+
if case .custom(let path) = clangTarget.moduleMapType {
592+
moduleMapPath = path
593593
} else if includeGroup.subitems.contains(where: { $0.path == clangTarget.c99name + ".h" }) {
594594
// If an umbrella header exists, enable Xcode's builtin module's feature rather than generating
595595
// a custom module map. This increases the compatibility of generated Xcode projects.
@@ -600,12 +600,13 @@ public func xcodeProject(
600600
}
601601
targetSettings.common.CLANG_ENABLE_MODULES = "YES"
602602
targetSettings.common.DEFINES_MODULE = "YES"
603-
} else {
604-
// Generate and drop the modulemap inside Xcodeproj folder.
605-
let path = xcodeprojPath.appending(components: "GeneratedModuleMap", clangTarget.c99name)
606-
var moduleMapGenerator = ModuleMapGenerator(for: clangTarget, fileSystem: fileSystem, diagnostics: diagnostics)
607-
try moduleMapGenerator.generateModuleMap(inDir: path)
608-
moduleMapPath = path.appending(component: moduleMapFilename)
603+
} else if let generatedModuleMapType = clangTarget.moduleMapType.generatedModuleMapType {
604+
// Generate and drop the modulemap inside the .xcodeproj wrapper.
605+
let path = xcodeprojPath.appending(components: "GeneratedModuleMap", clangTarget.c99name, moduleMapFilename)
606+
try fileSystem.createDirectory(path.parentDirectory, recursive: true)
607+
let moduleMapGenerator = ModuleMapGenerator(targetName: clangTarget.name, moduleName: clangTarget.c99name, publicHeadersDir: clangTarget.includeDir, fileSystem: fileSystem)
608+
try moduleMapGenerator.generateModuleMap(type: generatedModuleMapType, at: path)
609+
moduleMapPath = path
609610
isGenerated = true
610611
}
611612

Tests/PackageLoadingTests/ModuleMapGenerationTests.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ class ModuleMapGeneration: XCTestCase {
128128
ModuleMapTester("Foo", in: fs) { result in
129129
result.checkNotCreated()
130130
result.checkDiagnostics { result in
131-
result.check(diagnostic: "target 'Foo' failed modulemap generation: umbrella header found at '/include/Foo/Foo.h', but more than one directory exists next to its parent directory: /include/Bar, /include/Foo; consider reducing them to one", behavior: .error)
131+
result.check(diagnostic: "target 'Foo' has invalid header layout: umbrella header found at '/include/Foo/Foo.h', but more than one directory exists next to its parent directory: /include/Bar; consider reducing them to one", behavior: .error)
132132
}
133133
}
134134

@@ -138,25 +138,32 @@ class ModuleMapGeneration: XCTestCase {
138138
ModuleMapTester("Foo", in: fs) { result in
139139
result.checkNotCreated()
140140
result.checkDiagnostics { result in
141-
result.check(diagnostic: "target 'Foo' failed modulemap generation: umbrella header found at '/include/Foo.h', but directories exist next to it: /include/Bar; consider removing them", behavior: .error)
141+
result.check(diagnostic: "target 'Foo' has invalid header layout: umbrella header found at '/include/Foo.h', but directories exist next to it: /include/Bar; consider removing them", behavior: .error)
142142
}
143143
}
144144
}
145145
}
146146

147-
func ModuleMapTester(_ name: String, includeDir: String = "include", in fileSystem: FileSystem, _ body: (ModuleMapResult) -> Void) {
148-
let includeDir = AbsolutePath.root.appending(component: includeDir)
149-
let target = ClangTarget(name: name, cLanguageStandard: nil, cxxLanguageStandard: nil, includeDir: includeDir, isTest: false, sources: Sources(paths: [], root: .root))
147+
/// Helper function to test module map generation. Given a target name and optionally the name of a public-headers directory, this function determines the module map type of the public-headers directory by examining the contents of a file system and invokes a given block to check the module result (including any diagnostics).
148+
func ModuleMapTester(_ targetName: String, includeDir: String = "include", in fileSystem: FileSystem, _ body: (ModuleMapResult) -> Void) {
149+
// Create a module map generator, and determine the type of module map to use for the header directory. This may emit diagnostics.
150150
let diagnostics = DiagnosticsEngine()
151-
var generator = ModuleMapGenerator(for: target, fileSystem: fileSystem, diagnostics: diagnostics)
152-
do {
153-
try generator.generateModuleMap(inDir: .root)
154-
} catch {
155-
//
151+
let moduleMapGenerator = ModuleMapGenerator(targetName: targetName, moduleName: targetName.spm_mangledToC99ExtendedIdentifier(), publicHeadersDir: AbsolutePath.root.appending(component: includeDir), fileSystem: fileSystem)
152+
let moduleMapType = moduleMapGenerator.determineModuleMapType(diagnostics: diagnostics)
153+
154+
// Generate a module map and capture any emitted diagnostics.
155+
let generatedModuleMapPath = AbsolutePath.root.appending(components: "module.modulemap")
156+
diagnostics.wrap {
157+
if let generatedModuleMapType = moduleMapType.generatedModuleMapType {
158+
try moduleMapGenerator.generateModuleMap(type: generatedModuleMapType, at: generatedModuleMapPath)
159+
}
156160
}
157-
let genPath = AbsolutePath.root.appending(components: "module.modulemap")
158-
let result = ModuleMapResult(diagnostics: diagnostics, path: genPath, fs: fileSystem)
161+
162+
// Invoke the closure to check the results.
163+
let result = ModuleMapResult(diagnostics: diagnostics, path: generatedModuleMapPath, fs: fileSystem)
159164
body(result)
165+
166+
// Check for any unexpected diagnostics (the ones the closure didn't check for).
160167
result.validateDiagnostics()
161168
}
162169

Tests/PackageLoadingTests/PackageBuilderTests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class PackageBuilderTests: XCTestCase {
211211
package.checkModule("clib") { module in
212212
module.check(c99name: "clib", type: .library)
213213
module.checkSources(root: "/Sources/clib", paths: "clib.c")
214+
module.check(moduleMapType: .custom(AbsolutePath("/Sources/clib/include/module.modulemap")))
214215
}
215216
}
216217
}
@@ -242,6 +243,7 @@ class PackageBuilderTests: XCTestCase {
242243
package.checkModule("clib") { module in
243244
module.check(c99name: "clib", type: .library)
244245
module.checkSources(root: "/Sources", paths: "clib/clib.c", "clib/clib2.c", "clib/nested/nested.c")
246+
module.check(moduleMapType: .umbrellaHeader(AbsolutePath("/Sources/clib/clib.h")))
245247
}
246248
}
247249
}
@@ -614,12 +616,14 @@ class PackageBuilderTests: XCTestCase {
614616
module.check(c99name: "Foo", type: .library)
615617
module.checkSources(root: "/Sources/Foo", paths: "Foo.c")
616618
module.check(includeDir: "/Sources/Foo/inc")
619+
module.check(moduleMapType: .custom(AbsolutePath("/Sources/Foo/inc/module.modulemap")))
617620
}
618621

619622
package.checkModule("Bar") { module in
620623
module.check(c99name: "Bar", type: .library)
621624
module.checkSources(root: "/Sources/Bar", paths: "Bar.c")
622625
module.check(includeDir: "/Sources/Bar/include")
626+
module.check(moduleMapType: .custom(AbsolutePath("/Sources/Bar/include/module.modulemap")))
623627
}
624628
}
625629
}
@@ -1712,6 +1716,7 @@ class PackageBuilderTests: XCTestCase {
17121716
package.checkModule("lib") { module in
17131717
module.checkSources(root: "/Sources/lib", paths: "lib.c")
17141718
module.check(includeDir: "/Sources/lib/include")
1719+
module.check(moduleMapType: .umbrellaHeader(AbsolutePath("/Sources/lib/include/lib.h")))
17151720
}
17161721
}
17171722
}
@@ -1736,6 +1741,7 @@ class PackageBuilderTests: XCTestCase {
17361741
package.checkModule("lib") { module in
17371742
module.checkSources(root: "/Sources/lib", paths: "movie.mkv", "lib.c")
17381743
module.check(includeDir: "/Sources/lib/include")
1744+
module.check(moduleMapType: .umbrellaHeader(AbsolutePath("/Sources/lib/include/lib.h")))
17391745
}
17401746
}
17411747
}
@@ -2181,6 +2187,13 @@ final class PackageBuilderTester {
21812187
XCTAssertEqual(target.includeDir.pathString, includeDir, file: file, line: line)
21822188
}
21832189

2190+
func check(moduleMapType: ModuleMapType, file: StaticString = #file, line: UInt = #line) {
2191+
guard case let target as ClangTarget = target else {
2192+
return XCTFail("Module map type is being checked on a non-Clang target", file: file, line: line)
2193+
}
2194+
XCTAssertEqual(target.moduleMapType, moduleMapType, file: file, line: line)
2195+
}
2196+
21842197
func check(c99name: String? = nil, type: PackageModel.Target.Kind? = nil, file: StaticString = #file, line: UInt = #line) {
21852198
if let c99name = c99name {
21862199
XCTAssertEqual(target.c99name, c99name, file: file, line: line)

0 commit comments

Comments
 (0)