Skip to content

Commit

Permalink
[Build] Generate ObjC compatibility header for library Swift targets
Browse files Browse the repository at this point in the history
This will generate the ObjC header for library Swift targets so they can be
used in other ObjC targets. This is only done for macOS.

<rdar://problem/51780725>
  • Loading branch information
aciidgh committed Sep 18, 2019
1 parent ccc14e9 commit 0a04609
Show file tree
Hide file tree
Showing 5 changed files with 289 additions and 10 deletions.
65 changes: 60 additions & 5 deletions Sources/Build/BuildPlan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -507,19 +507,31 @@ public final class SwiftTargetBuildDescription {
/// True if this is the test discovery target.
public let testDiscoveryTarget: Bool

/// The filesystem to operate on.
let fs: FileSystem

/// The modulemap file for this target, if any.
private(set) var moduleMap: AbsolutePath?

/// Create a new target description with target and build parameters.
init(
target: ResolvedTarget,
buildParameters: BuildParameters,
isTestTarget: Bool? = nil,
testDiscoveryTarget: Bool = false
) {
testDiscoveryTarget: Bool = false,
fs: FileSystem = localFileSystem
) throws {
assert(target.underlyingTarget is SwiftTarget, "underlying target type mismatch \(target)")
self.target = target
self.buildParameters = buildParameters
// Unless mentioned explicitly, use the target type to determine if this is a test target.
self.isTestTarget = isTestTarget ?? (target.type == .test)
self.testDiscoveryTarget = testDiscoveryTarget
self.fs = fs

if shouldEmitObjCCompatibilityHeader {
self.moduleMap = try self.generateModuleMap()
}
}

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

// Emit the ObjC compatibility header if enabled.
if shouldEmitObjCCompatibilityHeader {
args += ["-emit-objc-header", "-emit-objc-header-path", objCompatibilityHeaderPath.pathString]
}

// Add arguments needed for code coverage if it is enabled.
if buildParameters.enableCodeCoverage {
args += ["-profile-coverage-mapping", "-profile-generate"]
Expand Down Expand Up @@ -575,6 +592,37 @@ public final class SwiftTargetBuildDescription {
return args
}

/// Returns true if ObjC compatibility header should be emitted.
private var shouldEmitObjCCompatibilityHeader: Bool {
return buildParameters.triple.isDarwin() && target.type == .library
}

/// Generates the module map for the Swift target and returns its path.
private func generateModuleMap() throws -> AbsolutePath {
let path = tempsPath.appending(component: moduleMapFilename)

let stream = BufferedOutputByteStream()
stream <<< "module \(target.c99name) {\n"
stream <<< " header \"" <<< objCompatibilityHeaderPath.pathString <<< "\"\n"
stream <<< " requires objc\n"
stream <<< "}\n"

// Return early if the contents are identical.
if fs.isFile(path), try fs.readFileContents(path) == stream.bytes {
return path
}

try fs.createDirectory(path.parentDirectory, recursive: true)
try fs.writeFileContents(path, bytes: stream.bytes)

return path
}

/// Returns the path to the ObjC compatibility header for this Swift target.
var objCompatibilityHeaderPath: AbsolutePath {
return tempsPath.appending(component: "\(target.name)-Swift.h")
}

/// Returns the build flags from the declared build settings.
private func buildSettingsFlags() -> [String] {
let scope = buildParameters.createScope(for: target)
Expand Down Expand Up @@ -937,7 +985,7 @@ public class BuildPlan {
throw Error.missingLinuxMain
}

let desc = SwiftTargetBuildDescription(
let desc = try SwiftTargetBuildDescription(
target: linuxMainTarget,
buildParameters: buildParameters,
isTestTarget: true
Expand Down Expand Up @@ -969,7 +1017,7 @@ public class BuildPlan {
dependencies: testProduct.targets.map(ResolvedTarget.Dependency.target)
)

let target = SwiftTargetBuildDescription(
let target = try SwiftTargetBuildDescription(
target: linuxMainTarget,
buildParameters: buildParameters,
isTestTarget: true,
Expand Down Expand Up @@ -1011,7 +1059,7 @@ public class BuildPlan {

switch target.underlyingTarget {
case is SwiftTarget:
targetMap[target] = .swift(SwiftTargetBuildDescription(target: target, buildParameters: buildParameters))
targetMap[target] = try .swift(SwiftTargetBuildDescription(target: target, buildParameters: buildParameters, fs: fileSystem))
case is ClangTarget:
targetMap[target] = try .clang(ClangTargetBuildDescription(
target: target,
Expand Down Expand Up @@ -1217,6 +1265,13 @@ public class BuildPlan {
private func plan(clangTarget: ClangTargetBuildDescription) {
for dependency in clangTarget.target.recursiveDependencies() {
switch dependency.underlyingTarget {
case is SwiftTarget:
if case let .swift(dependencyTargetDescription)? = targetMap[dependency] {
if let moduleMap = dependencyTargetDescription.moduleMap {
clangTarget.additionalFlags += ["-fmodule-map-file=\(moduleMap.pathString)"]
}
}

case let target as ClangTarget where target.type == .library:
// Setup search paths for C dependencies:
clangTarget.additionalFlags += ["-I", target.includeDir.pathString]
Expand Down
32 changes: 30 additions & 2 deletions Sources/Build/llbuild.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,35 @@ public final class LLBuildManifestGenerator {
(target.clangTarget.cLanguageStandard, SupportedLanguageExtension.cExtensions),
]

var externalDependencies = SortedArray<String>()

func addStaticTargetInputs(_ target: ResolvedTarget) {
if case .swift(let desc)? = plan.targetMap[target], target.type == .library {
externalDependencies.insert(desc.moduleOutputPath.pathString)
}
}

for dependency in target.target.dependencies {
switch dependency {
case .target(let target):
addStaticTargetInputs(target)

case .product(let product):
switch product.type {
case .executable, .library(.dynamic):
// Establish a dependency on binary of the product.
externalDependencies += [plan.productMap[product]!.binary.pathString]

case .library(.automatic), .library(.static):
for target in product.targets {
addStaticTargetInputs(target)
}
case .test:
break
}
}
}

let commands: [Command] = try target.compilePaths().map({ path in
var args = target.basicArguments()
args += ["-MD", "-MT", "dependencies", "-MF", path.deps.pathString]
Expand All @@ -323,8 +352,7 @@ public final class LLBuildManifestGenerator {
args += ["-c", path.source.pathString, "-o", path.object.pathString]
let clang = ClangTool(
desc: "Compiling \(target.target.name) \(path.filename)",
//FIXME: Should we add build time dependency on dependent targets?
inputs: [path.source.pathString],
inputs: externalDependencies + [path.source.pathString],
outputs: [path.object.pathString],
args: [try plan.buildParameters.toolchain.getClangCompiler().pathString] + args,
deps: path.deps.pathString)
Expand Down
184 changes: 184 additions & 0 deletions Tests/BuildTests/BuildPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,190 @@ final class BuildPlanTests: XCTestCase {
"""), contents)
}
}

func testObjCHeader1() throws {
// This has a Swift and ObjC target in the same package.
let fs = InMemoryFileSystem(emptyFiles:
"/PkgA/Sources/Bar/main.m",
"/PkgA/Sources/Foo/Foo.swift"
)

let diagnostics = DiagnosticsEngine()
let graph = loadPackageGraph(root: "/PkgA", fs: fs, diagnostics: diagnostics,
manifests: [
Manifest.createV4Manifest(
name: "PkgA",
path: "/PkgA",
url: "/PkgA",
targets: [
TargetDescription(name: "Foo", dependencies: []),
TargetDescription(name: "Bar", dependencies: ["Foo"]),
]),
]
)
XCTAssertNoDiagnostics(diagnostics)

let plan = try BuildPlan(buildParameters: mockBuildParameters(), graph: graph, diagnostics: diagnostics, fileSystem: fs)
let result = BuildPlanResult(plan: plan)

let fooTarget = try result.target(for: "Foo").swiftTarget().compileArguments()
#if os(macOS)
XCTAssertMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
#else
XCTAssertNoMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
#endif

let barTarget = try result.target(for: "Bar").clangTarget().basicArguments()
#if os(macOS)
XCTAssertMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
#else
XCTAssertNoMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
#endif

mktmpdir { path in
let yaml = path.appending(component: "debug.yaml")
let llbuild = LLBuildManifestGenerator(plan, client: "swift-build")
try llbuild.generateManifest(at: yaml)
let contents = try localFileSystem.readFileContents(yaml).description
XCTAssertMatch(contents, .contains("""
"/path/to/build/debug/Bar.build/main.m.o":
tool: clang
description: "Compiling Bar main.m"
inputs: ["/path/to/build/debug/Foo.swiftmodule","/PkgA/Sources/Bar/main.m"]
"""))
}
}

func testObjCHeader2() throws {
// This has a Swift and ObjC target in different packages with automatic product type.
let fs = InMemoryFileSystem(emptyFiles:
"/PkgA/Sources/Bar/main.m",
"/PkgB/Sources/Foo/Foo.swift"
)

let diagnostics = DiagnosticsEngine()
let graph = loadPackageGraph(root: "/PkgA", fs: fs, diagnostics: diagnostics,
manifests: [
Manifest.createV4Manifest(
name: "PkgA",
path: "/PkgA",
url: "/PkgA",
dependencies: [
PackageDependencyDescription(url: "/PkgB", requirement: .upToNextMajor(from: "1.0.0")),
],
targets: [
TargetDescription(name: "Bar", dependencies: ["Foo"]),
]),
Manifest.createV4Manifest(
name: "PkgB",
path: "/PkgB",
url: "/PkgB",
products: [
ProductDescription(name: "Foo", targets: ["Foo"]),
],
targets: [
TargetDescription(name: "Foo", dependencies: []),
]),
]
)
XCTAssertNoDiagnostics(diagnostics)

let plan = try BuildPlan(buildParameters: mockBuildParameters(), graph: graph, diagnostics: diagnostics, fileSystem: fs)
let result = BuildPlanResult(plan: plan)

let fooTarget = try result.target(for: "Foo").swiftTarget().compileArguments()
#if os(macOS)
XCTAssertMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
#else
XCTAssertNoMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
#endif

let barTarget = try result.target(for: "Bar").clangTarget().basicArguments()
#if os(macOS)
XCTAssertMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
#else
XCTAssertNoMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
#endif

mktmpdir { path in
let yaml = path.appending(component: "debug.yaml")
let llbuild = LLBuildManifestGenerator(plan, client: "swift-build")
try llbuild.generateManifest(at: yaml)
let contents = try localFileSystem.readFileContents(yaml).description
XCTAssertMatch(contents, .contains("""
"/path/to/build/debug/Bar.build/main.m.o":
tool: clang
description: "Compiling Bar main.m"
inputs: ["/path/to/build/debug/Foo.swiftmodule","/PkgA/Sources/Bar/main.m"]
"""))
}
}

func testObjCHeader3() throws {
// This has a Swift and ObjC target in different packages with dynamic product type.
let fs = InMemoryFileSystem(emptyFiles:
"/PkgA/Sources/Bar/main.m",
"/PkgB/Sources/Foo/Foo.swift"
)

let diagnostics = DiagnosticsEngine()
let graph = loadPackageGraph(root: "/PkgA", fs: fs, diagnostics: diagnostics,
manifests: [
Manifest.createV4Manifest(
name: "PkgA",
path: "/PkgA",
url: "/PkgA",
dependencies: [
PackageDependencyDescription(url: "/PkgB", requirement: .upToNextMajor(from: "1.0.0")),
],
targets: [
TargetDescription(name: "Bar", dependencies: ["Foo"]),
]),
Manifest.createV4Manifest(
name: "PkgB",
path: "/PkgB",
url: "/PkgB",
products: [
ProductDescription(name: "Foo", type: .library(.dynamic), targets: ["Foo"]),
],
targets: [
TargetDescription(name: "Foo", dependencies: []),
]),
]
)
XCTAssertNoDiagnostics(diagnostics)

let plan = try BuildPlan(buildParameters: mockBuildParameters(), graph: graph, diagnostics: diagnostics, fileSystem: fs)
let dynamicLibraryExtension = plan.buildParameters.triple.dynamicLibraryExtension
let result = BuildPlanResult(plan: plan)

let fooTarget = try result.target(for: "Foo").swiftTarget().compileArguments()
#if os(macOS)
XCTAssertMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
#else
XCTAssertNoMatch(fooTarget, [.anySequence, "-emit-objc-header", "-emit-objc-header-path", "/path/to/build/debug/Foo.build/Foo-Swift.h", .anySequence])
#endif

let barTarget = try result.target(for: "Bar").clangTarget().basicArguments()
#if os(macOS)
XCTAssertMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
#else
XCTAssertNoMatch(barTarget, [.anySequence, "-fmodule-map-file=/path/to/build/debug/Foo.build/module.modulemap", .anySequence])
#endif

mktmpdir { path in
let yaml = path.appending(component: "debug.yaml")
let llbuild = LLBuildManifestGenerator(plan, client: "swift-build")
try llbuild.generateManifest(at: yaml)
let contents = try localFileSystem.readFileContents(yaml).description
XCTAssertMatch(contents, .contains("""
"/path/to/build/debug/Bar.build/main.m.o":
tool: clang
description: "Compiling Bar main.m"
inputs: ["/path/to/build/debug/libFoo\(dynamicLibraryExtension)","/PkgA/Sources/Bar/main.m"]
"""))
}
}
}

// MARK:- Test Helpers
Expand Down
3 changes: 3 additions & 0 deletions Tests/BuildTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ extension BuildPlanTests {
("testExtraBuildFlags", testExtraBuildFlags),
("testIndexStore", testIndexStore),
("testNonReachableProductsAndTargets", testNonReachableProductsAndTargets),
("testObjCHeader1", testObjCHeader1),
("testObjCHeader2", testObjCHeader2),
("testObjCHeader3", testObjCHeader3),
("testPkgConfigGenericDiagnostic", testPkgConfigGenericDiagnostic),
("testPkgConfigHintDiagnostic", testPkgConfigHintDiagnostic),
("testPlatforms", testPlatforms),
Expand Down

0 comments on commit 0a04609

Please sign in to comment.