Skip to content

Commit b1f2f6c

Browse files
committed
[Xcodeproj] Add a Plist enum
Also, moves Build settings to use this new type respecting proper quoting rules. - <rdar://problem/28039632> SR-1754: Spaces should be escaped in HEADER_SEARCH_PATHS
1 parent d57de8f commit b1f2f6c

File tree

4 files changed

+119
-20
lines changed

4 files changed

+119
-20
lines changed

Sources/Xcodeproj/Module+PBXProj.swift

Lines changed: 30 additions & 18 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: Any)? {
159159
let headerPathKey = "HEADER_SEARCH_PATHS"
160160
var headerPaths = dependencies.flatMap { module -> AbsolutePath? in
161161
switch module {
@@ -174,36 +174,48 @@ extension Module {
174174
}
175175

176176
guard !headerPaths.isEmpty else { return nil }
177-
178-
if headerPaths.count == 1, let first = headerPaths.first {
179-
return (headerPathKey, first.asString)
180-
}
181-
182-
let headerPathValue = headerPaths.map{ $0.asString }.joined(separator: " ")
183177

184-
return (headerPathKey, headerPathValue)
178+
return (headerPathKey, headerPaths.map { $0.asString } )
185179
}
186180

187181
func getDebugBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> String {
188182
var buildSettings = try getCommonBuildSettings(options, xcodeProjectPath: xcodeProjectPath)
189183
if let headerSearchPaths = headerSearchPaths {
190184
buildSettings[headerSearchPaths.key] = headerSearchPaths.value
191185
}
192-
// FIXME: Need to honor actual quoting rules here.
193-
return buildSettings.map{ "\($0) = '\($1)';" }.joined(separator: " ")
186+
return toPlist(buildSettings).serialize()
194187
}
195188

196189
func getReleaseBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> String {
197190
var buildSettings = try getCommonBuildSettings(options, xcodeProjectPath: xcodeProjectPath)
198191
if let headerSearchPaths = headerSearchPaths {
199192
buildSettings[headerSearchPaths.key] = headerSearchPaths.value
200193
}
201-
// FIXME: Need to honor actual quoting rules here.
202-
return buildSettings.map{ "\($0) = '\($1)';" }.joined(separator: " ")
194+
return toPlist(buildSettings).serialize()
195+
}
196+
197+
/// Converts build settings dictionary to a Plist object.
198+
///
199+
/// Adds string values in dictionaries as is and array values are quoted and then converted
200+
/// to a string joined by whitespace.
201+
private func toPlist(_ buildSettings: [String: Any]) -> Plist {
202+
var buildSettingsPlist = [String: Plist]()
203+
for (k, v) in buildSettings {
204+
switch v {
205+
case let value as String:
206+
buildSettingsPlist[k] = .string(value)
207+
case let value as [String]:
208+
let escaped = value.map { "\"" + Plist.escape(string: $0) + "\"" }.joined(separator: " ")
209+
buildSettingsPlist[k] = .string(escaped)
210+
default:
211+
fatalError("build setting dictionary should only contain String or [String]")
212+
}
213+
}
214+
return .dictionary(buildSettingsPlist)
203215
}
204216

205-
private func getCommonBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> [String: String] {
206-
var buildSettings = [String: String]()
217+
private func getCommonBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> [String: Any] {
218+
var buildSettings = [String: Any]()
207219
let plistPath = xcodeProjectPath.appending(component: infoPlistFileName)
208220

209221
if isTest {
@@ -220,7 +232,7 @@ extension Module {
220232
//
221233
// This means the built binaries are not suitable for distribution,
222234
// among other things.
223-
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"
235+
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = ["$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"]
224236
if isLibrary {
225237
buildSettings["ENABLE_TESTABILITY"] = "YES"
226238

@@ -259,13 +271,13 @@ extension Module {
259271
// example would be `@executable_path/../lib` but there are
260272
// other problems to solve first, e.g. how to deal with the
261273
// Swift standard library paths).
262-
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = buildSettings["LD_RUNPATH_SEARCH_PATHS"]! + " @executable_path"
274+
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = buildSettings["LD_RUNPATH_SEARCH_PATHS"] as! [String] + ["@executable_path"]
263275
}
264276
}
265277

266278
if let pkgArgs = try? self.pkgConfigArgs() {
267-
buildSettings["OTHER_LDFLAGS"] = (["$(inherited)"] + pkgArgs.libs).joined(separator: " ")
268-
buildSettings["OTHER_SWIFT_FLAGS"] = (["$(inherited)"] + pkgArgs.cFlags).joined(separator: " ")
279+
buildSettings["OTHER_LDFLAGS"] = ["$(inherited)"] + pkgArgs.libs
280+
buildSettings["OTHER_SWIFT_FLAGS"] = ["$(inherited)"] + pkgArgs.cFlags
269281
}
270282

271283
// Add framework search path to build settings.

Sources/Xcodeproj/Plist.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
extension Plist {
35+
/// Serializes the Plist enum to string.
36+
func serialize() -> String {
37+
switch self {
38+
case .string(let str):
39+
return "\"" + Plist.escape(string: str) + "\""
40+
case .array(let items):
41+
return "(" + items.map { $0.serialize() }.joined(separator: ", ") + ")"
42+
case .dictionary(let items):
43+
return "{" + items.map { " \($0) = \($1.serialize()) " }.joined(separator: "; ") + "; };"
44+
}
45+
}
46+
47+
/// Escapes the string for plist.
48+
/// Finds the instances of quote (") and backward slash (\) and prepends
49+
/// the escape character backward slash (\).
50+
static func escape(string: String) -> String {
51+
func needsEscape(_ char: UInt8) -> Bool {
52+
return char == UInt8(ascii: "\\") || char == UInt8(ascii: "\"")
53+
}
54+
55+
guard let pos = string.utf8.index(where: needsEscape) else {
56+
return string
57+
}
58+
var newString = String(string.utf8[string.utf8.startIndex..<pos])!
59+
for char in string.utf8[pos..<string.utf8.endIndex] {
60+
if needsEscape(char) {
61+
newString += "\\"
62+
}
63+
newString += String(UnicodeScalar(char))
64+
}
65+
return newString
66+
}
67+
}

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)