Skip to content

Commit 25ad105

Browse files
authored
[6.0] pkgconfig: Apply PKG_CONFIG_SYSROOTDIR when generating paths (swiftlang#7472)
Cherry pick of swiftlang#7461 SwiftPM's pkg-config implementation sets the `pc_sysrootdir` variable, but most `.pc` files do not use this variable directly. Instead, they rely on the pkg-config tool rewriting the generated paths to include the sysroot prefix when necessary. SwiftPM does not do this, so it does not generate the correct compiler flags to use libraries from a sysroot. This problem was reported in issue swiftlang#7409 There are two major pkg-config implementations which handle sysroot differently: * `pkg-config` (the original https://pkg-config.freedesktop.org implementation) prepends sysroot after variable expansion, when it creates the compiler flag lists * `pkgconf` (the newer http://pkgconf.org implementation) prepends sysroot to variables when they are defined, so sysroot is included when they are expanded `pkg-config`'s method skips single character compiler flags, such as `-I` and `-L`, and has special cases for longer options. It does not handle spaces between the flags and their values properly, and prepends sysroot multiple times in some cases, such as when the .pc file uses the `sysroot_dir` variable directly or has been rewritten to hard-code the sysroot prefix. `pkgconf`'s method handles spaces correctly, although it also makes extra checks to ensure that sysroot is not applied more than once. In 2024 `pkg-config` is the more popular option according to Homebrew installation statistics, but the major Linux distributions have generally switched to `pkgconf`. We will use `pkgconf`'s method here as it seems more robust than `pkg-config`'s, and `pkgconf`'s greater popularity on Linux means libraries developed there may depend on the specific way it handles `.pc` files. SwiftPM will now apply the sysroot prefix to compiler flags, such as include (`-I`) and library (`-L`) search paths. This is a partial fix for swiftlang#7409. The sysroot prefix is only applied when the `PKG_CONFIG_SYSROOT_DIR` environment variable is set. A future commit could apply an appropriate sysroot automatically when the `--experimental-swift-sdk` flag is used. **Scope**: Limited to packages relying on system libraries that utilize pkg-config. **Risk**: Low, changes are isolated and the scope is limited to a small fraction of packages. **Testing**: Automated with new test cases. **Issues**: swiftlang#7409 **Reviewer**: @MaxDesiatov.
1 parent 9ede8a8 commit 25ad105

11 files changed

+206
-6
lines changed

Sources/PackageLoading/PkgConfig.swift

+90-4
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ public struct PkgConfig {
4747
name: String,
4848
additionalSearchPaths: [AbsolutePath]? = .none,
4949
brewPrefix: AbsolutePath? = .none,
50+
sysrootDir: AbsolutePath? = .none,
5051
fileSystem: FileSystem,
5152
observabilityScope: ObservabilityScope
5253
) throws {
5354
try self.init(
5455
name: name,
5556
additionalSearchPaths: additionalSearchPaths ?? [],
5657
brewPrefix: brewPrefix,
58+
sysrootDir: sysrootDir,
5759
loadingContext: LoadingContext(),
5860
fileSystem: fileSystem,
5961
observabilityScope: observabilityScope
@@ -64,6 +66,7 @@ public struct PkgConfig {
6466
name: String,
6567
additionalSearchPaths: [AbsolutePath],
6668
brewPrefix: AbsolutePath?,
69+
sysrootDir: AbsolutePath?,
6770
loadingContext: LoadingContext,
6871
fileSystem: FileSystem,
6972
observabilityScope: ObservabilityScope
@@ -85,7 +88,7 @@ public struct PkgConfig {
8588
)
8689
}
8790

88-
var parser = try PkgConfigParser(pcFile: pcFile, fileSystem: fileSystem)
91+
var parser = try PkgConfigParser(pcFile: pcFile, fileSystem: fileSystem, sysrootDir: ProcessEnv.block["PKG_CONFIG_SYSROOT_DIR"])
8992
try parser.parse()
9093

9194
func getFlags(from dependencies: [String]) throws -> (cFlags: [String], libs: [String]) {
@@ -103,6 +106,7 @@ public struct PkgConfig {
103106
name: dep,
104107
additionalSearchPaths: additionalSearchPaths,
105108
brewPrefix: brewPrefix,
109+
sysrootDir: sysrootDir,
106110
loadingContext: loadingContext,
107111
fileSystem: fileSystem,
108112
observabilityScope: observabilityScope
@@ -162,13 +166,93 @@ internal struct PkgConfigParser {
162166
public private(set) var privateDependencies = [String]()
163167
public private(set) var cFlags = [String]()
164168
public private(set) var libs = [String]()
169+
public private(set) var sysrootDir: String?
165170

166-
public init(pcFile: AbsolutePath, fileSystem: FileSystem) throws {
171+
public init(pcFile: AbsolutePath, fileSystem: FileSystem, sysrootDir: String?) throws {
167172
guard fileSystem.isFile(pcFile) else {
168173
throw StringError("invalid pcfile \(pcFile)")
169174
}
170175
self.pcFile = pcFile
171176
self.fileSystem = fileSystem
177+
self.sysrootDir = sysrootDir
178+
}
179+
180+
// Compress repeated path separators to one.
181+
private func compressPathSeparators(_ value: String) -> String {
182+
let components = value.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/")
183+
if value.hasPrefix("/") {
184+
return "/" + components
185+
} else {
186+
return components
187+
}
188+
}
189+
190+
// Trim duplicate sysroot prefixes, matching the approach of pkgconf
191+
private func trimDuplicateSysroot(_ value: String) -> String {
192+
// If sysroot has been applied more than once, remove the first instance.
193+
// pkgconf makes this check after variable expansion to handle rare .pc
194+
// files which expand ${pc_sysrootdir} directly:
195+
// https://github.com/pkgconf/pkgconf/issues/123
196+
//
197+
// For example:
198+
// /sysroot/sysroot/remainder -> /sysroot/remainder
199+
//
200+
// However, pkgconf's algorithm searches for an additional sysrootdir anywhere in
201+
// the string after the initial prefix, rather than looking for two sysrootdir prefixes
202+
// directly next to each other:
203+
//
204+
// /sysroot/filler/sysroot/remainder -> /filler/sysroot/remainder
205+
//
206+
// It might seem more logical not to strip sysroot in this case, as it is not a double
207+
// prefix, but for compatibility trimDuplicateSysroot is faithful to pkgconf's approach
208+
// in the functions `pkgconf_tuple_parse` and `should_rewrite_sysroot`.
209+
210+
// Only trim if sysroot is defined with a meaningful value
211+
guard let sysrootDir, sysrootDir != "/" else {
212+
return value
213+
}
214+
215+
// Only trim absolute paths starting with sysroot
216+
guard value.hasPrefix("/"), value.hasPrefix(sysrootDir) else {
217+
return value
218+
}
219+
220+
// If sysroot appears multiple times, trim the prefix
221+
// N.B. sysroot can appear anywhere in the remainder
222+
// of the value, mirroring pkgconf's logic
223+
let pathSuffix = value.dropFirst(sysrootDir.count)
224+
if pathSuffix.contains(sysrootDir) {
225+
return String(pathSuffix)
226+
} else {
227+
return value
228+
}
229+
}
230+
231+
// Apply sysroot to generated paths, matching the approach of pkgconf
232+
private func applySysroot(_ value: String) -> String {
233+
// The two main pkg-config implementations handle sysroot differently:
234+
//
235+
// `pkg-config` (freedesktop.org) prepends sysroot after variable expansion, when in creates the compiler flag lists
236+
// `pkgconf` prepends sysroot to variables when they are defined, so sysroot is included when they are expanded
237+
//
238+
// pkg-config's method skips single character compiler flags, such as '-I' and '-L', and has special cases for longer options.
239+
// It does not handle spaces between the flags and their values properly, and prepends sysroot multiple times in some cases,
240+
// such as when the .pc file uses the sysroot_dir variable directly or has been rewritten to hard-code the sysroot prefix.
241+
//
242+
// pkgconf's method handles spaces correctly, although it also requires extra checks to ensure that sysroot is not applied
243+
// more than once.
244+
//
245+
// In 2024 pkg-config is the more popular option according to Homebrew installation statistics, but the major Linux distributions
246+
// have generally switched to pkgconf.
247+
//
248+
// We will use pkgconf's method here as it seems more robust than pkg-config's, and pkgconf's greater popularity on Linux
249+
// means libraries developed there may depend on the specific way it handles .pc files.
250+
251+
if value.hasPrefix("/"), let sysrootDir, !value.hasPrefix(sysrootDir) {
252+
return compressPathSeparators(trimDuplicateSysroot(sysrootDir + value))
253+
} else {
254+
return compressPathSeparators(trimDuplicateSysroot(value))
255+
}
172256
}
173257

174258
public mutating func parse() throws {
@@ -183,7 +267,9 @@ internal struct PkgConfigParser {
183267
variables["pcfiledir"] = pcFile.parentDirectory.pathString
184268

185269
// Add pc_sysrootdir variable. This is the path of the sysroot directory for pc files.
186-
variables["pc_sysrootdir"] = ProcessEnv.block["PKG_CONFIG_SYSROOT_DIR"] ?? AbsolutePath.root.pathString
270+
// pkgconf does not define pc_sysrootdir if the path of the .pc file is outside sysrootdir.
271+
// SwiftPM does not currently make that check.
272+
variables["pc_sysrootdir"] = sysrootDir ?? AbsolutePath.root.pathString
187273

188274
let fileContents: String = try fileSystem.readFileContents(pcFile)
189275
for line in fileContents.components(separatedBy: "\n") {
@@ -199,7 +285,7 @@ internal struct PkgConfigParser {
199285
// Found a variable.
200286
let (name, maybeValue) = line.spm_split(around: "=")
201287
let value = maybeValue?.spm_chuzzle() ?? ""
202-
variables[name.spm_chuzzle() ?? ""] = try resolveVariables(value)
288+
variables[name.spm_chuzzle() ?? ""] = try applySysroot(resolveVariables(value))
203289
} else {
204290
// Unexpected thing in the pc file, abort.
205291
throw PkgConfigError.parsingError("Unexpected line: \(line) in \(pcFile)")

Tests/PackageLoadingTests/PkgConfigParserTests.swift

+82-2
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,92 @@ final class PkgConfigParserTests: XCTestCase {
244244
}
245245
}
246246

247+
func testSysrootDir() throws {
248+
// sysroot should be prepended to all path variables, and should therefore appear in cflags and libs.
249+
try loadPCFile("gtk+-3.0.pc", sysrootDir: "/opt/sysroot/somewhere") { parser in
250+
XCTAssertEqual(parser.variables, [
251+
"libdir": "/opt/sysroot/somewhere/usr/local/Cellar/gtk+3/3.18.9/lib",
252+
"gtk_host": "x86_64-apple-darwin15.3.0",
253+
"includedir": "/opt/sysroot/somewhere/usr/local/Cellar/gtk+3/3.18.9/include",
254+
"prefix": "/opt/sysroot/somewhere/usr/local/Cellar/gtk+3/3.18.9",
255+
"gtk_binary_version": "3.0.0",
256+
"exec_prefix": "/opt/sysroot/somewhere/usr/local/Cellar/gtk+3/3.18.9",
257+
"targets": "quartz",
258+
"pcfiledir": parser.pcFile.parentDirectory.pathString,
259+
"pc_sysrootdir": "/opt/sysroot/somewhere"
260+
])
261+
XCTAssertEqual(parser.dependencies, ["gdk-3.0", "atk", "cairo", "cairo-gobject", "gdk-pixbuf-2.0", "gio-2.0"])
262+
XCTAssertEqual(parser.privateDependencies, ["atk", "epoxy", "gio-unix-2.0"])
263+
XCTAssertEqual(parser.cFlags, ["-I/opt/sysroot/somewhere/usr/local/Cellar/gtk+3/3.18.9/include/gtk-3.0"])
264+
XCTAssertEqual(parser.libs, ["-L/opt/sysroot/somewhere/usr/local/Cellar/gtk+3/3.18.9/lib", "-lgtk-3"])
265+
}
266+
267+
// sysroot should be not be prepended if it is already a prefix
268+
// - pkgconf makes this check, but pkg-config does not
269+
// - If the .pc file lies outside sysrootDir, pkgconf sets pc_sysrootdir to the empty string
270+
// https://github.com/pkgconf/pkgconf/issues/213
271+
// SwiftPM does not currently implement this special case.
272+
try loadPCFile("gtk+-3.0.pc", sysrootDir: "/usr/local/Cellar") { parser in
273+
XCTAssertEqual(parser.variables, [
274+
"libdir": "/usr/local/Cellar/gtk+3/3.18.9/lib",
275+
"gtk_host": "x86_64-apple-darwin15.3.0",
276+
"includedir": "/usr/local/Cellar/gtk+3/3.18.9/include",
277+
"prefix": "/usr/local/Cellar/gtk+3/3.18.9",
278+
"gtk_binary_version": "3.0.0",
279+
"exec_prefix": "/usr/local/Cellar/gtk+3/3.18.9",
280+
"targets": "quartz",
281+
"pcfiledir": parser.pcFile.parentDirectory.pathString,
282+
"pc_sysrootdir": "/usr/local/Cellar"
283+
])
284+
XCTAssertEqual(parser.dependencies, ["gdk-3.0", "atk", "cairo", "cairo-gobject", "gdk-pixbuf-2.0", "gio-2.0"])
285+
XCTAssertEqual(parser.privateDependencies, ["atk", "epoxy", "gio-unix-2.0"])
286+
XCTAssertEqual(parser.cFlags, ["-I/usr/local/Cellar/gtk+3/3.18.9/include/gtk-3.0"])
287+
XCTAssertEqual(parser.libs, ["-L/usr/local/Cellar/gtk+3/3.18.9/lib", "-lgtk-3"])
288+
}
289+
290+
// sysroot should be not be double-prepended if it is used explicitly by the .pc file
291+
// - pkgconf makes this check, but pkg-config does not
292+
try loadPCFile("double_sysroot.pc", sysrootDir: "/sysroot") { parser in
293+
XCTAssertEqual(parser.variables, [
294+
"prefix": "/sysroot/usr",
295+
"datarootdir": "/sysroot/usr/share",
296+
"pkgdatadir": "/sysroot/usr/share/pkgdata",
297+
"pcfiledir": parser.pcFile.parentDirectory.pathString,
298+
"pc_sysrootdir": "/sysroot"
299+
])
300+
}
301+
302+
// pkgconfig strips a leading sysroot prefix if sysroot appears anywhere else in the
303+
// expanded variable. SwiftPM's implementation is faithful to pkgconfig, even
304+
// thought it might seem more logical not to strip the prefix in this case.
305+
try loadPCFile("not_double_sysroot.pc", sysrootDir: "/sysroot") { parser in
306+
XCTAssertEqual(parser.variables, [
307+
"prefix": "/sysroot/usr",
308+
"datarootdir": "/sysroot/usr/share",
309+
"pkgdatadir": "/filler/sysroot/usr/share/pkgdata",
310+
"pcfiledir": parser.pcFile.parentDirectory.pathString,
311+
"pc_sysrootdir": "/sysroot"
312+
])
313+
}
314+
315+
// pkgconfig does not strip sysroot if it is a relative path
316+
try loadPCFile("double_sysroot.pc", sysrootDir: "sysroot") { parser in
317+
XCTAssertEqual(parser.variables, [
318+
"prefix": "sysroot/usr",
319+
"datarootdir": "sysroot/usr/share",
320+
"pkgdatadir": "sysroot/sysroot/usr/share/pkgdata",
321+
"pcfiledir": parser.pcFile.parentDirectory.pathString,
322+
"pc_sysrootdir": "sysroot"
323+
])
324+
}
325+
}
326+
247327
private func pcFilePath(_ inputName: String) -> AbsolutePath {
248328
return AbsolutePath(#file).parentDirectory.appending(components: "pkgconfigInputs", inputName)
249329
}
250330

251-
private func loadPCFile(_ inputName: String, body: ((PkgConfigParser) -> Void)? = nil) throws {
252-
var parser = try PkgConfigParser(pcFile: pcFilePath(inputName), fileSystem: localFileSystem)
331+
private func loadPCFile(_ inputName: String, sysrootDir: String? = nil, body: ((PkgConfigParser) -> Void)? = nil) throws {
332+
var parser = try PkgConfigParser(pcFile: pcFilePath(inputName), fileSystem: localFileSystem, sysrootDir: sysrootDir)
253333
try parser.parse()
254334
body?(parser)
255335
}

Tests/PackageLoadingTests/pkgconfigInputs/case_insensitive.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33

44
#some comment
5+
Name: case_insensitive
6+
Version: 1
7+
Description: Demonstrate that pkgconfig keys are case-insensitive
58

69
# upstream pkg-config parser allows Cflags & CFlags as key
710
CFlags: -I/usr/local/include

Tests/PackageLoadingTests/pkgconfigInputs/deps_variable.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33
my_dep=atk
44
#some comment
5+
Name: deps_variable
6+
Version: 1
7+
Description: Demonstrate use of a locally-defined variable
58

69
Requires: gdk-3.0 >= 1.0.0 ${my_dep}
710
Libs: -L${prefix} -lgtk-3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
prefix=/usr
2+
datarootdir=${prefix}/share
3+
pkgdatadir=${pc_sysrootdir}/${datarootdir}/pkgdata
4+
5+
Name: double_sysroot
6+
Description: Demonstrate double-prefixing of pc_sysrootdir (https://github.com/pkgconf/pkgconf/issues/123)
7+
Version: 1

Tests/PackageLoadingTests/pkgconfigInputs/dummy_dependency.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33

44
#some comment
5+
Name: dummy_dependency
6+
Version: 1
7+
Description: Demonstrate a blank dependency entry
58

69
Requires: pango, , fontconfig >= 2.13.0
710
Libs:-L${prefix} -lpangoft2-1.0

Tests/PackageLoadingTests/pkgconfigInputs/empty_cflags.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33

44
#some comment
5+
Name: empty_cflags
6+
Version: 1
7+
Description: Demonstrate an empty cflags list
58

69
Requires: gdk-3.0 atk
710
Libs:-L${prefix} -lgtk-3

Tests/PackageLoadingTests/pkgconfigInputs/escaped_spaces.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33
my_dep=atk
44
#some comment
5+
Name: escaped_spaces
6+
Version: 1
7+
Description: Demonstrate use of escape characters in flag values
58

69
Requires: gdk-3.0 >= 1.0.0 ${my_dep}
710
Libs: -L"${prefix}" -l"gtk 3" -wantareal\\here -one\\ -two

Tests/PackageLoadingTests/pkgconfigInputs/failure_case.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33

44
#some comment
5+
Name: failure_case
6+
Version: 1
7+
Description: Demonstrate failure caused by use of an undefined variable
58

69
Requires: gdk-3.0 >= 1.0.0
710
Libs: -L${prefix} -lgtk-3 ${my_dep}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
prefix=/usr
2+
datarootdir=${prefix}/share
3+
pkgdatadir=${pc_sysrootdir}/filler/${datarootdir}/pkgdata
4+
5+
Name: double_sysroot
6+
Description: Demonstrate pc_sysrootdir appearing elsewhere in a path - this is not a double prefix and should not be removed

Tests/PackageLoadingTests/pkgconfigInputs/quotes_failure.pc

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ prefix=/usr/local/bin
22
exec_prefix=${prefix}
33
my_dep=atk
44
#some comment
5+
Name: quotes_failure
6+
Version: 1
7+
Description: Demonstrate failure due to unbalanced quotes
58

69
Requires: gdk-3.0 >= 1.0.0 ${my_dep}
710
Libs: -L"${prefix}" -l"gt"k3" -wantareal\\here -one\\ -two

0 commit comments

Comments
 (0)