Skip to content

Commit 1aa1050

Browse files
committed
[Xcodeproj] Add a Plist enum
Also, moves Build settings to use this new type respecting proper quoting rules.
1 parent edfa624 commit 1aa1050

File tree

4 files changed

+87
-24
lines changed

4 files changed

+87
-24
lines changed

Sources/Xcodeproj/Module+PBXProj.swift

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ extension Module {
155155
}
156156
}
157157

158-
var headerSearchPaths: (key: String, value: String)? {
158+
var headerSearchPaths: (key: String, value: Plist)? {
159159
let headerPathKey = "HEADER_SEARCH_PATHS"
160160
let headerPaths = dependencies.flatMap { module -> AbsolutePath? in
161161
switch module {
@@ -169,36 +169,28 @@ extension Module {
169169
}
170170

171171
guard !headerPaths.isEmpty else { return nil }
172-
173-
if headerPaths.count == 1, let first = headerPaths.first {
174-
return (headerPathKey, first.asString)
175-
}
176-
177-
let headerPathValue = headerPaths.map{ $0.asString }.joined(separator: " ")
178172

179-
return (headerPathKey, headerPathValue)
173+
return (headerPathKey, .array(headerPaths.map { .string($0.asString) }) )
180174
}
181175

182176
func getDebugBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> String {
183177
var buildSettings = try getCommonBuildSettings(options, xcodeProjectPath: xcodeProjectPath)
184178
if let headerSearchPaths = headerSearchPaths {
185179
buildSettings[headerSearchPaths.key] = headerSearchPaths.value
186180
}
187-
// FIXME: Need to honor actual quoting rules here.
188-
return buildSettings.map{ "\($0) = '\($1)';" }.joined(separator: " ")
181+
return Plist.dictionary(buildSettings).serialize()
189182
}
190183

191184
func getReleaseBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> String {
192185
var buildSettings = try getCommonBuildSettings(options, xcodeProjectPath: xcodeProjectPath)
193186
if let headerSearchPaths = headerSearchPaths {
194187
buildSettings[headerSearchPaths.key] = headerSearchPaths.value
195188
}
196-
// FIXME: Need to honor actual quoting rules here.
197-
return buildSettings.map{ "\($0) = '\($1)';" }.joined(separator: " ")
189+
return Plist.dictionary(buildSettings).serialize()
198190
}
199191

200-
private func getCommonBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> [String: String] {
201-
var buildSettings = [String: String]()
192+
private func getCommonBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> [String: Plist] {
193+
var buildSettings = [String: Plist]()
202194
let plistPath = xcodeProjectPath.appending(component: infoPlistFileName)
203195

204196
if isTest {
@@ -207,15 +199,15 @@ extension Module {
207199
//FIXME this should not be required
208200
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = "@loader_path/../Frameworks"
209201

210-
buildSettings["INFOPLIST_FILE"] = plistPath.relative(to: xcodeProjectPath.parentDirectory).asString
202+
buildSettings["INFOPLIST_FILE"] = .string(plistPath.relative(to: xcodeProjectPath.parentDirectory).asString)
211203
} else {
212204
// We currently force a search path to the toolchain, since we
213205
// cannot establish an expected location for the Swift standard
214206
// libraries.
215207
//
216208
// This means the built binaries are not suitable for distribution,
217209
// among other things.
218-
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"
210+
let toolchainPath = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"
219211
if isLibrary {
220212
buildSettings["ENABLE_TESTABILITY"] = "YES"
221213

@@ -234,12 +226,13 @@ extension Module {
234226
// default behavior on all packages.
235227

236228
buildSettings["PRODUCT_NAME"] = "$(TARGET_NAME:c99extidentifier)"
237-
buildSettings["INFOPLIST_FILE"] = plistPath.relative(to: xcodeProjectPath.parentDirectory).asString
229+
buildSettings["INFOPLIST_FILE"] = .string(plistPath.relative(to: xcodeProjectPath.parentDirectory).asString)
238230

239231
buildSettings["PRODUCT_MODULE_NAME"] = "$(TARGET_NAME:c99extidentifier)"
240232

241233
// FIXME: This should be user speficiable
242-
buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = c99name
234+
buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = .string(c99name)
235+
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = .string(toolchainPath)
243236
} else {
244237
// override default behavior, instead link dynamically
245238
buildSettings["SWIFT_FORCE_STATIC_LINK_STDLIB"] = "NO"
@@ -254,13 +247,13 @@ extension Module {
254247
// example would be `@executable_path/../lib` but there are
255248
// other problems to solve first, e.g. how to deal with the
256249
// Swift standard library paths).
257-
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = buildSettings["LD_RUNPATH_SEARCH_PATHS"]! + " @executable_path"
250+
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = .array([.string(toolchainPath), "@executable_path"])
258251
}
259252
}
260253

261254
if let pkgArgs = try? self.pkgConfigArgs() {
262-
buildSettings["OTHER_LDFLAGS"] = (["$(inherited)"] + pkgArgs.libs).joined(separator: " ")
263-
buildSettings["OTHER_SWIFT_FLAGS"] = (["$(inherited)"] + pkgArgs.cFlags).joined(separator: " ")
255+
buildSettings["OTHER_LDFLAGS"] = .array(["$(inherited)"] + pkgArgs.libs.map(Plist.string))
256+
buildSettings["OTHER_SWIFT_FLAGS"] = .array(["$(inherited)"] + pkgArgs.cFlags.map(Plist.string))
264257
}
265258

266259
// Add framework search path to build settings.
@@ -281,7 +274,7 @@ extension Module {
281274
moduleMapPath = path.appending(component: moduleMapFilename)
282275
}
283276

284-
buildSettings["MODULEMAP_FILE"] = moduleMapPath.relative(to: xcodeProjectPath.parentDirectory).asString
277+
buildSettings["MODULEMAP_FILE"] = .string(moduleMapPath.relative(to: xcodeProjectPath.parentDirectory).asString)
285278
}
286279

287280
// At the moment, set the Swift version to 3 (we will need to make this dynamic), but for now this is necessary.

Sources/Xcodeproj/Plist.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright 2015 - 2016 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
/// A enum representing data types for legacy Plist type.
12+
/// see: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/PropertyLists/OldStylePlists/OldStylePLists.html
13+
enum Plist {
14+
case string(String)
15+
case array([Plist])
16+
case dictionary([String: Plist])
17+
}
18+
19+
extension Plist: ExpressibleByStringLiteral {
20+
public typealias UnicodeScalarLiteralType = StringLiteralType
21+
public typealias ExtendedGraphemeClusterLiteralType = StringLiteralType
22+
23+
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
24+
self = .string(value)
25+
}
26+
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
27+
self = .string(value)
28+
}
29+
public init(stringLiteral value: StringLiteralType) {
30+
self = .string(value)
31+
}
32+
}
33+
34+
private let escapedQuote = "\\\""
35+
36+
extension Plist {
37+
/// Serializes the Plist enum to string.
38+
func serialize() -> String {
39+
switch self {
40+
case .string(let str):
41+
// If the string has spaces Xcode represents it as: `"\"some string\""`.
42+
let hasSpace = str.utf8.contains(UInt8(ascii: " "))
43+
return "\"" + (hasSpace ? escapedQuote : "") + str.escape(ascii: "\"") + (hasSpace ? escapedQuote : "") + "\""
44+
case .array(let items):
45+
return "(" + items.map { $0.serialize() }.joined(separator: ", ") + ")"
46+
case .dictionary(let items):
47+
return "{" + items.map { " \($0) = \($1.serialize()) " }.joined(separator: "; ") + "; };"
48+
}
49+
}
50+
}

Sources/Xcodeproj/pbxproj().swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,12 +234,12 @@ public func pbxproj(srcroot: AbsolutePath, projectRoot: AbsolutePath, xcodeprojP
234234
print(" };")
235235
print(" \(module.debugConfigurationReference) = {")
236236
print(" isa = XCBuildConfiguration;")
237-
print(" buildSettings = { \(try module.getDebugBuildSettings(options, xcodeProjectPath: xcodeprojPath)) };")
237+
print(" buildSettings = \(try module.getDebugBuildSettings(options, xcodeProjectPath: xcodeprojPath))")
238238
print(" name = Debug;")
239239
print(" };")
240240
print(" \(module.releaseConfigurationReference) = {")
241241
print(" isa = XCBuildConfiguration;")
242-
print(" buildSettings = { \(try module.getReleaseBuildSettings(options, xcodeProjectPath: xcodeprojPath)) };")
242+
print(" buildSettings = \(try module.getReleaseBuildSettings(options, xcodeProjectPath: xcodeprojPath))")
243243
print(" name = Release;")
244244
print(" };")
245245

Tests/XcodeprojTests/PlistTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright 2015 - 2016 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
@testable import Xcodeproj
13+
14+
class PlistTests: XCTestCase {
15+
func testBasics() {
16+
XCTAssertEqual("\"\\\"hello \\\" world\\\"\"", Plist.string("hello \" world").serialize())
17+
XCTAssertEqual("(\"\\\"hello world\\\"\", \"cool\")", Plist.array([.string("hello world"), .string("cool")]).serialize())
18+
XCTAssertEqual("{ user = \"cool\" ; polo = (\"\\\"hello \\\" world\\\"\", \"cool\") ; };", Plist.dictionary(["user": .string("cool"), "polo": Plist.array([.string("hello \" world"), .string("cool")])]).serialize())
19+
}
20+
}

0 commit comments

Comments
 (0)