Commit 587894a9 authored by Drew's avatar Drew

Umbrella headers v2

We now include the synthesized module map in our build products.  To
support this, a new `:modulemap "synthesized"` directive is available
(and must be used if `umbrella-header` is used.)  We could potentially
have other modulemap modes besides synthesized (explicit, for example).

We use one modulemap privately as part of the build process (to store
the umbrella header) and a different one publicly (which doesn't have
the umbrella header).

This is (surprisingly!) totally legal under the Clang module map

> However, in some cases, the presence or absence of particular headers is used to distinguish between the “public” and “private” APIs of a particular library. For example, a library may contain the headers Foo.h and Foo_Private.h, providing public and private APIs, respectively. Additionally, Foo_Private.h may only be available on some versions of library, and absent in others. One cannot easily express this with a single module map file in the library:

module Foo {
  header "Foo.h"

  explicit module Private {
    header "Foo_Private.h"

> because the header Foo_Private.h won’t always be available. The module
> map file could be customized based on whether Foo_Private.h is
> available or not, but doing so requires custom build machinery.

We are the custom build machinery of which the clattner foretold.

Note that (some) Swift engineers claim this is dangerous, but it works,
is compliant with the Clang specification, and the tests pass.

I should probably also document one other thing that I found diagnosing
why the previous approach didn't work.  A "swiftmodule" (which nobody
seems to understand) actually (sometimes) functions as an overlay for a
Clang module.  See e.g.

What happen is that if you compile with -import-underlying-module (which
we do in the umbrella case) the swift module that gets built is a
"overlay" that points to some underlying clang module, see here:  Then at
import time somebody goes looking for that underlying module (e.g.
modulemap) in addition to the .swiftmodule.

However there is no reason (as far as Clang is concerned) why the
modulemap it finds at runtime may not be totally different than the
modulemap we used at compile time, so that's what we do.
parent ad42c8b4
Pipeline #1050 failed with stage
......@@ -19,6 +19,22 @@ import atpkg
import Glibc //need sleep
/**Synthesize a module map.
- parameter name: The name of the module to synthesize
- parameter umbrellaHeader: A path to the umbrella header. The path must be relative to the exported module map file.
- returns String contents of the synthesized modulemap
private func synthesizeModuleMap(name: String, umbrellaHeader: String?) -> String {
var s = ""
s += "module \(name) {\n"
if let u = umbrellaHeader {
s += " umbrella header \"\(u)\"\n"
s += "\n"
s += "}\n"
return s
/**The ATllbuild tool builds a swift module via llbuild.
For more information on this tool, see `docs/` */
final class ATllbuild : Tool {
......@@ -77,6 +93,11 @@ final class ATllbuild : Tool {
case Executable
case StaticLibrary
enum ModuleMapType {
case None
case Synthesized
* Calculates the llbuild.yaml contents for the given configuration options
......@@ -200,6 +221,7 @@ final class ATllbuild : Tool {
case XCTestStrict = "xctest-strict"
case PublishProduct = "publish-product"
case UmbrellaHeader = "umbrella-header"
case ModuleMap = "module-map"
static var allOptions : [Options] {
......@@ -219,7 +241,8 @@ final class ATllbuild : Tool {
......@@ -283,7 +306,25 @@ final class ATllbuild : Tool {
//check for modulemaps
for product in linkWithProduct {
let productName = product.componentsSeparatedByString(".")[0]
let moduleMapPath = workDirectory + "/products/\(productName).modulemap"
if manager.fileExistsAtPath(moduleMapPath) {
/*per, pretty much
the only way to do this is to create a file called `module.modulemap`. That
potentially conflicts with other modulemaps, so we give it its own directory, namespaced
by the product name. */
let pathName = workDirectory + "/include/\(productName)"
try! manager.createDirectoryAtPath(pathName, withIntermediateDirectories:false, attributes: nil)
try! manager.copyItemAtPath_SWIFTBUG(moduleMapPath, toPath: pathName + "/module.modulemap")
guard let sourceDescriptions = task[Options.Source.rawValue]?.vector?.flatMap({$0.string}) else { fatalError("Can't find sources for atllbuild.") }
var sources = collectSources(sourceDescriptions, taskForCalculatingPath: task)
......@@ -307,19 +348,21 @@ final class ATllbuild : Tool {
let moduleMap: ModuleMapType
if task[Options.ModuleMap.rawValue]?.string == "synthesized" {
moduleMap = .Synthesized
else {
moduleMap = .None
guard let name = task[Options.Name.rawValue]?.string else { fatalError("No name for atllbuild task") }
if let umbrellaHeader = task[Options.UmbrellaHeader.rawValue]?.string {
var s = ""
s += "module \(name) {\n"
s += " umbrella header \"Umbrella.h\"\n"
s += "\n"
s += " export *\n"
s += " module * { export * }\n"
s += "}\n"
precondition(moduleMap == .Synthesized, ":\(Options.UmbrellaHeader.rawValue) \"synthesized\" must be used with the \(Options.UmbrellaHeader.rawValue) option")
let s = synthesizeModuleMap(name, umbrellaHeader: "Umbrella.h")
try! s.writeToFile(workDirectory+"/include/module.modulemap", atomically: false, encoding: NSUTF8StringEncoding)
try! manager.copyItemAtPath_SWIFTBUG(umbrellaHeader, toPath: workDirectory + "/include/Umbrella.h")
try! manager.copyItemAtPath_SWIFTBUG(task.importedPath + umbrellaHeader, toPath: workDirectory + "/include/Umbrella.h")
compileOptions.append(workDirectory + "/include/")
......@@ -360,6 +403,14 @@ final class ATllbuild : Tool {
let yaml = llbuildyaml(sources, workdir: workDirectory, modulename: name, linkSDK: sdk, compileOptions: compileOptions, linkOptions: linkOptions, outputType: outputType, linkWithProduct: linkWithProduct, swiftCPath: swiftCPath)
let _ = try? yaml.writeToFile(llbuildyamlpath, atomically: false, encoding: NSUTF8StringEncoding)
if bootstrapOnly { return }
switch moduleMap {
case .None:
case .Synthesized:
let s = synthesizeModuleMap(name, umbrellaHeader: nil)
try! s.writeToFile(workDirectory + "/products/\(name).modulemap", atomically: false, encoding: NSUTF8StringEncoding)
let cmd = "\(SwiftBuildToolpath) -f \(llbuildyamlpath)"
......@@ -377,6 +428,14 @@ final class ATllbuild : Tool {
case .StaticLibrary:
try! copyByOverwriting("\(workDirectory)/products/\(name).a", toPath: "bin/\(name).a")
switch moduleMap {
case .None:
case .Synthesized:
try! copyByOverwriting("\(workDirectory)/products/\(name).modulemap", toPath: "bin/\(name).modulemap")
......@@ -8,6 +8,28 @@
:name "UmbrellaHeader"
:output-type "static-library"
:umbrella-header "UmbrellaHeader.h"
:module-map "synthesized"
:publish-product true
:build-test {
:tool "atllbuild"
:sources ["tests/**.swift"]
:name "UmbrellaHeaderTests"
:output-type "executable"
:xctestify true
:xctest-strict true
:dependencies ["default"]
:publish-product true
:link-with ["UmbrellaHeader.a"]
:check {
:tool "xctestrun"
:dependencies ["build-test"]
:test-executable "bin/UmbrellaHeaderTests"
\ No newline at end of file
import XCTest
import UmbrellaHeader
class MyTest : XCTestCase {
func testLoad() {
extension MyTest : XCTestCaseProvider {
var allTests : [(String, () throws -> Void)] {
return [
("testLoad", testLoad)
\ No newline at end of file
......@@ -12,7 +12,7 @@ $ATBUILD atbuild
echo "****************UMBRELLA TEST**************"
cd $DIR/tests/fixtures/umbrella_header
$ATBUILD check
echo "****************PUBLISHPRODUCT TEST**************"
cd $DIR/tests/fixtures/publish_product
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment