Skip to content

Commit 7f27fcf

Browse files
committed
[Xcodeproj] Use Xcode's modulemap generation when umbrella header is present
This makes the C language frameworks actual frameworks when umbrealla header is present in the target. It should be possible to embed these targets in other Xcode projects using subprojects or workspaces.
1 parent 370776d commit 7f27fcf

File tree

5 files changed

+150
-11
lines changed

5 files changed

+150
-11
lines changed

Sources/Xcodeproj/XcodeProjectModel.swift

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,13 @@ public struct Xcode {
302302
init(fileRef: FileReference) {
303303
self.fileRef = fileRef
304304
}
305+
306+
var settings = Settings()
307+
308+
/// A set of file settings.
309+
public struct Settings {
310+
var ATTRIBUTES: [String]?
311+
}
305312
}
306313

307314
/// A table of build settings, which for the sake of simplicity consists
@@ -336,6 +343,7 @@ public struct Xcode {
336343
// they are all either strings or arrays of strings, because even
337344
// a boolean may be a macro reference expression.
338345
var CLANG_CXX_LANGUAGE_STANDARD: String?
346+
var CLANG_ENABLE_MODULES: String?
339347
var CLANG_ENABLE_OBJC_ARC: String?
340348
var COMBINE_HIDPI_IMAGES: String?
341349
var COPY_PHASE_STRIP: String?
@@ -375,8 +383,50 @@ public struct Xcode {
375383
var USE_HEADERMAP: String?
376384
var LD: String?
377385

378-
init(CLANG_CXX_LANGUAGE_STANDARD: String? = nil, CLANG_ENABLE_OBJC_ARC: String? = nil, COMBINE_HIDPI_IMAGES: String? = nil, COPY_PHASE_STRIP: String? = nil, DEBUG_INFORMATION_FORMAT: String? = nil, DEFINES_MODULE: String? = nil, DYLIB_INSTALL_NAME_BASE: String? = nil, EMBEDDED_CONTENT_CONTAINS_SWIFT: String? = nil, ENABLE_NS_ASSERTIONS: String? = nil, ENABLE_TESTABILITY: String? = nil, FRAMEWORK_SEARCH_PATHS: [String]? = nil, GCC_C_LANGUAGE_STANDARD: String? = nil, GCC_OPTIMIZATION_LEVEL: String? = nil, GCC_PREPROCESSOR_DEFINITIONS: [String]? = nil, HEADER_SEARCH_PATHS: [String]? = nil, INFOPLIST_FILE: String? = nil, LD_RUNPATH_SEARCH_PATHS: [String]? = nil, LIBRARY_SEARCH_PATHS: [String]? = nil, MACOSX_DEPLOYMENT_TARGET: String? = nil, MODULEMAP_FILE: String? = nil, ONLY_ACTIVE_ARCH: String? = nil, OTHER_CFLAGS: [String]? = nil, OTHER_LDFLAGS: [String]? = nil, OTHER_SWIFT_FLAGS: [String]? = nil, PRODUCT_BUNDLE_IDENTIFIER: String? = nil, PRODUCT_MODULE_NAME: String? = nil, PRODUCT_NAME: String? = nil, PROJECT_NAME: String? = nil, SDKROOT: String? = nil, SKIP_INSTALL: String? = nil, SUPPORTED_PLATFORMS: [String]? = nil, SWIFT_ACTIVE_COMPILATION_CONDITIONS: [String]? = nil, SWIFT_FORCE_STATIC_LINK_STDLIB: String? = nil, SWIFT_FORCE_DYNAMIC_LINK_STDLIB: String? = nil, SWIFT_OPTIMIZATION_LEVEL: String? = nil, SWIFT_VERSION: String? = nil, TARGET_NAME: String? = nil, USE_HEADERMAP: String? = nil, LD: String? = nil) {
386+
init(
387+
CLANG_CXX_LANGUAGE_STANDARD: String? = nil,
388+
CLANG_ENABLE_MODULES: String? = nil,
389+
CLANG_ENABLE_OBJC_ARC: String? = nil,
390+
COMBINE_HIDPI_IMAGES: String? = nil,
391+
COPY_PHASE_STRIP: String? = nil,
392+
DEBUG_INFORMATION_FORMAT: String? = nil,
393+
DEFINES_MODULE: String? = nil,
394+
DYLIB_INSTALL_NAME_BASE: String? = nil,
395+
EMBEDDED_CONTENT_CONTAINS_SWIFT: String? = nil,
396+
ENABLE_NS_ASSERTIONS: String? = nil,
397+
ENABLE_TESTABILITY: String? = nil,
398+
FRAMEWORK_SEARCH_PATHS: [String]? = nil,
399+
GCC_C_LANGUAGE_STANDARD: String? = nil,
400+
GCC_OPTIMIZATION_LEVEL: String? = nil,
401+
GCC_PREPROCESSOR_DEFINITIONS: [String]? = nil,
402+
HEADER_SEARCH_PATHS: [String]? = nil,
403+
INFOPLIST_FILE: String? = nil,
404+
LD_RUNPATH_SEARCH_PATHS: [String]? = nil,
405+
LIBRARY_SEARCH_PATHS: [String]? = nil,
406+
MACOSX_DEPLOYMENT_TARGET: String? = nil,
407+
MODULEMAP_FILE: String? = nil,
408+
ONLY_ACTIVE_ARCH: String? = nil,
409+
OTHER_CFLAGS: [String]? = nil,
410+
OTHER_LDFLAGS: [String]? = nil,
411+
OTHER_SWIFT_FLAGS: [String]? = nil,
412+
PRODUCT_BUNDLE_IDENTIFIER: String? = nil,
413+
PRODUCT_MODULE_NAME: String? = nil,
414+
PRODUCT_NAME: String? = nil,
415+
PROJECT_NAME: String? = nil,
416+
SDKROOT: String? = nil,
417+
SKIP_INSTALL: String? = nil,
418+
SUPPORTED_PLATFORMS: [String]? = nil,
419+
SWIFT_ACTIVE_COMPILATION_CONDITIONS: [String]? = nil,
420+
SWIFT_FORCE_STATIC_LINK_STDLIB: String? = nil,
421+
SWIFT_FORCE_DYNAMIC_LINK_STDLIB: String? = nil,
422+
SWIFT_OPTIMIZATION_LEVEL: String? = nil,
423+
SWIFT_VERSION: String? = nil,
424+
TARGET_NAME: String? = nil,
425+
USE_HEADERMAP: String? = nil,
426+
LD: String? = nil
427+
) {
379428
self.CLANG_CXX_LANGUAGE_STANDARD = CLANG_CXX_LANGUAGE_STANDARD
429+
self.CLANG_ENABLE_MODULES = CLANG_ENABLE_MODULES
380430
self.CLANG_ENABLE_OBJC_ARC = CLANG_CXX_LANGUAGE_STANDARD
381431
self.COMBINE_HIDPI_IMAGES = COMBINE_HIDPI_IMAGES
382432
self.COPY_PHASE_STRIP = COPY_PHASE_STRIP

Sources/Xcodeproj/XcodeProjectModelSerialization.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,12 @@ extension Xcode.BuildFile: PropertyListSerializable {
286286
if let fileRef = fileRef {
287287
dict["fileRef"] = .identifier(serializer.id(of: fileRef))
288288
}
289+
290+
let settingsDict = settings.asPropertyList()
291+
if !settingsDict.isEmpty {
292+
dict["settings"] = settingsDict
293+
}
294+
289295
return dict
290296
}
291297
}
@@ -359,8 +365,11 @@ extension Xcode.BuildSettingsTable: PropertyListSerializable {
359365
}
360366
}
361367

362-
extension Xcode.BuildSettingsTable.BuildSettings {
368+
protocol PropertyListDictionaryConvertible {
369+
func asPropertyList() -> PropertyList
370+
}
363371

372+
extension PropertyListDictionaryConvertible {
364373
/// Returns a property list representation of the build settings, in which
365374
/// every struct field is represented as a dictionary entry. Fields of
366375
/// type `String` are represented as `PropertyList.string` values; fields
@@ -410,6 +419,9 @@ extension Xcode.BuildSettingsTable.BuildSettings {
410419
}
411420
}
412421

422+
extension Xcode.BuildFile.Settings: PropertyListDictionaryConvertible {}
423+
extension Xcode.BuildSettingsTable.BuildSettings: PropertyListDictionaryConvertible {}
424+
413425
/// Private helper function that combines a base property list and an overlay
414426
/// property list, respecting the semantics of `$(inherited)` as we go.
415427
/// FIXME: This should possibly be done while constructing the property list.
@@ -566,3 +578,13 @@ extension PropertyListSerializable {
566578
}
567579
}
568580

581+
extension PropertyList {
582+
var isEmpty: Bool {
583+
switch self {
584+
case let .identifier(string): return string.isEmpty
585+
case let .string(string): return string.isEmpty
586+
case let .array(array): return array.isEmpty
587+
case let .dictionary(dictionary): return dictionary.isEmpty
588+
}
589+
}
590+
}

Sources/Xcodeproj/pbxproj().swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ func xcodeProject(
424424
targetSettings.common.INFOPLIST_FILE = infoPlistFilePath.relative(to: sourceRootDir).asString
425425

426426
if target.type == .test {
427+
targetSettings.common.CLANG_ENABLE_MODULES = "YES"
427428
targetSettings.common.EMBEDDED_CONTENT_CONTAINS_SWIFT = "YES"
428429
targetSettings.common.LD_RUNPATH_SEARCH_PATHS = ["$(inherited)", "@loader_path/../Frameworks", "@loader_path/Frameworks"]
429430
} else {
@@ -547,16 +548,27 @@ func xcodeProject(
547548
// Disable defines target for clang target because our clang targets are not proper framework targets.
548549
// Also see: <rdar://problem/29825757>
549550
targetSettings.common.DEFINES_MODULE = "NO"
551+
550552
// Generate a modulemap for clangTarget (if not provided by user) and
551553
// add to the build settings.
552-
let moduleMapPath: AbsolutePath
554+
var moduleMapPath: AbsolutePath?
553555

554556
// If the modulemap is generated (as opposed to user provided).
555-
let isGenerated: Bool
557+
var isGenerated = false
558+
556559
// If user provided the modulemap no need to generate.
557560
if fileSystem.isFile(clangTarget.moduleMapPath) {
558561
moduleMapPath = clangTarget.moduleMapPath
559-
isGenerated = false
562+
} else if includeGroup.subitems.contains(where: { $0.path == clangTarget.c99name + ".h" }) {
563+
// If an umbrella header exists, enable Xcode's builtin module's feature rather than generating
564+
// a custom module map. This increases the compatibility of generated Xcode projects.
565+
let headerPhase = xcodeTarget.addHeadersBuildPhase()
566+
for case let header as Xcode.FileReference in includeGroup.subitems {
567+
let buildFile = headerPhase.addBuildFile(fileRef: header)
568+
buildFile.settings.ATTRIBUTES = ["Public"]
569+
}
570+
targetSettings.common.CLANG_ENABLE_MODULES = "YES"
571+
targetSettings.common.DEFINES_MODULE = "YES"
560572
} else {
561573
// Generate and drop the modulemap inside Xcodeproj folder.
562574
let path = xcodeprojPath.appending(components: "GeneratedModuleMap", clangTarget.c99name)
@@ -565,9 +577,12 @@ func xcodeProject(
565577
moduleMapPath = path.appending(component: moduleMapFilename)
566578
isGenerated = true
567579
}
568-
includeGroup.addFileReference(path: moduleMapPath.asString, name: moduleMapPath.basename)
569-
// Save this modulemap path mapped to target so we can later wire it up for its dependees.
570-
modulesToModuleMap[target] = (moduleMapPath, isGenerated)
580+
581+
if let moduleMapPath = moduleMapPath {
582+
includeGroup.addFileReference(path: moduleMapPath.asString, name: moduleMapPath.basename)
583+
// Save this modulemap path mapped to target so we can later wire it up for its dependees.
584+
modulesToModuleMap[target] = (moduleMapPath, isGenerated)
585+
}
571586
}
572587
}
573588

Tests/XcodeprojTests/PackageGraphTests.swift

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class PackageGraphTests: XCTestCase {
2727
"/Bar/Sources/Sea2/include/Sea2.h",
2828
"/Bar/Sources/Sea2/include/module.modulemap",
2929
"/Bar/Sources/Sea2/Sea2.c",
30+
"/Bar/Sources/Sea3/include/header.h",
31+
"/Bar/Sources/Sea3/Sea3.c",
3032
"/Bar/Tests/BarTests/barTests.swift",
3133
"/Overrides.xcconfig"
3234
)
@@ -47,17 +49,20 @@ class PackageGraphTests: XCTestCase {
4749
result.check(references:
4850
"Package.swift",
4951
"Configs/Overrides.xcconfig",
52+
"Sources/Sea3/Sea3.c",
53+
"Sources/Sea3/include/header.h",
54+
"Sources/Sea3/include/module.modulemap",
5055
"Sources/Sea2/Sea2.c",
5156
"Sources/Sea2/include/Sea2.h",
5257
"Sources/Sea2/include/module.modulemap",
5358
"Sources/Bar/bar.swift",
5459
"Sources/Sea/Sea.c",
5560
"Sources/Sea/include/Sea.h",
56-
"Sources/Sea/include/module.modulemap",
5761
"Tests/BarTests/barTests.swift",
5862
"Dependencies/Foo 1.0.0/Package.swift",
5963
"Dependencies/Foo 1.0.0/foo.swift",
6064
"Products/Foo.framework",
65+
"Products/Sea3.framework",
6166
"Products/Sea2.framework",
6267
"Products/Bar.framework",
6368
"Products/Sea.framework",
@@ -97,6 +102,8 @@ class PackageGraphTests: XCTestCase {
97102
result.check(target: "Sea") { targetResult in
98103
targetResult.check(productType: .framework)
99104
targetResult.check(dependencies: ["Foo"])
105+
XCTAssertEqual(targetResult.commonBuildSettings.CLANG_ENABLE_MODULES, "YES")
106+
XCTAssertEqual(targetResult.commonBuildSettings.DEFINES_MODULE, "YES")
100107
XCTAssertEqual(targetResult.commonBuildSettings.MODULEMAP_FILE, nil)
101108
XCTAssertEqual(targetResult.commonBuildSettings.OTHER_CFLAGS?.first, "$(inherited)")
102109
XCTAssertEqual(targetResult.commonBuildSettings.OTHER_LDFLAGS?.first, "$(inherited)")
@@ -108,6 +115,8 @@ class PackageGraphTests: XCTestCase {
108115
result.check(target: "Sea2") { targetResult in
109116
targetResult.check(productType: .framework)
110117
targetResult.check(dependencies: ["Foo"])
118+
XCTAssertNil(targetResult.commonBuildSettings.CLANG_ENABLE_MODULES)
119+
XCTAssertEqual(targetResult.commonBuildSettings.DEFINES_MODULE, "NO")
111120
XCTAssertEqual(targetResult.commonBuildSettings.MODULEMAP_FILE, nil)
112121
XCTAssertEqual(targetResult.commonBuildSettings.OTHER_CFLAGS?.first, "$(inherited)")
113122
XCTAssertEqual(targetResult.commonBuildSettings.OTHER_LDFLAGS?.first, "$(inherited)")
@@ -116,6 +125,13 @@ class PackageGraphTests: XCTestCase {
116125
XCTAssertEqual(targetResult.target.buildSettings.xcconfigFileRef?.path, "../Overrides.xcconfig")
117126
}
118127

128+
result.check(target: "Sea3") { targetResult in
129+
targetResult.check(productType: .framework)
130+
XCTAssertNil(targetResult.commonBuildSettings.CLANG_ENABLE_MODULES)
131+
XCTAssertEqual(targetResult.commonBuildSettings.DEFINES_MODULE, "NO")
132+
XCTAssertEqual(targetResult.commonBuildSettings.MODULEMAP_FILE, nil)
133+
}
134+
119135
result.check(target: "BarTests") { targetResult in
120136
targetResult.check(productType: .unitTest)
121137
targetResult.check(dependencies: ["Bar", "Foo"])
@@ -182,13 +198,11 @@ class PackageGraphTests: XCTestCase {
182198
XCTAssertEqual(targetResult.target.buildSettings.common.OTHER_SWIFT_FLAGS ?? [], [
183199
"$(inherited)", "-Xcc",
184200
"-fmodule-map-file=$(SRCROOT)/Sources/Sea2/include/module.modulemap",
185-
"-Xcc", "-fmodule-map-file=$(SRCROOT)/build/xcodeproj/GeneratedModuleMap/Sea/module.modulemap",
186201
])
187202
XCTAssertEqual(targetResult.target.buildSettings.common.HEADER_SEARCH_PATHS ?? [], [
188203
"$(inherited)",
189204
"$(SRCROOT)/Sources/Sea2/include",
190205
"$(SRCROOT)/Sources/Sea/include",
191-
"$(SRCROOT)/build/xcodeproj/GeneratedModuleMap/Sea"
192206
])
193207
}
194208
result.check(target: "Sea") { targetResult in

Tests/XcodeprojTests/XcodeProjectModelSerializationTests.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,47 @@ class XcodeProjectModelSerializationTests: XCTestCase {
141141
}
142142
XCTAssertEqual(activeCompilationConditions, activeCompilationConditionsValues)
143143
}
144+
145+
func testBuildFileSettingsSerialization() {
146+
147+
// Create build file settings.
148+
var buildFileSettings = Xcode.BuildFile.Settings()
149+
150+
let attributeValues = ["Public"]
151+
buildFileSettings.ATTRIBUTES = attributeValues
152+
153+
// Serialize it to a property list.
154+
let plist = buildFileSettings.asPropertyList()
155+
156+
// Assert things about plist
157+
guard case let .dictionary(buildFileSettingsDict) = plist else {
158+
XCTFail("build file settings plist must be a dictionary")
159+
return
160+
}
161+
162+
guard let attributesPlist = buildFileSettingsDict["ATTRIBUTES"] else {
163+
XCTFail("build file settings plist must contain ATTRIBUTES")
164+
return
165+
}
166+
167+
guard case let .array(attributePlists) = attributesPlist else {
168+
XCTFail("attributes plist must be an array")
169+
return
170+
}
171+
172+
let attributes = attributePlists.compactMap { attributePlist -> String? in
173+
guard case let .string(attribute) = attributePlist else {
174+
XCTFail("attribute plist must be a string")
175+
return nil
176+
}
177+
return attribute
178+
}
179+
XCTAssertEqual(attributes, attributeValues)
180+
}
144181

145182
static var allTests = [
146183
("testBasicProjectSerialization", testBasicProjectSerialization),
147184
("testBuildSettingsSerialization", testBuildSettingsSerialization),
185+
("testBuildFileSettingsSerialization", testBuildFileSettingsSerialization),
148186
]
149187
}

0 commit comments

Comments
 (0)