Skip to content

Commit 669225c

Browse files
committed
Add support for custom module maps
- This involved changing the all-product-headers.yaml overlay to only overlay the headers in the directory, and passing it to the Clang compile command. - Additionally, building a Mixed target dependency will pass the build directory module map instead of the custom one. This is because the custom one needs to be modified to include the generated Swift header. And, because the the rest of the module map's contents coms from the custom one, it may have relative header paths, so the all-product-headers.yaml overlay file needs to be passed along with the build directory module map when building a mixed target as a dependency. - Added TODO to address warning when using custom module map and resources.
1 parent 76c5303 commit 669225c

File tree

3 files changed

+129
-52
lines changed

3 files changed

+129
-52
lines changed

Sources/Basics/VFSOverlay.swift

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,25 +96,33 @@ public extension VFSOverlay {
9696
/// - Parameters:
9797
/// - directoryPath: The directory to recursively search for resources in.
9898
/// - fileSystem: The file system to search.
99+
/// - shouldInclude: A closure used to determine if the tree should include a given path.
100+
/// Defaults to including any path.
99101
/// - Returns: An array of `VFSOverlay.Resource`s from the given directory.
100102
/// - Throws: An error if the given path is a not a directory.
101103
/// - Note: This API will recursively scan all subpaths of the given path.
102104
static func overlayResources(
103105
directoryPath: AbsolutePath,
104-
fileSystem: FileSystem
106+
fileSystem: FileSystem,
107+
shouldInclude: (AbsolutePath) -> Bool = { _ in true }
105108
) throws -> [VFSOverlay.Resource] {
106109
return
107110
// Absolute path to each resource in the directory.
108111
try fileSystem.getDirectoryContents(directoryPath).map(directoryPath.appending(component:))
109112
// Map each path to a corresponding VFSOverlay, recursing for directories.
110113
.compactMap { resourcePath in
114+
guard shouldInclude(resourcePath) else {
115+
return nil
116+
}
117+
111118
if fileSystem.isDirectory(resourcePath) {
112119
return VFSOverlay.Directory(
113120
name: resourcePath.basename,
114121
contents:
115122
try overlayResources(
116123
directoryPath: resourcePath,
117-
fileSystem: fileSystem
124+
fileSystem: fileSystem,
125+
shouldInclude: shouldInclude
118126
)
119127
)
120128
} else if fileSystem.isFile(resourcePath) {

Sources/Build/BuildPlan.swift

Lines changed: 119 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,62 @@ public final class ClangTargetBuildDescription {
331331

332332
// Try computing modulemap path for a C library. This also creates the file in the file system, if needed.
333333
if target.type == .library {
334-
// If there's a custom module map, use it as given.
335-
if case .custom(let path) = clangTarget.moduleMapType {
336-
self.moduleMap = path
334+
if case .custom(let customModuleMapPath) = clangTarget.moduleMapType {
335+
if isWithinMixedTarget {
336+
let customModuleMapContents: String = try fileSystem.readFileContents(customModuleMapPath)
337+
338+
// Check that custom module map does not contain a Swift
339+
// submodule.
340+
if customModuleMapContents.contains("\(clangTarget.c99name).Swift") {
341+
throw StringError("The target's module map may not " +
342+
"contain a Swift submodule for " +
343+
"the module \(target.c99name).")
344+
}
345+
346+
// Add a submodule to wrap the generated Swift header in
347+
// the custom module map.
348+
let modifiedModuleMapContents = """
349+
\(customModuleMapContents)
350+
351+
module \(target.c99name).Swift {
352+
header "\(target.c99name)-Swift.h"
353+
requires objc
354+
}
355+
"""
356+
357+
// Write the modified contents to a new module map in the
358+
// build directory.
359+
let writePath = tempsPath.appending(component: moduleMapFilename)
360+
try fileSystem.createDirectory(writePath.parentDirectory, recursive: true)
361+
362+
// If the file exists with the identical contents, we don't need to rewrite it.
363+
// Otherwise, compiler will recompile even if nothing else has changed.
364+
let contents = try? fileSystem.readFileContents(writePath) as String
365+
if contents != modifiedModuleMapContents {
366+
try fileSystem.writeFileContents(writePath, string: modifiedModuleMapContents)
367+
}
368+
369+
self.moduleMap = writePath
370+
371+
// The unextended module map purposefully excludes the
372+
// generated Swift header. This is so, when compiling the
373+
// mixed target's Swift part, the generated Swift header is
374+
// not considered an input (the header is an output of
375+
// compiling the mixed target's Swift part).
376+
//
377+
// At this point, the custom module map has been checked
378+
// that it does not include the Swift submodule. For that
379+
// reason, it can be copied to the build directory to
380+
// double as the unextended module map.
381+
try? fileSystem.copy(
382+
from: customModuleMapPath,
383+
to: tempsPath.appending(component: unextendedModuleMapFilename)
384+
)
385+
} else {
386+
// When not building within a mixed target, use the custom
387+
// module map as it does not need to be modified.
388+
self.moduleMap = customModuleMapPath
389+
}
337390
}
338391
// If a generated module map is needed, generate one now in our temporary directory.
339392
else if let generatedModuleMapType = clangTarget.moduleMapType.generatedModuleMapType {
@@ -463,7 +516,15 @@ public final class ClangTargetBuildDescription {
463516
args += ["-F", buildParameters.buildPath.pathString]
464517
}
465518

466-
args += ["-I", clangTarget.includeDir.pathString]
519+
// For mixed targets, the `include` directory is instead overlayed over
520+
// the build directory and that directory is passed as a header search
521+
// path (See `MixedTargetBuildDescription`). This is done to avoid
522+
// module re-declaration errors for cases when a mixed target contains
523+
// a custom module map.
524+
if !isWithinMixedTarget {
525+
args += ["-I", clangTarget.includeDir.pathString]
526+
}
527+
467528
args += additionalFlags
468529
if enableModules {
469530
args += try moduleCacheArgs
@@ -1361,6 +1422,10 @@ public final class MixedTargetBuildDescription {
13611422
swiftTargetBuildDescription.resourceBundleInfoPlistPath
13621423
}
13631424

1425+
/// The path to the VFS overlay file that overlays the public headers of
1426+
/// the Clang half of the target over the target's build directory.
1427+
private(set) var allProductHeadersOverlay: AbsolutePath? = nil
1428+
13641429
/// The modulemap file for this target, if any
13651430
var moduleMap: AbsolutePath? { clangTargetBuildDescription.moduleMap }
13661431

@@ -1439,7 +1504,6 @@ public final class MixedTargetBuildDescription {
14391504
// with mappings to the target's module map and public headers.
14401505
let publicHeadersPath = clangTargetBuildDescription.clangTarget.includeDir
14411506
let buildArtifactDirectory = swiftTargetBuildDescription.tempsPath
1442-
let generatedInteropHeaderPath = swiftTargetBuildDescription.objCompatibilityHeaderPath
14431507
let allProductHeadersPath = buildArtifactDirectory
14441508
.appending(component: "all-product-headers.yaml")
14451509

@@ -1460,39 +1524,35 @@ public final class MixedTargetBuildDescription {
14601524
)
14611525
]).write(to: unextendedModuleMapOverlayPath, fileSystem: fileSystem)
14621526

1527+
// TODO(ncooke3): What happens if a custom module map exists with a
1528+
// name other than `module.modulemap`?
14631529
try VFSOverlay(roots: [
14641530
VFSOverlay.Directory(
1465-
name: publicHeadersPath.pathString,
1531+
name: buildArtifactDirectory.pathString,
14661532
contents:
14671533
// Public headers
14681534
try VFSOverlay.overlayResources(
14691535
directoryPath: publicHeadersPath,
1470-
fileSystem: fileSystem
1471-
)
1472-
),
1473-
VFSOverlay.Directory(
1474-
name: buildArtifactDirectory.pathString,
1475-
contents: [
1476-
// Module map
1477-
VFSOverlay.File(
1478-
name: moduleMapFilename,
1479-
externalContents:
1480-
buildArtifactDirectory.appending(component: moduleMapFilename).pathString
1481-
)
1482-
]
1483-
),
1484-
VFSOverlay.Directory(
1485-
name: buildArtifactDirectory.pathString,
1486-
contents: [
1487-
// Generated $(ModuleName)-Swift.h header
1488-
VFSOverlay.File(
1489-
name: generatedInteropHeaderPath.basename,
1490-
externalContents: generatedInteropHeaderPath.pathString
1536+
fileSystem: fileSystem,
1537+
shouldInclude: {
1538+
// Filter out a potential custom module map as
1539+
// only the generated module map in the build
1540+
// directory is used.
1541+
!$0.pathString.hasSuffix("module.modulemap")
1542+
}
14911543
)
1492-
]
1493-
),
1544+
)
14941545
]).write(to: allProductHeadersPath, fileSystem: fileSystem)
14951546

1547+
// The Objective-C umbrella is virtually placed in the build
1548+
// directory via a VFS overlay. This is done to preserve
1549+
// relative paths in custom module maps. But when resources
1550+
// exist, a warning will appear because the generated resource
1551+
// header (also in the build directory) is not included in the
1552+
// umbrella directory. Passing `-Wno-incomplete-umbrella` seems
1553+
// to prevent it but is not an ideal workaround.
1554+
// TODO(ncooke3): Investigate a way around this.
1555+
14961556
swiftTargetBuildDescription.additionalFlags.append(
14971557
// Builds Objective-C portion of module.
14981558
"-import-underlying-module"
@@ -1505,14 +1565,19 @@ public final class MixedTargetBuildDescription {
15051565
"-ivfsoverlay", unextendedModuleMapOverlayPath.pathString
15061566
)
15071567

1508-
// For mixed targets, the Swift half of the target will generate
1509-
// an Objective-C compatibility header in the build folder.
1510-
// Compiling the Objective-C half of the target may require this
1511-
// generated header if the Objective-C half uses any APIs from
1512-
// the Swift half. For successful compilation, the directory
1513-
// with the generated header is added as a header search path.
1514-
clangTargetBuildDescription.additionalFlags.append("-I\(buildArtifactDirectory)")
1515-
}
1568+
// Overlay the public headers over the build directory and add
1569+
// the directory to the header search paths. This will also help
1570+
// pickup generated headers in the build directory like the
1571+
// generated interop Swift header.
1572+
clangTargetBuildDescription.additionalFlags += [
1573+
"-I",
1574+
buildArtifactDirectory.pathString,
1575+
"-ivfsoverlay",
1576+
allProductHeadersPath.pathString
1577+
]
1578+
1579+
self.allProductHeadersOverlay = allProductHeadersPath
1580+
}
15161581
}
15171582
}
15181583

@@ -2546,8 +2611,13 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
25462611
case let target as MixedTarget where target.type == .library:
25472612
// Add the modulemap of the dependency if it has one.
25482613
if case let .mixed(dependencyTargetDescription)? = targetMap[dependency] {
2549-
if let moduleMap = dependencyTargetDescription.moduleMap {
2550-
clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"]
2614+
if let moduleMap = dependencyTargetDescription.moduleMap,
2615+
let allProductHeadersOverlay = dependencyTargetDescription.allProductHeadersOverlay {
2616+
clangTarget.additionalFlags += [
2617+
"-fmodule-map-file=\(moduleMap.pathString)",
2618+
"-ivfsoverlay",
2619+
allProductHeadersOverlay.pathString
2620+
]
25512621
}
25522622
}
25532623
case let target as SystemLibraryTarget:
@@ -2608,11 +2678,14 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
26082678
// Add the path to modulemap of the dependency. Currently we
26092679
// require that all Mixed targets have a modulemap as it is
26102680
// required for interoperability.
2611-
guard let moduleMap = target.moduleMap else { break }
2681+
guard
2682+
let moduleMap = target.moduleMap,
2683+
let allProductHeadersOverlay = target.allProductHeadersOverlay
2684+
else { break }
26122685
swiftTarget.appendClangFlags(
26132686
"-fmodule-map-file=\(moduleMap.pathString)",
2614-
"-I",
2615-
target.clangTargetBuildDescription.clangTarget.includeDir.pathString
2687+
"-ivfsoverlay",
2688+
allProductHeadersOverlay.pathString
26162689
)
26172690
default:
26182691
break
@@ -2622,6 +2695,10 @@ public class BuildPlan: SPMBuildCore.BuildPlan {
26222695

26232696
/// Plan a Mixed target.
26242697
private func plan(mixedTarget: MixedTargetBuildDescription) throws {
2698+
// TODO(ncooke3): Add tests for when mixed target depends on
2699+
// - MixedTarget
2700+
// - ClangTarget
2701+
// - SwiftTarget
26252702
try plan(swiftTarget: mixedTarget.swiftTargetBuildDescription)
26262703
try plan(clangTarget: mixedTarget.clangTargetBuildDescription)
26272704
}

Sources/PackageModel/Target.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -596,14 +596,6 @@ public final class MixedTarget: Target {
596596
)
597597
}
598598

599-
if case .custom = moduleMapType {
600-
throw StringError(
601-
"Target with mixed sources at \(path) contains a custom " +
602-
"module map; targets with mixed language sources " +
603-
"do not support custom module maps."
604-
)
605-
}
606-
607599
let swiftSources = Sources(
608600
paths: sources.paths.filter { path in
609601
guard let ext = path.extension else { return false }

0 commit comments

Comments
 (0)