Skip to content

Commit 00079ae

Browse files
committed
AppImageBundler: Put lib deps in usr/lib instead of usr/bin (required implementing runpath patching)
1 parent e765048 commit 00079ae

File tree

5 files changed

+164
-52
lines changed

5 files changed

+164
-52
lines changed

Diff for: Sources/swift-bundler/Bundler/AppImageBundler.swift

+79-40
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ enum AppImageBundler: Bundler {
7171
{
7272
Self.copyDynamicLibraryDependencies(
7373
of: appExecutable,
74-
to: appDir.appendingPathComponent("usr/bin")
74+
to: appDir.appendingPathComponent("usr/lib")
7575
)
7676
},
7777
{ Self.createSymlinks(at: appDir, appName: context.appName) },
@@ -91,59 +91,96 @@ enum AppImageBundler: Bundler {
9191

9292
// MARK: Private methods
9393

94+
/// Copies dynamic library dependencies of the specified executable file into
95+
/// the `AppDir`, and updates the runpaths of the executable and moved dynamic
96+
/// libraries accordingly.
9497
private static func copyDynamicLibraryDependencies(
9598
of appExecutable: URL,
9699
to destination: URL
97100
) -> Result<Void, AppImageBundlerError> {
101+
log.info("Copying dynamic library dependencies of main executable")
98102
return Process.create(
99103
"ldd",
100104
arguments: [appExecutable.path],
101105
runSilentlyWhenNotVerbose: false
102-
)
103-
.getOutput()
104-
.mapError { error in
105-
.failedToEnumerateDynamicDependencies(error)
106-
}
107-
.flatMap { output in
108-
let lines = output.split(separator: "\n")
109-
for line in lines {
110-
guard let libraryPath = try? lddLineParser.parse(line) else {
111-
continue
112-
}
113-
114-
let libraryURL = URL(fileURLWithPath: libraryPath)
115-
let destination = destination.appendingPathComponent(
116-
libraryURL.lastPathComponent
117-
)
118-
119-
#if os(Linux)
120-
// URL.resolvingSymlinksInPath is broken on Linux
121-
let resolvedLibraryPath = String(unsafeUninitializedCapacity: 4097) { buffer in
122-
realpath(libraryPath, buffer.baseAddress)
123-
return strlen(UnsafePointer(buffer.baseAddress!))
106+
).getOutput()
107+
.mapError { error in
108+
.failedToEnumerateDynamicDependencies(error)
109+
}
110+
.flatMap { (output: String) -> Result<Void, AppImageBundlerError> in
111+
// Parse the output of ldd line-by-line and copy the located libraries to
112+
// the destination directory (updating runpaths appropriately).
113+
let lines = output.split(separator: "\n")
114+
for line in lines {
115+
guard let libraryPath = try? lddLineParser.parse(line) else {
116+
continue
124117
}
125-
let resolvedLibraryURL = URL(fileURLWithPath: resolvedLibraryPath)
126-
#else
127-
let resolvedLibraryURL = libraryURL.resolvingSymlinksInPath()
128-
#endif
129-
130-
do {
131-
try FileManager.default.copyItem(
132-
at: resolvedLibraryURL,
133-
to: destination
118+
119+
let libraryURL = URL(fileURLWithPath: libraryPath)
120+
let destination = destination.appendingPathComponent(
121+
libraryURL.lastPathComponent
134122
)
135-
} catch {
136-
return .failure(
137-
.failedToCopyDynamicLibrary(
138-
source: libraryURL,
139-
destination: destination,
140-
error
123+
124+
// Resolve symlinks in case the library itself is a symlinnk (we want
125+
// to copy the actual library not the symlink).
126+
#if os(Linux)
127+
// URL.resolvingSymlinksInPath is broken on Linux
128+
let resolvedLibraryPath = String(unsafeUninitializedCapacity: 4097) { buffer in
129+
realpath(libraryPath, buffer.baseAddress)
130+
return strlen(UnsafePointer(buffer.baseAddress!))
131+
}
132+
let resolvedLibraryURL = URL(fileURLWithPath: resolvedLibraryPath)
133+
#else
134+
let resolvedLibraryURL = libraryURL.resolvingSymlinksInPath()
135+
#endif
136+
137+
// Copy the library to the provided destination directory.
138+
do {
139+
try FileManager.default.copyItem(
140+
at: resolvedLibraryURL,
141+
to: destination
141142
)
143+
} catch {
144+
return .failure(
145+
.failedToCopyDynamicLibrary(
146+
source: libraryURL,
147+
destination: destination,
148+
error
149+
)
150+
)
151+
}
152+
153+
// Update the library's runpath so that it only looks for its dependencies in
154+
// the current directory (before falling back to the system wide default runpath).
155+
switch PatchElfTool.setRunpath(of: destination, to: "$ORIGIN") {
156+
case .success:
157+
break
158+
case .failure(let error):
159+
return .failure(
160+
.failedToCopyDynamicLibrary(
161+
source: libraryURL,
162+
destination: destination,
163+
error
164+
)
165+
)
166+
}
167+
}
168+
return .success()
169+
}
170+
.flatMap { (_: Void) -> Result<Void, AppImageBundlerError> in
171+
// Update the main executable's runpath
172+
guard
173+
let relativeDestination = destination.relativePath(
174+
from: appExecutable.deletingLastPathComponent()
142175
)
176+
else {
177+
return .failure(.failedToUpdateMainExecutableRunpath(executable: appExecutable, nil))
143178
}
179+
return PatchElfTool.setRunpath(of: appExecutable, to: "$ORIGIN/\(relativeDestination)")
180+
.mapError { error in
181+
.failedToUpdateMainExecutableRunpath(executable: appExecutable, error)
182+
}
144183
}
145-
return .success()
146-
}
147184
}
148185

149186
private static func copyResources(
@@ -212,13 +249,15 @@ enum AppImageBundler: Bundler {
212249
// bundlers.
213250
let appDir = outputDirectory.appendingPathComponent("\(appName).AppDir")
214251
let binDir = appDir.appendingPathComponent("usr/bin")
252+
let libDir = appDir.appendingPathComponent("usr/lib")
215253
let iconDir = appDir.appendingPathComponent("usr/share/icons/hicolor/1024x1024/apps")
216254

217255
do {
218256
if fileManager.itemExists(at: appDir, withType: .directory) {
219257
try fileManager.removeItem(at: appDir)
220258
}
221259
try fileManager.createDirectory(at: binDir)
260+
try fileManager.createDirectory(at: libDir)
222261
try fileManager.createDirectory(at: iconDir)
223262
return .success()
224263
} catch {

Diff for: Sources/swift-bundler/Bundler/AppImageBundlerError.swift

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ enum AppImageBundlerError: LocalizedError {
1212
case failedToEnumerateResourceBundles(directory: URL, Error)
1313
case failedToEnumerateDynamicDependencies(ProcessError)
1414
case failedToCopyDynamicLibrary(source: URL, destination: URL, Error)
15+
case failedToUpdateMainExecutableRunpath(executable: URL, Error?)
1516

1617
var errorDescription: String? {
1718
switch self {
@@ -50,6 +51,12 @@ enum AppImageBundlerError: LocalizedError {
5051
Failed to copy dynamic library from '\(source.relativePath)' to \
5152
'\(destination.relativePath)'
5253
"""
54+
case .failedToUpdateMainExecutableRunpath(let executable, let underlyingError):
55+
let reason = underlyingError?.localizedDescription ?? "unknown reason"
56+
return """
57+
Failed to update the runpath of the main executable at \
58+
'\(executable.relativePath)': \(reason)
59+
"""
5360
}
5461
}
5562
}

Diff for: Sources/swift-bundler/Bundler/PatchElfTool.swift

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Foundation
2+
3+
/// A warpper around `patchelf`.
4+
enum PatchElfTool {
5+
static func setRunpath(
6+
of elfFile: URL,
7+
to newRunpath: String
8+
) -> Result<Void, PatchElfToolError> {
9+
Process.locate("patchelf")
10+
.mapError { error in
11+
.patchelfNotFound(error)
12+
}
13+
.flatMap { patchelf in
14+
let result = Process.create(
15+
patchelf,
16+
arguments: [elfFile.path, "--set-rpath", newRunpath],
17+
runSilentlyWhenNotVerbose: false
18+
).runAndWait()
19+
.mapError { error in
20+
PatchElfToolError.failedToSetRunpath(elfFile: elfFile, error)
21+
}
22+
return result
23+
}
24+
}
25+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
/// An error thrown by ``PatchElfTool``.
4+
enum PatchElfToolError: LocalizedError {
5+
case patchelfNotFound(ProcessError)
6+
case failedToSetRunpath(elfFile: URL, ProcessError)
7+
8+
var errorDescription: String {
9+
switch self {
10+
case .patchelfNotFound:
11+
return """
12+
Command 'patchelf' not found, but required by selected bundler. \
13+
Please install it and try again
14+
"""
15+
case .failedToSetRunpath(let elfFile, let error):
16+
return """
17+
Failed to set runpath of '\(elfFile.path)': \(error.localizedDescription)
18+
"""
19+
}
20+
}
21+
}

Diff for: Sources/swift-bundler/Extensions/Process.swift

+32-12
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,17 @@ extension Process {
5050
// Thanks Martin! https://forums.swift.org/t/the-problem-with-a-frozen-process-in-swift-process-class/39579/6
5151
var output = Data()
5252
var currentLine: String?
53-
let group = DispatchGroup()
54-
group.enter()
55-
pipe.fileHandleForReading.readabilityHandler = { fh in
56-
// TODO: All of this Process code is getting pretty ridiculous and janky, we should switch to
57-
// the experimental proposed Subprocess API (swift-experimental-subprocess)
58-
let newData = fh.availableData
59-
if newData.isEmpty {
53+
54+
func handleData(_ data: Data, isFinal: Bool = false) {
55+
if pipe.fileHandleForReading.readabilityHandler == nil {
56+
return
57+
}
58+
59+
if data.isEmpty {
6060
pipe.fileHandleForReading.readabilityHandler = nil
61-
group.leave()
6261
} else {
63-
output.append(contentsOf: newData)
64-
if let handleLine = handleLine, let string = String(data: newData, encoding: .utf8) {
62+
output.append(contentsOf: data)
63+
if let handleLine = handleLine, let string = String(data: data, encoding: .utf8) {
6564
let lines = ((currentLine ?? "") + string).split(
6665
separator: "\n", omittingEmptySubsequences: false)
6766
if let lastLine = lines.last, lastLine != "" {
@@ -77,12 +76,32 @@ extension Process {
7776
}
7877
}
7978

79+
pipe.fileHandleForReading.readabilityHandler = { fh in
80+
// TODO: All of this Process code is getting pretty ridiculous and janky, we should switch to
81+
// the experimental proposed Subprocess API (swift-experimental-subprocess)
82+
let newData = fh.availableData
83+
handleData(newData)
84+
}
85+
8086
return runAndWait()
8187
.map { _ in
82-
group.wait()
88+
try? pipe.fileHandleForWriting.close()
89+
90+
// `readToEnd()` doesn't exist in 10.15.0, but it doesn't really matter anyway cause this
91+
// code only seems to be relevant on Linux anyway (but probably doesn't hurt on macOS
92+
// when available).
93+
if #available(macOS 10.15.4, *) {
94+
if let data = try? pipe.fileHandleForReading.readToEnd() {
95+
handleData(data, isFinal: true)
96+
}
97+
}
98+
99+
pipe.fileHandleForReading.readabilityHandler = nil
100+
83101
if let currentLine = currentLine {
84102
handleLine?(currentLine)
85103
}
104+
86105
return output
87106
}
88107
.mapError { error in
@@ -213,7 +232,8 @@ extension Process {
213232
arguments: [
214233
"-c",
215234
"which \(tool)",
216-
]
235+
],
236+
runSilentlyWhenNotVerbose: false
217237
)
218238
.getOutput()
219239
.map { path in

0 commit comments

Comments
 (0)