Skip to content

Commit 4e468ab

Browse files
authored
Merge pull request #2341 from aciidb0mb3r/objc-header
[Build] Generate ObjC compatibility header for library Swift targets
2 parents f562016 + 0a04609 commit 4e468ab

File tree

5 files changed

+289
-10
lines changed

5 files changed

+289
-10
lines changed

Sources/Build/BuildPlan.swift

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -507,19 +507,31 @@ public final class SwiftTargetBuildDescription {
507507
/// True if this is the test discovery target.
508508
public let testDiscoveryTarget: Bool
509509

510+
/// The filesystem to operate on.
511+
let fs: FileSystem
512+
513+
/// The modulemap file for this target, if any.
514+
private(set) var moduleMap: AbsolutePath?
515+
510516
/// Create a new target description with target and build parameters.
511517
init(
512518
target: ResolvedTarget,
513519
buildParameters: BuildParameters,
514520
isTestTarget: Bool? = nil,
515-
testDiscoveryTarget: Bool = false
516-
) {
521+
testDiscoveryTarget: Bool = false,
522+
fs: FileSystem = localFileSystem
523+
) throws {
517524
assert(target.underlyingTarget is SwiftTarget, "underlying target type mismatch \(target)")
518525
self.target = target
519526
self.buildParameters = buildParameters
520527
// Unless mentioned explicitly, use the target type to determine if this is a test target.
521528
self.isTestTarget = isTestTarget ?? (target.type == .test)
522529
self.testDiscoveryTarget = testDiscoveryTarget
530+
self.fs = fs
531+
532+
if shouldEmitObjCCompatibilityHeader {
533+
self.moduleMap = try self.generateModuleMap()
534+
}
523535
}
524536

525537
/// The arguments needed to compile this target.
@@ -548,6 +560,11 @@ public final class SwiftTargetBuildDescription {
548560
args += buildParameters.sanitizers.compileSwiftFlags()
549561
args += ["-parseable-output"]
550562

563+
// Emit the ObjC compatibility header if enabled.
564+
if shouldEmitObjCCompatibilityHeader {
565+
args += ["-emit-objc-header", "-emit-objc-header-path", objCompatibilityHeaderPath.pathString]
566+
}
567+
551568
// Add arguments needed for code coverage if it is enabled.
552569
if buildParameters.enableCodeCoverage {
553570
args += ["-profile-coverage-mapping", "-profile-generate"]
@@ -575,6 +592,37 @@ public final class SwiftTargetBuildDescription {
575592
return args
576593
}
577594

595+
/// Returns true if ObjC compatibility header should be emitted.
596+
private var shouldEmitObjCCompatibilityHeader: Bool {
597+
return buildParameters.triple.isDarwin() && target.type == .library
598+
}
599+
600+
/// Generates the module map for the Swift target and returns its path.
601+
private func generateModuleMap() throws -> AbsolutePath {
602+
let path = tempsPath.appending(component: moduleMapFilename)
603+
604+
let stream = BufferedOutputByteStream()
605+
stream <<< "module \(target.c99name) {\n"
606+
stream <<< " header \"" <<< objCompatibilityHeaderPath.pathString <<< "\"\n"
607+
stream <<< " requires objc\n"
608+
stream <<< "}\n"
609+
610+
// Return early if the contents are identical.
611+
if fs.isFile(path), try fs.readFileContents(path) == stream.bytes {
612+
return path
613+
}
614+
615+
try fs.createDirectory(path.parentDirectory, recursive: true)
616+
try fs.writeFileContents(path, bytes: stream.bytes)
617+
618+
return path
619+
}
620+
621+
/// Returns the path to the ObjC compatibility header for this Swift target.
622+
var objCompatibilityHeaderPath: AbsolutePath {
623+
return tempsPath.appending(component: "\(target.name)-Swift.h")
624+
}
625+
578626
/// Returns the build flags from the declared build settings.
579627
private func buildSettingsFlags() -> [String] {
580628
let scope = buildParameters.createScope(for: target)
@@ -937,7 +985,7 @@ public class BuildPlan {
937985
throw Error.missingLinuxMain
938986
}
939987

940-
let desc = SwiftTargetBuildDescription(
988+
let desc = try SwiftTargetBuildDescription(
941989
target: linuxMainTarget,
942990
buildParameters: buildParameters,
943991
isTestTarget: true
@@ -969,7 +1017,7 @@ public class BuildPlan {
9691017
dependencies: testProduct.targets.map(ResolvedTarget.Dependency.target)
9701018
)
9711019

972-
let target = SwiftTargetBuildDescription(
1020+
let target = try SwiftTargetBuildDescription(
9731021
target: linuxMainTarget,
9741022
buildParameters: buildParameters,
9751023
isTestTarget: true,
@@ -1011,7 +1059,7 @@ public class BuildPlan {
10111059

10121060
switch target.underlyingTarget {
10131061
case is SwiftTarget:
1014-
targetMap[target] = .swift(SwiftTargetBuildDescription(target: target, buildParameters: buildParameters))
1062+
targetMap[target] = try .swift(SwiftTargetBuildDescription(target: target, buildParameters: buildParameters, fs: fileSystem))
10151063
case is ClangTarget:
10161064
targetMap[target] = try .clang(ClangTargetBuildDescription(
10171065
target: target,
@@ -1217,6 +1265,13 @@ public class BuildPlan {
12171265
private func plan(clangTarget: ClangTargetBuildDescription) {
12181266
for dependency in clangTarget.target.recursiveDependencies() {
12191267
switch dependency.underlyingTarget {
1268+
case is SwiftTarget:
1269+
if case let .swift(dependencyTargetDescription)? = targetMap[dependency] {
1270+
if let moduleMap = dependencyTargetDescription.moduleMap {
1271+
clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"]
1272+
}
1273+
}
1274+
12201275
case let target as ClangTarget where target.type == .library:
12211276
// Setup search paths for C dependencies:
12221277
clangTarget.additionalFlags += ["-I", target.includeDir.pathString]

Sources/Build/llbuild.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,35 @@ public final class LLBuildManifestGenerator {
307307
(target.clangTarget.cLanguageStandard, SupportedLanguageExtension.cExtensions),
308308
]
309309

310+
var externalDependencies = SortedArray<String>()
311+
312+
func addStaticTargetInputs(_ target: ResolvedTarget) {
313+
if case .swift(let desc)? = plan.targetMap[target], target.type == .library {
314+
externalDependencies.insert(desc.moduleOutputPath.pathString)
315+
}
316+
}
317+
318+
for dependency in target.target.dependencies {
319+
switch dependency {
320+
case .target(let target):
321+
addStaticTargetInputs(target)
322+
323+
case .product(let product):
324+
switch product.type {
325+
case .executable, .library(.dynamic):
326+
// Establish a dependency on binary of the product.
327+
externalDependencies += [plan.productMap[product]!.binary.pathString]
328+
329+
case .library(.automatic), .library(.static):
330+
for target in product.targets {
331+
addStaticTargetInputs(target)
332+
}
333+
case .test:
334+
break
335+
}
336+
}
337+
}
338+
310339
let commands: [Command] = try target.compilePaths().map({ path in
311340
var args = target.basicArguments()
312341
args += ["-MD", "-MT", "dependencies", "-MF", path.deps.pathString]
@@ -323,8 +352,7 @@ public final class LLBuildManifestGenerator {
323352
args += ["-c", path.source.pathString, "-o", path.object.pathString]
324353
let clang = ClangTool(
325354
desc: "Compiling \(target.target.name) \(path.filename)",
326-
//FIXME: Should we add build time dependency on dependent targets?
327-
inputs: [path.source.pathString],
355+
inputs: externalDependencies + [path.source.pathString],
328356
outputs: [path.object.pathString],
329357
args: [try plan.buildParameters.toolchain.getClangCompiler().pathString] + args,
330358
deps: path.deps.pathString)

Tests/BuildTests/BuildPlanTests.swift

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,6 +1481,190 @@ final class BuildPlanTests: XCTestCase {
14811481
"""), contents)
14821482
}
14831483
}
1484+
1485+
func testObjCHeader1() throws {
1486+
// This has a Swift and ObjC target in the same package.
1487+
let fs = InMemoryFileSystem(emptyFiles:
1488+
"/PkgA/Sources/Bar/main.m",
1489+
"/PkgA/Sources/Foo/Foo.swift"
1490+
)
1491+
1492+
let diagnostics = DiagnosticsEngine()
1493+
let graph = loadPackageGraph(root: "/PkgA", fs: fs, diagnostics: diagnostics,
1494+
manifests: [
1495+
Manifest.createV4Manifest(
1496+
name: "PkgA",
1497+
path: "/PkgA",
1498+
url: "/PkgA",
1499+
targets: [
1500+
TargetDescription(name: "Foo", dependencies: []),
1501+
TargetDescription(name: "Bar", dependencies: ["Foo"]),
1502+
]),
1503+
]
1504+
)
1505+
XCTAssertNoDiagnostics(diagnostics)
1506+
1507+
let plan = try BuildPlan(buildParameters: mockBuildParameters(), graph: graph, diagnostics: diagnostics, fileSystem: fs)
1508+
let result = BuildPlanResult(plan: plan)
1509+
1510+
let fooTarget = try result.target(for: "Foo").swiftTarget().compileArguments()
1511+
#if os(macOS)
1512+
XCTAssertMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
1513+
#else
1514+
XCTAssertNoMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
1515+
#endif
1516+
1517+
let barTarget = try result.target(for: "Bar").clangTarget().basicArguments()
1518+
#if os(macOS)
1519+
XCTAssertMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
1520+
#else
1521+
XCTAssertNoMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
1522+
#endif
1523+
1524+
mktmpdir { path in
1525+
let yaml = path.appending(component: "debug.yaml")
1526+
let llbuild = LLBuildManifestGenerator(plan, client: "swift-build")
1527+
try llbuild.generateManifest(at: yaml)
1528+
let contents = try localFileSystem.readFileContents(yaml).description
1529+
XCTAssertMatch(contents, .contains("""
1530+
"/path/to/build/debug/Bar.build/main.m.o":
1531+
tool: clang
1532+
description: "Compiling Bar main.m"
1533+
inputs: ["/path/to/build/debug/Foo.swiftmodule","/PkgA/Sources/Bar/main.m"]
1534+
"""))
1535+
}
1536+
}
1537+
1538+
func testObjCHeader2() throws {
1539+
// This has a Swift and ObjC target in different packages with automatic product type.
1540+
let fs = InMemoryFileSystem(emptyFiles:
1541+
"/PkgA/Sources/Bar/main.m",
1542+
"/PkgB/Sources/Foo/Foo.swift"
1543+
)
1544+
1545+
let diagnostics = DiagnosticsEngine()
1546+
let graph = loadPackageGraph(root: "/PkgA", fs: fs, diagnostics: diagnostics,
1547+
manifests: [
1548+
Manifest.createV4Manifest(
1549+
name: "PkgA",
1550+
path: "/PkgA",
1551+
url: "/PkgA",
1552+
dependencies: [
1553+
PackageDependencyDescription(url: "/PkgB", requirement: .upToNextMajor(from: "1.0.0")),
1554+
],
1555+
targets: [
1556+
TargetDescription(name: "Bar", dependencies: ["Foo"]),
1557+
]),
1558+
Manifest.createV4Manifest(
1559+
name: "PkgB",
1560+
path: "/PkgB",
1561+
url: "/PkgB",
1562+
products: [
1563+
ProductDescription(name: "Foo", targets: ["Foo"]),
1564+
],
1565+
targets: [
1566+
TargetDescription(name: "Foo", dependencies: []),
1567+
]),
1568+
]
1569+
)
1570+
XCTAssertNoDiagnostics(diagnostics)
1571+
1572+
let plan = try BuildPlan(buildParameters: mockBuildParameters(), graph: graph, diagnostics: diagnostics, fileSystem: fs)
1573+
let result = BuildPlanResult(plan: plan)
1574+
1575+
let fooTarget = try result.target(for: "Foo").swiftTarget().compileArguments()
1576+
#if os(macOS)
1577+
XCTAssertMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
1578+
#else
1579+
XCTAssertNoMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
1580+
#endif
1581+
1582+
let barTarget = try result.target(for: "Bar").clangTarget().basicArguments()
1583+
#if os(macOS)
1584+
XCTAssertMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
1585+
#else
1586+
XCTAssertNoMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
1587+
#endif
1588+
1589+
mktmpdir { path in
1590+
let yaml = path.appending(component: "debug.yaml")
1591+
let llbuild = LLBuildManifestGenerator(plan, client: "swift-build")
1592+
try llbuild.generateManifest(at: yaml)
1593+
let contents = try localFileSystem.readFileContents(yaml).description
1594+
XCTAssertMatch(contents, .contains("""
1595+
"/path/to/build/debug/Bar.build/main.m.o":
1596+
tool: clang
1597+
description: "Compiling Bar main.m"
1598+
inputs: ["/path/to/build/debug/Foo.swiftmodule","/PkgA/Sources/Bar/main.m"]
1599+
"""))
1600+
}
1601+
}
1602+
1603+
func testObjCHeader3() throws {
1604+
// This has a Swift and ObjC target in different packages with dynamic product type.
1605+
let fs = InMemoryFileSystem(emptyFiles:
1606+
"/PkgA/Sources/Bar/main.m",
1607+
"/PkgB/Sources/Foo/Foo.swift"
1608+
)
1609+
1610+
let diagnostics = DiagnosticsEngine()
1611+
let graph = loadPackageGraph(root: "/PkgA", fs: fs, diagnostics: diagnostics,
1612+
manifests: [
1613+
Manifest.createV4Manifest(
1614+
name: "PkgA",
1615+
path: "/PkgA",
1616+
url: "/PkgA",
1617+
dependencies: [
1618+
PackageDependencyDescription(url: "/PkgB", requirement: .upToNextMajor(from: "1.0.0")),
1619+
],
1620+
targets: [
1621+
TargetDescription(name: "Bar", dependencies: ["Foo"]),
1622+
]),
1623+
Manifest.createV4Manifest(
1624+
name: "PkgB",
1625+
path: "/PkgB",
1626+
url: "/PkgB",
1627+
products: [
1628+
ProductDescription(name: "Foo", type: .library(.dynamic), targets: ["Foo"]),
1629+
],
1630+
targets: [
1631+
TargetDescription(name: "Foo", dependencies: []),
1632+
]),
1633+
]
1634+
)
1635+
XCTAssertNoDiagnostics(diagnostics)
1636+
1637+
let plan = try BuildPlan(buildParameters: mockBuildParameters(), graph: graph, diagnostics: diagnostics, fileSystem: fs)
1638+
let dynamicLibraryExtension = plan.buildParameters.triple.dynamicLibraryExtension
1639+
let result = BuildPlanResult(plan: plan)
1640+
1641+
let fooTarget = try result.target(for: "Foo").swiftTarget().compileArguments()
1642+
#if os(macOS)
1643+
XCTAssertMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
1644+
#else
1645+
XCTAssertNoMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
1646+
#endif
1647+
1648+
let barTarget = try result.target(for: "Bar").clangTarget().basicArguments()
1649+
#if os(macOS)
1650+
XCTAssertMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
1651+
#else
1652+
XCTAssertNoMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
1653+
#endif
1654+
1655+
mktmpdir { path in
1656+
let yaml = path.appending(component: "debug.yaml")
1657+
let llbuild = LLBuildManifestGenerator(plan, client: "swift-build")
1658+
try llbuild.generateManifest(at: yaml)
1659+
let contents = try localFileSystem.readFileContents(yaml).description
1660+
XCTAssertMatch(contents, .contains("""
1661+
"/path/to/build/debug/Bar.build/main.m.o":
1662+
tool: clang
1663+
description: "Compiling Bar main.m"
1664+
inputs: ["/path/to/build/debug/libFoo\(dynamicLibraryExtension)","/PkgA/Sources/Bar/main.m"]
1665+
"""))
1666+
}
1667+
}
14841668
}
14851669

14861670
// MARK:- Test Helpers

Tests/BuildTests/XCTestManifests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ extension BuildPlanTests {
2121
("testExtraBuildFlags", testExtraBuildFlags),
2222
("testIndexStore", testIndexStore),
2323
("testNonReachableProductsAndTargets", testNonReachableProductsAndTargets),
24+
("testObjCHeader1", testObjCHeader1),
25+
("testObjCHeader2", testObjCHeader2),
26+
("testObjCHeader3", testObjCHeader3),
2427
("testPkgConfigGenericDiagnostic", testPkgConfigGenericDiagnostic),
2528
("testPkgConfigHintDiagnostic", testPkgConfigHintDiagnostic),
2629
("testPlatforms", testPlatforms),

0 commit comments

Comments
 (0)