Skip to content

Commit 5f71a12

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

File tree

4 files changed

+132
-24
lines changed

4 files changed

+132
-24
lines changed

Sources/Xcodeproj/Module+PBXProj.swift

Lines changed: 37 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
var headerPaths = dependencies.flatMap { module -> AbsolutePath? in
161161
switch module {
@@ -174,36 +174,28 @@ 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, .array(headerPaths.map { .string($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 Plist.dictionary(buildSettings).quoteStrings().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 Plist.dictionary(buildSettings).quoteStrings().serialize()
203195
}
204196

205-
private func getCommonBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> [String: String] {
206-
var buildSettings = [String: String]()
197+
private func getCommonBuildSettings(_ options: XcodeprojOptions, xcodeProjectPath: AbsolutePath) throws -> [String: Plist] {
198+
var buildSettings = [String: Plist]()
207199
let plistPath = xcodeProjectPath.appending(component: infoPlistFileName)
208200

209201
if isTest {
@@ -212,15 +204,15 @@ extension Module {
212204
//FIXME this should not be required
213205
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = "@loader_path/../Frameworks"
214206

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

@@ -239,12 +231,13 @@ extension Module {
239231
// default behavior on all packages.
240232

241233
buildSettings["PRODUCT_NAME"] = "$(TARGET_NAME:c99extidentifier)"
242-
buildSettings["INFOPLIST_FILE"] = plistPath.relative(to: xcodeProjectPath.parentDirectory).asString
234+
buildSettings["INFOPLIST_FILE"] = .string(plistPath.relative(to: xcodeProjectPath.parentDirectory).asString)
243235

244236
buildSettings["PRODUCT_MODULE_NAME"] = "$(TARGET_NAME:c99extidentifier)"
245237

246238
// FIXME: This should be user speficiable
247-
buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = c99name
239+
buildSettings["PRODUCT_BUNDLE_IDENTIFIER"] = .string(c99name)
240+
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = .string(toolchainPath)
248241
} else {
249242
// override default behavior, instead link dynamically
250243
buildSettings["SWIFT_FORCE_STATIC_LINK_STDLIB"] = "NO"
@@ -259,13 +252,13 @@ extension Module {
259252
// example would be `@executable_path/../lib` but there are
260253
// other problems to solve first, e.g. how to deal with the
261254
// Swift standard library paths).
262-
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = buildSettings["LD_RUNPATH_SEARCH_PATHS"]! + " @executable_path"
255+
buildSettings["LD_RUNPATH_SEARCH_PATHS"] = .array([.string(toolchainPath), "@executable_path"])
263256
}
264257
}
265258

266259
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: " ")
260+
buildSettings["OTHER_LDFLAGS"] = .array(["$(inherited)"] + pkgArgs.libs.map(Plist.string))
261+
buildSettings["OTHER_SWIFT_FLAGS"] = .array(["$(inherited)"] + pkgArgs.cFlags.map(Plist.string))
269262
}
270263

271264
// Add framework search path to build settings.
@@ -286,7 +279,7 @@ extension Module {
286279
moduleMapPath = path.appending(component: moduleMapFilename)
287280
}
288281

289-
buildSettings["MODULEMAP_FILE"] = moduleMapPath.relative(to: xcodeProjectPath.parentDirectory).asString
282+
buildSettings["MODULEMAP_FILE"] = .string(moduleMapPath.relative(to: xcodeProjectPath.parentDirectory).asString)
290283
}
291284

292285
// At the moment, set the Swift version to 3 (we will need to make this dynamic), but for now this is necessary.
@@ -298,3 +291,25 @@ extension Module {
298291
return buildSettings
299292
}
300293
}
294+
295+
extension Plist {
296+
// FIXME: This is only internal for unit testing.
297+
/// Quotes the strings in the Plist.
298+
func quoteStrings() -> Plist {
299+
switch self {
300+
case .string(let str):
301+
// Only quote if string has a space character.
302+
guard str.utf8.contains(UInt8(ascii: " ")) else {
303+
return .string(str)
304+
}
305+
return .string("\"" + str + "\"")
306+
case .array(let items):
307+
return .array(items.map { $0.quoteStrings() })
308+
case .dictionary(var items):
309+
for (k,v) in items {
310+
items[k] = v.quoteStrings()
311+
}
312+
return .dictionary(items)
313+
}
314+
}
315+
}

Sources/Xcodeproj/Plist.swift

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

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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
21+
func testBuildSettingStringQuoting() {
22+
XCTAssertEqual("\"\\\"hello world\\\"\"", Plist.string("hello world").quoteStrings().serialize())
23+
XCTAssertEqual("(\"\\\"hello world\\\"\", \"cool\")", Plist.array([.string("hello world"), .string("cool")]).quoteStrings().serialize())
24+
}
25+
}

0 commit comments

Comments
 (0)