@@ -13,31 +13,48 @@ import func POSIX.getenv
13
13
14
14
enum PkgConfigError : ErrorProtocol {
15
15
case CouldNotFindConfigFile
16
+ case ParsingError( String )
16
17
}
17
18
18
19
struct PkgConfig {
19
- static let searchPaths = [ " /usr/local/lib/pkgconfig " ,
20
+ private static let searchPaths = [ " /usr/local/lib/pkgconfig " ,
20
21
" /usr/local/share/pkgconfig " ,
21
22
" /usr/lib/pkgconfig " ,
22
23
" /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 " ,
26
24
]
27
-
28
25
let name : String
29
26
let pcFile : String
30
- var cFlags = [ String] ( )
31
- var libs = [ String] ( )
32
- private var parser : PkgConfigParser
27
+ let cFlags : [ String ]
28
+ let libs : [ String ]
33
29
34
30
init ( name: String ) throws {
35
31
self . name = name
36
32
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
38
55
}
39
56
40
- static var envSearchPaths : [ String ] {
57
+ private static var envSearchPaths : [ String ] {
41
58
if let configPath = getenv ( " PKG_CONFIG_PATH " ) {
42
59
return configPath. characters. split ( separator: " : " ) . map ( String . init)
43
60
}
@@ -53,106 +70,153 @@ struct PkgConfig {
53
70
}
54
71
throw PkgConfigError . CouldNotFindConfigFile
55
72
}
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
- }
81
73
}
82
74
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] ( )
86
78
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 = " "
96
81
97
82
init ( pcFile: String ) {
83
+ precondition ( pcFile. isFile)
98
84
self . pcFile = pcFile
99
85
}
100
86
101
87
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
+
102
96
let file = File ( path: self . pcFile)
103
97
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.
106
108
let name = line [ line. startIndex..< equalsIndex]
107
109
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) " )
115
114
}
116
115
}
117
116
}
118
117
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 ] {
121
132
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
+ }
128
157
}
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
+ }
131
172
} else {
132
- deps. append ( depString)
173
+ // Otherwise it is a dependency.
174
+ deps. append ( arg)
133
175
}
134
176
}
135
177
return deps
136
178
}
137
179
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)
147
194
}
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
+ }
151
215
}
152
- return resolved
216
+ return result
153
217
}
154
218
155
- func value( line: String ) -> String {
219
+ private func value( line: String ) -> String {
156
220
guard let colonIndex = line. characters. index ( of: " : " ) else {
157
221
return " "
158
222
}
0 commit comments