Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
furby-tm authored Nov 26, 2024
2 parents 08d6cf2 + fda5a92 commit 9ff23e1
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 37 deletions.
9 changes: 9 additions & 0 deletions Sources/swift-bundler/AsyncMain.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Foundation
import Rainbow

#if os(Linux)
import Glibc
#endif

@main
struct AsyncMain {
static func main() async {
Expand All @@ -11,6 +15,11 @@ struct AsyncMain {
for process in processes {
process.terminate()
}
#if os(Linux)
for pid in appImagePIDs {
kill(pid, SIGKILL)
}
#endif
Foundation.exit(1)
}
}
Expand Down
31 changes: 10 additions & 21 deletions Sources/swift-bundler/Bundler/Runner/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,31 +263,20 @@ enum Runner {
}
}

/// Runs a linux executable.
/// - Parameters:
/// - bundle: The app bundle to run.
/// - arguments: Command line arguments to pass to the app.
/// - environmentVariables: Environment variables to pass to the app.
/// - Returns: A failure if an error occurs.
static func runLinuxExecutable(
bundle: URL,
arguments: [String],
environmentVariables: [String: String]
) -> Result<Void, RunnerError> {
let process = Process.create(
bundle.path,
arguments: arguments,
runSilentlyWhenNotVerbose: false
)
process.addEnvironmentVariables(environmentVariables)

do {
try process.run()
} catch {
return .failure(.failedToRunExecutable(.failedToRunProcess(error)))
}

process.waitUntilExit()

let exitStatus = Int(process.terminationStatus)
if exitStatus != 0 {
return .failure(.failedToRunExecutable(.nonZeroExitStatus(exitStatus)))
} else {
return .success()
}
Process.runAppImage(bundle.path, arguments: arguments)
.mapError { error in
.failedToRunExecutable(error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,10 @@ enum SwiftPackageManager {
"--target=\(targetTriple)",
"-isysroot", sdkPath,
].flatMap { ["-Xcc", $0] }
case .macOS, .linux:
case .linux:
// Force statically linking against the Swift runtime libraries
platformArguments = ["--static-swift-stdlib"]
case .macOS:
platformArguments = []
}

Expand Down
91 changes: 77 additions & 14 deletions Sources/swift-bundler/Extensions/Process.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import Foundation
/// If the program is killed, all processes in this array are terminated before the program exits.
var processes: [Process] = []

#if os(Linux)
/// The PIDs of all AppImage processes started manually (due to the weird
/// workaround required).
var appImagePIDs: [pid_t] = []
#endif

extension Process {
/// A string created by concatenating all of the program's arguments together. Suitable for error messages,
/// but not necessarily 100% correct.
Expand Down Expand Up @@ -189,14 +195,32 @@ extension Process {
/// - Parameter tool: The tool to expand into a full path.
/// - Returns: The absolute path to the tool, or a failure if the tool can't be located.
static func locate(_ tool: String) -> Result<String, ProcessError> {
Process.create(
// Restrict the set of inputs to avoid command injection. This is very dodgy but there
// doesn't seem to be any nice way to call bash built-ins directly with an argument
// vector. Better approaches are extremely welcome!!
guard
tool.allSatisfy({ character in
character.isASCII
&& (character.isLetter
|| character.isNumber || character == "-" || character == "_")
})
else {
return .failure(.invalidToolName(tool))
}

return Process.create(
"/bin/sh",
arguments: [
"-c",
"which \(tool)",
]
).getOutput().map { path in
return path.trimmingCharacters(in: .whitespacesAndNewlines)
)
.getOutput()
.map { path in
path.trimmingCharacters(in: .whitespacesAndNewlines)
}
.mapError { error in
.failedToLocateTool(tool, error)
}
}

Expand All @@ -208,22 +232,52 @@ extension Process {
///
/// The issue occurs even without any pipes attached, so it's not the classic
/// full pipes issue.
static func runAppImage(_ appImage: String, arguments: [String]) -> Result<Void, ProcessError> {
static func runAppImage(
_ appImage: String,
arguments: [String],
additionalEnvironmentVariables: [String: String] = [:]
) -> Result<Void, ProcessError> {
#if os(Linux)
let selfPid = getpid()
setpgid(0, selfPid)
let childPid = fork()
if childPid == 0 {
setpgid(0, selfPid)
let cArguments =
(["/usr/bin/env", appImage] + arguments).map { strdup($0) }
+ [UnsafeMutablePointer<CChar>(bitPattern: 0)]
execv("/usr/bin/env", cArguments)
var environment = ProcessInfo.processInfo.environment
for (key, value) in additionalEnvironmentVariables {
guard isValidEnvironmentVariableKey(key) else {
return .failure(.invalidEnvironmentVariableKey(key))
}
environment[key] = value
}

let environmentArray =
environment.map { key, value in
strdup("\(key)=\(value)")
} + [UnsafeMutablePointer<CChar>(bitPattern: 0)]

// Locate the tool or interpret it as a relative/absolute path.
let executablePath: String
switch locate(appImage) {
case .success(let path):
executablePath = path
case .failure(.invalidToolName):
executablePath = appImage
case .failure(let error):
return .failure(error)
}

let cArguments =
([executablePath] + arguments).map { strdup($0) }
+ [UnsafeMutablePointer<CChar>(bitPattern: 0)]

let selfPID = getpid()
setpgid(0, selfPID)
let childPID = fork()
if childPID == 0 {
setpgid(0, selfPID)
execve(executablePath, cArguments, environmentArray)
// We only ever get here if the execv fails
Foundation.exit(-1)
} else {
appImagePIDs.append(childPID)
var status: Int32 = 0
waitpid(childPid, &status, 0)
waitpid(childPID, &status, 0)
if status != 0 {
return .failure(
.nonZeroExitStatus(Int(status))
Expand All @@ -245,4 +299,13 @@ extension Process {
}
#endif
}

/// Validates an environment variable key. Currently only used by a Linux workaround
/// that has to interface with low-level APIs.
private static func isValidEnvironmentVariableKey(_ key: String) -> Bool {
key.allSatisfy({ character in
character.isASCII
&& (character.isLetter || character.isNumber || character == "_")
}) && key.first?.isNumber == false
}
}
15 changes: 14 additions & 1 deletion Sources/swift-bundler/Extensions/ProcessError.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Foundation

/// An error returned by custom methods added to `Process`.
enum ProcessError: LocalizedError {
indirect enum ProcessError: LocalizedError {
case invalidUTF8Output(output: Data)
case nonZeroExitStatus(Int)
case nonZeroExitStatusWithOutput(Data, Int)
case failedToRunProcess(Error)
case invalidEnvironmentVariableKey(String)
case invalidToolName(String)
case failedToLocateTool(String, ProcessError)

var errorDescription: String? {
switch self {
Expand All @@ -21,6 +24,16 @@ enum ProcessError: LocalizedError {
"""
case .failedToRunProcess:
return "The process failed to run"
case .invalidEnvironmentVariableKey(let key):
return "Invalid environment variable key '\(key)'"
case .invalidToolName(let name):
return
"""
Invalid tool name '\(name)'. Must be contain only alphanumeric \
characters, hyphens, and underscores.
"""
case .failedToLocateTool(let tool, _):
return "Failed to locate '\(tool)'. Ensure that you have it installed."
}
}
}

0 comments on commit 9ff23e1

Please sign in to comment.