Skip to content

Commit 77d1134

Browse files
committed
Merge pull request #265 from aciidb0mb3r/pkg-updates
Refactor pkgconfig code
2 parents 16ddd05 + fb5e87b commit 77d1134

File tree

6 files changed

+256
-103
lines changed

6 files changed

+256
-103
lines changed

Sources/Build/Buildable.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ extension SwiftModule {
6767
}
6868

6969
do {
70-
var pkgConfig = try PkgConfig(name: pkgConfigName)
71-
try pkgConfig.load()
70+
let pkgConfig = try PkgConfig(name: pkgConfigName)
7271
return pkgConfig.cFlags.map{["-Xcc", $0]}.flatten() + pkgConfig.libs
7372
}
7473
catch PkgConfigError.CouldNotFindConfigFile {

Sources/Build/PkgConfig.swift

Lines changed: 145 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,48 @@ import func POSIX.getenv
1313

1414
enum PkgConfigError: ErrorProtocol {
1515
case CouldNotFindConfigFile
16+
case ParsingError(String)
1617
}
1718

1819
struct PkgConfig {
19-
static let searchPaths = ["/usr/local/lib/pkgconfig",
20+
private static let searchPaths = ["/usr/local/lib/pkgconfig",
2021
"/usr/local/share/pkgconfig",
2122
"/usr/lib/pkgconfig",
2223
"/usr/share/pkgconfig",
23-
// FIXME: These should only be searched for linux?
24-
"/usr/lib/x86_64-linux-gnu/pkgconfig",
25-
"/usr/local/lib/x86_64-linux-gnu/pkgconfig",
2624
]
27-
2825
let name: String
2926
let pcFile: String
30-
var cFlags = [String]()
31-
var libs = [String]()
32-
private var parser: PkgConfigParser
27+
let cFlags: [String]
28+
let libs: [String]
3329

3430
init(name: String) throws {
3531
self.name = name
3632
self.pcFile = try PkgConfig.locatePCFile(name: name)
37-
parser = PkgConfigParser(pcFile: pcFile)
33+
34+
var parser = PkgConfigParser(pcFile: pcFile)
35+
try parser.parse()
36+
37+
var cFlags = [String]()
38+
var libs = [String]()
39+
40+
// FIXME: handle spaces in paths.
41+
cFlags += parser.cFlags.characters.split(separator: " ").map(String.init)
42+
libs += parser.libs.characters.split(separator: " ").map(String.init)
43+
44+
// If parser found dependencies in pc file, get their flags too.
45+
if(!parser.dependencies.isEmpty) {
46+
for dep in parser.dependencies {
47+
let pkg = try PkgConfig(name: dep)
48+
cFlags += pkg.cFlags
49+
libs += pkg.libs
50+
}
51+
}
52+
53+
self.cFlags = cFlags
54+
self.libs = libs
3855
}
3956

40-
static var envSearchPaths: [String] {
57+
private static var envSearchPaths: [String] {
4158
if let configPath = getenv("PKG_CONFIG_PATH") {
4259
return configPath.characters.split(separator: ":").map(String.init)
4360
}
@@ -53,106 +70,153 @@ struct PkgConfig {
5370
}
5471
throw PkgConfigError.CouldNotFindConfigFile
5572
}
56-
57-
mutating func load() throws {
58-
cFlags = [String]()
59-
libs = [String]()
60-
try parser.parse()
61-
if let cFlags = parser.cFlags {
62-
// FIXME: handle spaces in paths.
63-
self.cFlags += cFlags.characters.split(separator: " ").map(String.init)
64-
}
65-
if let libs = parser.libs {
66-
// FIXME: handle spaces in paths.
67-
self.libs += libs.characters.split(separator: " ").map(String.init)
68-
}
69-
70-
if(parser.dependencies.isEmpty) {
71-
return
72-
}
73-
74-
for dep in parser.dependencies {
75-
var pkg = try PkgConfig(name: dep)
76-
try pkg.load()
77-
self.cFlags += pkg.cFlags
78-
self.libs += pkg.libs
79-
}
80-
}
8173
}
8274

83-
private struct PkgConfigParser {
84-
let pcFile: String
85-
var variables = [String: String]()
75+
struct PkgConfigParser {
76+
private let pcFile: String
77+
private(set) var variables = [String: String]()
8678
var dependencies = [String]()
87-
var cFlags: String?
88-
var libs: String?
89-
90-
enum Token {
91-
case CFlats(String)
92-
case Libs(String)
93-
case Dependencies(String)
94-
case Variable(name: String, value: String)
95-
}
79+
var cFlags = ""
80+
var libs = ""
9681

9782
init(pcFile: String) {
83+
precondition(pcFile.isFile)
9884
self.pcFile = pcFile
9985
}
10086

10187
mutating func parse() throws {
88+
89+
func removeComment(line: String) -> String {
90+
if let commentIndex = line.characters.index(of: "#") {
91+
return line[line.characters.startIndex..<commentIndex]
92+
}
93+
return line
94+
}
95+
10296
let file = File(path: self.pcFile)
10397
for line in try file.enumerate() {
104-
if !line.characters.contains(":") && line.characters.contains("=") {
105-
let equalsIndex = line.characters.index(of: "=")!
98+
// Ignore any commented line.
99+
if line.hasPrefix("#") || line.isEmpty { continue }
100+
// Remove any trailing comment from the line.
101+
let line = removeComment(line: line)
102+
103+
if let colonIndex = line.characters.index(of: ":") where line[colonIndex.successor()] == " " {
104+
// Found a key-value pair.
105+
try parseKeyValue(line: line)
106+
} else if let equalsIndex = line.characters.index(of: "=") {
107+
// Found a variable.
106108
let name = line[line.startIndex..<equalsIndex]
107109
let value = line[equalsIndex.successor()..<line.endIndex]
108-
variables[name] = resolveVariables(value)
109-
} else if line.hasPrefix("Requires: ") {
110-
dependencies = parseDependencies(value(line: line))
111-
} else if line.hasPrefix("Libs: ") {
112-
libs = resolveVariables(value(line: line)).chomp()
113-
} else if line.hasPrefix("Cflags: ") {
114-
cFlags = resolveVariables( value(line: line)).chomp()
110+
variables[name] = try resolveVariables(value)
111+
} else {
112+
// Unexpected thing in the pc file, abort.
113+
throw PkgConfigError.ParsingError("Unexpecting line: \(line) in \(pcFile)")
115114
}
116115
}
117116
}
118117

119-
func parseDependencies(_ depString: String) -> [String] {
120-
let exploded = depString.characters.split(separator: " ").map(String.init)
118+
private mutating func parseKeyValue(line: String) throws {
119+
if line.hasPrefix("Requires: ") {
120+
dependencies = try parseDependencies(value(line: line))
121+
} else if line.hasPrefix("Libs: ") {
122+
libs = try resolveVariables(value(line: line)).chomp()
123+
} else if line.hasPrefix("Cflags: ") {
124+
cFlags = try resolveVariables(value(line: line)).chomp()
125+
}
126+
}
127+
128+
/// Parses `Requires: ` string into array of dependencies.
129+
/// The dependecy string has seperator which can be (multiple) space or a comma.
130+
/// Additionally each there can be an optional version constaint to a dependency.
131+
private func parseDependencies(_ depString: String) throws -> [String] {
121132
let operators = ["=", "<", ">", "<=", ">="]
122-
var deps = [String]()
123-
var skipNext = false
124-
for depString in exploded {
125-
if skipNext {
126-
skipNext = false
127-
continue
133+
let seperators = [" ", ","]
134+
135+
// Look at a char at an index if present.
136+
func peek(idx: Int) -> Character? {
137+
guard idx <= depString.characters.count - 1 else { return nil }
138+
return depString.characters[depString.characters.startIndex.advanced(by: idx)]
139+
}
140+
141+
// This converts the string which can be seperated by comma or spaces
142+
// into an array of string.
143+
func tokenize() -> [String] {
144+
var tokens = [String]()
145+
var token = ""
146+
for (idx, char) in depString.characters.enumerated() {
147+
// Encountered a seperator, use the token.
148+
if seperators.contains(String(char)) {
149+
// If next character is a space skip.
150+
if let peeked = peek(idx: idx+1) where peeked == " " { continue }
151+
// Append to array of tokens and reset token variable.
152+
tokens.append(token)
153+
token = ""
154+
} else {
155+
token += String(char)
156+
}
128157
}
129-
if operators.contains(depString) {
130-
skipNext = true
158+
// Append the last collected token if present.
159+
if !token.isEmpty { tokens += [token] }
160+
return tokens
161+
}
162+
163+
var deps = [String]()
164+
var it = tokenize().makeIterator()
165+
while let arg = it.next() {
166+
// If we encounter an operator then we need to skip the next token.
167+
if operators.contains(arg) {
168+
// We should have a version number next, skip.
169+
guard let _ = it.next() else {
170+
throw PkgConfigError.ParsingError("Expected version number after \(deps.last) \(arg) in \"\(depString)\" in \(pcFile)")
171+
}
131172
} else {
132-
deps.append(depString)
173+
// Otherwise it is a dependency.
174+
deps.append(arg)
133175
}
134176
}
135177
return deps
136178
}
137179

138-
func resolveVariables(_ line: String) -> String {
139-
func resolve(_ string: String) -> String {
140-
var resolvedString = string
141-
guard let dollar = resolvedString.characters.index(of: "$") else { return string }
142-
guard let variableEndIndex = resolvedString.characters.index(of: "}") else {return string }
143-
let variable = string[dollar.successor().successor()..<variableEndIndex]
144-
let value = variables[variable]!
145-
resolvedString = resolvedString[resolvedString.startIndex..<dollar] + value + resolvedString[variableEndIndex.successor()..<resolvedString.endIndex]
146-
return resolvedString
180+
/// Perform variable expansion on the line by processing the each fragment of the string until complete.
181+
/// Variables occur in form of ${variableName}, we search for a variable linearly
182+
/// in the string and if found, lookup the value of the variable in our dictionary and
183+
/// replace the variable name with its value.
184+
private func resolveVariables(_ line: String) throws -> String {
185+
typealias StringIndex = String.CharacterView.Index
186+
187+
// Returns variable name, start index and end index of a variable in a string if present.
188+
// We make sure it of form ${name} otherwise it is not a variable.
189+
func findVariable(_ fragment: String) -> (name: String, startIndex: StringIndex, endIndex: StringIndex)? {
190+
guard let dollar = fragment.characters.index(of: "$") else { return nil }
191+
guard dollar != fragment.endIndex && fragment.characters[dollar.successor()] == "{" else { return nil }
192+
guard let variableEndIndex = fragment.characters.index(of: "}") else { return nil }
193+
return (fragment[dollar.successor().successor()..<variableEndIndex], dollar, variableEndIndex)
147194
}
148-
var resolved = line
149-
while resolved.characters.contains("$") {
150-
resolved = resolve(resolved)
195+
196+
var result = ""
197+
var fragment = line
198+
while !fragment.isEmpty {
199+
// Look for a variable in our current fragment.
200+
if let variable = findVariable(fragment) {
201+
// Append the contents before the variable.
202+
result += fragment[fragment.characters.startIndex..<variable.startIndex]
203+
guard let variableValue = variables[variable.name] else {
204+
throw PkgConfigError.ParsingError("Expected variable in \(pcFile)")
205+
}
206+
// Append the value of the variable.
207+
result += variableValue
208+
// Update the fragment with post variable string.
209+
fragment = fragment[variable.endIndex.successor()..<fragment.characters.endIndex]
210+
} else {
211+
// No variable found, just append rest of the fragment to result.
212+
result += fragment
213+
fragment = ""
214+
}
151215
}
152-
return resolved
216+
return result
153217
}
154218

155-
func value(line: String) -> String {
219+
private func value(line: String) -> String {
156220
guard let colonIndex = line.characters.index(of: ":") else {
157221
return ""
158222
}

Sources/Build/misc.swift

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -188,29 +188,23 @@ extension SystemPackageProvider {
188188
}
189189
}
190190

191-
static func providerForCurrentPlatform(providers: [SystemPackageProvider]) -> SystemPackageProvider? {
192-
guard let uname = try? popen(["uname"]).chomp().lowercased() else { return nil }
193-
switch uname {
194-
case "darwin":
195-
for provider in providers {
196-
if case .Brew = provider {
197-
return provider
198-
}
191+
var isAvailable: Bool {
192+
guard let platform = Platform.currentPlatform() else { return false }
193+
switch self {
194+
case .Brew(_):
195+
if case .Darwin = platform {
196+
return true
199197
}
200-
case "linux":
201-
if "/etc/debian_version".isFile {
202-
for provider in providers {
203-
if case .Apt = provider {
204-
return provider
205-
}
206-
}
198+
case .Apt(_):
199+
if case .Linux(.Debian) = platform {
200+
return true
207201
}
208-
break
209-
210-
default:
211-
return nil
212202
}
213-
return nil
203+
return false
204+
}
205+
206+
static func providerForCurrentPlatform(providers: [SystemPackageProvider]) -> SystemPackageProvider? {
207+
return providers.filter{ $0.isAvailable }.first
214208
}
215209
}
216210

Sources/Utility/Platform.swift

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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 func POSIX.popen
12+
13+
public enum Platform {
14+
case Darwin
15+
case Linux(LinuxFlavor)
16+
17+
public enum LinuxFlavor {
18+
case Debian
19+
}
20+
21+
public static func currentPlatform() -> Platform? {
22+
guard let uname = try? popen(["uname"]).chomp().lowercased() else { return nil }
23+
switch uname {
24+
case "darwin":
25+
return .Darwin
26+
case "linux":
27+
if "/etc/debian_version".isFile {
28+
return .Linux(.Debian)
29+
}
30+
default:
31+
return nil
32+
}
33+
return nil
34+
}
35+
}

0 commit comments

Comments
 (0)