diff --git a/Package.resolved b/Package.resolved index a948421e..5dbd3e1c 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9731c883c87b6c53588707a1dbbc85fb7c43f965751c47869247d8dd8fe67f1e", + "originHash" : "5516525be0e9028235a6e894a82da546c8194a90651e5cbcf922f1bf20cb2c5f", "pins" : [ { "identity" : "async-http-client", @@ -181,13 +181,21 @@ "version" : "1.8.2" } }, + { + "identity" : "swift-subprocess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-subprocess", + "state" : { + "revision" : "afc1f734feb29c3a1ebbd97cc1fe943f8e5d80e5" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "a34201439c74b53f0fd71ef11741af7e7caf01e1", - "version" : "1.4.2" + "revision" : "61e4ca4b81b9e09e2ec863b00c340eb13497dac6", + "version" : "1.5.0" } }, { @@ -219,4 +227,4 @@ } ], "version" : 3 -} \ No newline at end of file +} diff --git a/Package.swift b/Package.swift index c550ffd5..135c3270 100644 --- a/Package.swift +++ b/Package.swift @@ -31,6 +31,7 @@ let package = Package( .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.7.2"), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.2"), .package(url: "https://github.com/apple/swift-system", from: "1.4.2"), + .package(url: "https://github.com/swiftlang/swift-subprocess", revision: "afc1f734feb29c3a1ebbd97cc1fe943f8e5d80e5"), // This dependency provides the correct version of the formatter so that you can run `swift run swiftformat Package.swift Plugins/ Sources/ Tests/` .package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.49.18"), ], @@ -67,6 +68,7 @@ let package = Package( .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"), .product(name: "SystemPackage", package: "swift-system"), + .product(name: "Subprocess", package: "swift-subprocess"), ], swiftSettings: swiftSettings, plugins: ["GenerateCommandModels"] diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index 386e69db..1d031509 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -255,7 +255,7 @@ public struct Linux: Platform { } if requireSignatureValidation { - guard (try? self.runProgram("gpg", "--version", quiet: true)) != nil else { + guard (try? await self.runProgram("gpg", "--version", quiet: true)) != nil else { var msg = "gpg is not installed. " if let manager { msg += """ @@ -321,7 +321,7 @@ public struct Linux: Platform { } return false case "yum": - try self.runProgram("yum", "list", "installed", package, quiet: true) + try await self.runProgram("yum", "list", "installed", package, quiet: true) return true default: return true @@ -382,7 +382,7 @@ public struct Linux: Platform { tmpDir / String(name) } - try self.runProgram((tmpDir / "swiftly").string, "init") + try await self.runProgram((tmpDir / "swiftly").string, "init") } } diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index 5f8fb0c4..018bd16c 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -142,7 +142,7 @@ public struct MacOS: Platform { try await sys.tar(.directory(installDir)).extract(.verbose, .archive(payload)).run(self, quiet: false) } - try self.runProgram((userHomeDir / ".swiftly/bin/swiftly").string, "init") + try await self.runProgram((userHomeDir / ".swiftly/bin/swiftly").string, "init") } public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose: Bool) diff --git a/Sources/SwiftlyCore/ModeledCommandLine.swift b/Sources/SwiftlyCore/ModeledCommandLine.swift index 10be5173..c291f13b 100644 --- a/Sources/SwiftlyCore/ModeledCommandLine.swift +++ b/Sources/SwiftlyCore/ModeledCommandLine.swift @@ -181,7 +181,7 @@ extension Runnable { newEnv = newValue } - try p.runProgram([executable] + args, quiet: quiet, env: newEnv) + try await p.runProgram([executable] + args, quiet: quiet, env: newEnv) } } diff --git a/Sources/SwiftlyCore/Platform+Process.swift b/Sources/SwiftlyCore/Platform+Process.swift new file mode 100644 index 00000000..6eaca7d4 --- /dev/null +++ b/Sources/SwiftlyCore/Platform+Process.swift @@ -0,0 +1,171 @@ +import Foundation +import Subprocess +#if os(macOS) +import System +#endif + +import SystemPackage + +extension Platform { +#if os(macOS) || os(Linux) + func proxyEnv(_ ctx: SwiftlyCoreContext, env: [String: String], toolchain: ToolchainVersion) async throws -> [String: String] { + var newEnv = env + + let tcPath = try await self.findToolchainLocation(ctx, toolchain) / "usr/bin" + guard try await fs.exists(atPath: tcPath) else { + throw SwiftlyError( + message: + "Toolchain \(toolchain) could not be located in \(tcPath). You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again." + ) + } + + var pathComponents = (newEnv["PATH"] ?? "").split(separator: ":").map { String($0) } + + // The toolchain goes to the beginning of the PATH + pathComponents.removeAll(where: { $0 == tcPath.string }) + pathComponents = [tcPath.string] + pathComponents + + // Remove swiftly bin directory from the PATH entirely + let swiftlyBinDir = self.swiftlyBinDir(ctx) + pathComponents.removeAll(where: { $0 == swiftlyBinDir.string }) + + newEnv["PATH"] = String(pathComponents.joined(separator: ":")) + + return newEnv + } + + /// Proxy the invocation of the provided command to the chosen toolchain. + /// + /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with + /// the exit code and program information. + /// + public func proxy(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:]) async throws { + let tcPath = (try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin" + + let commandTcPath = tcPath / command + let commandToRun = if try await fs.exists(atPath: commandTcPath) { + commandTcPath.string + } else { + command + } + + var newEnv = try await self.proxyEnv(ctx, env: ProcessInfo.processInfo.environment, toolchain: toolchain) + for (key, value) in env { + newEnv[key] = value + } + +#if os(macOS) + // On macOS, we try to set SDKROOT if its empty for tools like clang++ that need it to + // find standard libraries that aren't in the toolchain, like libc++. Here we + // use xcrun to tell us what the default sdk root should be. + if newEnv["SDKROOT"] == nil { + newEnv["SDKROOT"] = (try? await self.runProgramOutput("/usr/bin/xcrun", "--show-sdk-path"))?.replacingOccurrences(of: "\n", with: "") + } +#endif + + try await self.runProgram([commandToRun] + arguments, env: newEnv) + } + + /// Proxy the invocation of the provided command to the chosen toolchain and capture the output. + /// + /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with + /// the exit code and program information. + /// + public func proxyOutput(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String]) async throws -> String? { + let tcPath = (try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin" + + let commandTcPath = tcPath / command + let commandToRun = if try await fs.exists(atPath: commandTcPath) { + commandTcPath.string + } else { + command + } + + return try await self.runProgramOutput(commandToRun, arguments, env: self.proxyEnv(ctx, env: ProcessInfo.processInfo.environment, toolchain: toolchain)) + } + + /// Run a program. + /// + /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with + /// the exit code and program information. + /// + public func runProgram(_ args: String..., quiet: Bool = false, env: [String: String]? = nil) + async throws + { + try await self.runProgram([String](args), quiet: quiet, env: env) + } + + /// Run a program. + /// + /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with + /// the exit code and program information. + /// + public func runProgram(_ args: [String], quiet: Bool = false, env: [String: String]? = nil) + async throws + { + if !quiet { + let result = try await run( + .path("/usr/bin/env"), + arguments: .init(args), + environment: env != nil ? .inherit.updating(env ?? [:]) : .inherit, + input: .fileDescriptor(.standardInput, closeAfterSpawningProcess: false), + output: .fileDescriptor(.standardOutput, closeAfterSpawningProcess: false), + error: .fileDescriptor(.standardError, closeAfterSpawningProcess: false), + ) + + if case let .exited(code) = result.terminationStatus, code != 0 { + throw RunProgramError(exitCode: code, program: args.first!, arguments: Array(args.dropFirst())) + } + } else { + let result = try await run( + .path("/usr/bin/env"), + arguments: .init(args), + environment: env != nil ? .inherit.updating(env ?? [:]) : .inherit, + output: .discarded, + error: .discarded, + ) + + if case let .exited(code) = result.terminationStatus, code != 0 { + throw RunProgramError(exitCode: code, program: args.first!, arguments: Array(args.dropFirst())) + } + } + + // TODO: handle exits with a signal + } + + /// Run a program and capture its output. + /// + /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with + /// the exit code and program information. + /// + public func runProgramOutput(_ program: String, _ args: String..., env: [String: String]? = nil) + async throws -> String? + { + try await self.runProgramOutput(program, [String](args), env: env) + } + + /// Run a program and capture its output. + /// + /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with + /// the exit code and program information. + /// + public func runProgramOutput(_ program: String, _ args: [String], env: [String: String]? = nil) + async throws -> String? + { + let result = try await run( + .path("/usr/bin/env"), + arguments: .init([program] + args), + environment: env != nil ? .inherit.updating(env ?? [:]) : .inherit, + output: .string(limit: 10 * 1024 * 1024, encoding: UTF8.self), + error: .fileDescriptor(.standardError, closeAfterSpawningProcess: false) + ) + + if case let .exited(code) = result.terminationStatus, code != 0 { + throw RunProgramError(exitCode: code, program: args.first!, arguments: Array(args.dropFirst())) + } + + return result.standardOutput + } + +#endif +} diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index e4782fc3..b1a6289a 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -161,192 +161,6 @@ extension Platform { } #if os(macOS) || os(Linux) - func proxyEnv(_ ctx: SwiftlyCoreContext, env: [String: String], toolchain: ToolchainVersion) async throws -> [String: String] { - var newEnv = env - - let tcPath = try await self.findToolchainLocation(ctx, toolchain) / "usr/bin" - guard try await fs.exists(atPath: tcPath) else { - throw SwiftlyError( - message: - "Toolchain \(toolchain) could not be located in \(tcPath). You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again." - ) - } - - var pathComponents = (newEnv["PATH"] ?? "").split(separator: ":").map { String($0) } - - // The toolchain goes to the beginning of the PATH - pathComponents.removeAll(where: { $0 == tcPath.string }) - pathComponents = [tcPath.string] + pathComponents - - // Remove swiftly bin directory from the PATH entirely - let swiftlyBinDir = self.swiftlyBinDir(ctx) - pathComponents.removeAll(where: { $0 == swiftlyBinDir.string }) - - newEnv["PATH"] = String(pathComponents.joined(separator: ":")) - - return newEnv - } - - /// Proxy the invocation of the provided command to the chosen toolchain. - /// - /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with - /// the exit code and program information. - /// - public func proxy(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:]) async throws { - let tcPath = (try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin" - - let commandTcPath = tcPath / command - let commandToRun = if try await fs.exists(atPath: commandTcPath) { - commandTcPath.string - } else { - command - } - - var newEnv = try await self.proxyEnv(ctx, env: ProcessInfo.processInfo.environment, toolchain: toolchain) - for (key, value) in env { - newEnv[key] = value - } - -#if os(macOS) - // On macOS, we try to set SDKROOT if its empty for tools like clang++ that need it to - // find standard libraries that aren't in the toolchain, like libc++. Here we - // use xcrun to tell us what the default sdk root should be. - if newEnv["SDKROOT"] == nil { - newEnv["SDKROOT"] = (try? await self.runProgramOutput("/usr/bin/xcrun", "--show-sdk-path"))?.replacingOccurrences(of: "\n", with: "") - } -#endif - - try self.runProgram([commandToRun] + arguments, env: newEnv) - } - - /// Proxy the invocation of the provided command to the chosen toolchain and capture the output. - /// - /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with - /// the exit code and program information. - /// - public func proxyOutput(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String]) async throws -> String? { - let tcPath = (try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin" - - let commandTcPath = tcPath / command - let commandToRun = if try await fs.exists(atPath: commandTcPath) { - commandTcPath.string - } else { - command - } - - return try await self.runProgramOutput(commandToRun, arguments, env: self.proxyEnv(ctx, env: ProcessInfo.processInfo.environment, toolchain: toolchain)) - } - - /// Run a program. - /// - /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with - /// the exit code and program information. - /// - public func runProgram(_ args: String..., quiet: Bool = false, env: [String: String]? = nil) - throws - { - try self.runProgram([String](args), quiet: quiet, env: env) - } - - /// Run a program. - /// - /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with - /// the exit code and program information. - /// - public func runProgram(_ args: [String], quiet: Bool = false, env: [String: String]? = nil) - throws - { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") - process.arguments = args - - if let env { - process.environment = env - } - - if quiet { - process.standardOutput = nil - process.standardError = nil - } - - try process.run() - // Attach this process to our process group so that Ctrl-C and other signals work - let pgid = tcgetpgrp(STDOUT_FILENO) - if pgid != -1 { - tcsetpgrp(STDOUT_FILENO, process.processIdentifier) - } - - defer { - if pgid != -1 { - tcsetpgrp(STDOUT_FILENO, pgid) - } - } - - process.waitUntilExit() - - guard process.terminationStatus == 0 else { - throw RunProgramError(exitCode: process.terminationStatus, program: args.first!, arguments: Array(args.dropFirst())) - } - } - - /// Run a program and capture its output. - /// - /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with - /// the exit code and program information. - /// - public func runProgramOutput(_ program: String, _ args: String..., env: [String: String]? = nil) - async throws -> String? - { - try await self.runProgramOutput(program, [String](args), env: env) - } - - /// Run a program and capture its output. - /// - /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with - /// the exit code and program information. - /// - public func runProgramOutput(_ program: String, _ args: [String], env: [String: String]? = nil) - async throws -> String? - { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/env") - process.arguments = [program] + args - - if let env { - process.environment = env - } - - let outPipe = Pipe() - process.standardInput = FileHandle.nullDevice - process.standardError = FileHandle.nullDevice - process.standardOutput = outPipe - - try process.run() - // Attach this process to our process group so that Ctrl-C and other signals work - let pgid = tcgetpgrp(STDOUT_FILENO) - if pgid != -1 { - tcsetpgrp(STDOUT_FILENO, process.processIdentifier) - } - defer { - if pgid != -1 { - tcsetpgrp(STDOUT_FILENO, pgid) - } - } - - let outData = try outPipe.fileHandleForReading.readToEnd() - - process.waitUntilExit() - - guard process.terminationStatus == 0 else { - throw RunProgramError(exitCode: process.terminationStatus, program: program, arguments: args) - } - - if let outData { - return String(data: outData, encoding: .utf8) - } else { - return nil - } - } // Install ourselves in the final location public func installSwiftlyBin(_ ctx: SwiftlyCoreContext) async throws { diff --git a/Sources/TestSwiftly/TestSwiftly.swift b/Sources/TestSwiftly/TestSwiftly.swift index 26a64cf3..6f3cafeb 100644 --- a/Sources/TestSwiftly/TestSwiftly.swift +++ b/Sources/TestSwiftly/TestSwiftly.swift @@ -119,7 +119,7 @@ struct TestSwiftly: AsyncParsableCommand { env["SWIFTLY_BIN_DIR"] = (customLoc! / "bin").string env["SWIFTLY_TOOLCHAINS_DIR"] = (customLoc! / "toolchains").string - try currentPlatform.runProgram(extractedSwiftly.string, "init", "--assume-yes", "--no-modify-profile", "--skip-install", quiet: false, env: env) + try await currentPlatform.runProgram(extractedSwiftly.string, "init", "--assume-yes", "--no-modify-profile", "--skip-install", quiet: false, env: env) try await sh(executable: .path(shell), .login, .command(". \"\(customLoc! / "env.sh")\" && swiftly install --assume-yes latest --post-install-file=./post-install.sh")).run(currentPlatform, env: env, quiet: false) } else { print("Installing swiftly to the default location.") @@ -132,7 +132,7 @@ struct TestSwiftly: AsyncParsableCommand { env["XDG_CONFIG_HOME"] = (fs.home / ".config").string } - try currentPlatform.runProgram(extractedSwiftly.string, "init", "--assume-yes", "--skip-install", quiet: false, env: env) + try await currentPlatform.runProgram(extractedSwiftly.string, "init", "--assume-yes", "--skip-install", quiet: false, env: env) try await sh(executable: .path(shell), .login, .command("swiftly install --assume-yes latest --post-install-file=./post-install.sh")).run(currentPlatform, env: env, quiet: false) } @@ -140,7 +140,7 @@ struct TestSwiftly: AsyncParsableCommand { if NSUserName() == "root" { if try await fs.exists(atPath: "./post-install.sh") { - try currentPlatform.runProgram(shell.string, "./post-install.sh", quiet: false) + try await currentPlatform.runProgram(shell.string, "./post-install.sh", quiet: false) } swiftReady = true } else if try await fs.exists(atPath: "./post-install.sh") { diff --git a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift index 2a7f8c94..4561d5cb 100644 --- a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift +++ b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift @@ -204,7 +204,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { customEnv["CC"] = "\(cwd)/Tools/build-swiftly-release/musl-clang" customEnv["MUSL_PREFIX"] = "\(fs.home / ".swiftpm/swift-sdks/\(sdkName).artifactbundle/\(sdkName)/swift-linux-musl/musl-1.2.5.sdk/\(arch)/usr")" - try currentPlatform.runProgram( + try await currentPlatform.runProgram( "./configure", "--prefix=\(pkgConfigPath)", "--enable-shared=no",