Skip to content

Commit dd1d29c

Browse files
committed
Use Subprocess for process management
1 parent c82a975 commit dd1d29c

File tree

7 files changed

+110
-80
lines changed

7 files changed

+110
-80
lines changed

Package.resolved

Lines changed: 12 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ let package = Package(
3131
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.7.2"),
3232
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.2"),
3333
.package(url: "https://github.com/apple/swift-system", from: "1.4.2"),
34+
.package(url: "https://github.com/swiftlang/swift-subprocess", revision: "afc1f734feb29c3a1ebbd97cc1fe943f8e5d80e5"),
3435
// This dependency provides the correct version of the formatter so that you can run `swift run swiftformat Package.swift Plugins/ Sources/ Tests/`
3536
.package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.49.18"),
3637
],
@@ -67,6 +68,7 @@ let package = Package(
6768
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
6869
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
6970
.product(name: "SystemPackage", package: "swift-system"),
71+
.product(name: "Subprocess", package: "swift-subprocess"),
7072
],
7173
swiftSettings: swiftSettings,
7274
plugins: ["GenerateCommandModels"]

Sources/MacOSPlatform/MacOS.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public struct MacOS: Platform {
142142
try await sys.tar(.directory(installDir)).extract(.verbose, .archive(payload)).run(self, quiet: false)
143143
}
144144

145-
try self.runProgram((userHomeDir / ".swiftly/bin/swiftly").string, "init")
145+
try await self.runProgram((userHomeDir / ".swiftly/bin/swiftly").string, "init")
146146
}
147147

148148
public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose: Bool)

Sources/SwiftlyCore/ModeledCommandLine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ extension Runnable {
181181
newEnv = newValue
182182
}
183183

184-
try p.runProgram([executable] + args, quiet: quiet, env: newEnv)
184+
try await p.runProgram([executable] + args, quiet: quiet, env: newEnv)
185185
}
186186
}
187187

Sources/SwiftlyCore/Platform.swift

Lines changed: 90 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import Foundation
22
import SystemPackage
3+
import Subprocess
4+
#if os(macOS)
5+
import System
6+
#else
7+
import SystemPackage
8+
#endif
39

410
public struct PlatformDefinition: Codable, Equatable, Sendable {
511
/// The name of the platform as it is used in the Swift download URLs.
@@ -57,31 +63,31 @@ public struct RunProgramError: Swift.Error {
5763
public protocol Platform: Sendable {
5864
/// The platform-specific default location on disk for swiftly's home
5965
/// directory.
60-
var defaultSwiftlyHomeDir: FilePath { get }
66+
var defaultSwiftlyHomeDir: SystemPackage.FilePath { get }
6167

6268
/// The directory which stores the swiftly executable itself as well as symlinks
6369
/// to executables in the "bin" directory of the active toolchain.
6470
///
6571
/// If a mocked home directory is set, this will be the "bin" subdirectory of the home directory.
6672
/// If not, this will be the SWIFTLY_BIN_DIR environment variable if set. If that's also unset,
6773
/// this will default to the platform's default location.
68-
func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> FilePath
74+
func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> SystemPackage.FilePath
6975

7076
/// The "toolchains" subdirectory that contains the Swift toolchains managed by swiftly.
71-
func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> FilePath
77+
func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> SystemPackage.FilePath
7278

7379
/// The file extension of the downloaded toolchain for this platform.
7480
/// e.g. for Linux systems this is "tar.gz" and on macOS it's "pkg".
7581
var toolchainFileExtension: String { get }
7682

7783
/// Installs a toolchain from a file on disk pointed to by the given path.
7884
/// After this completes, a user can “use” the toolchain.
79-
func install(_ ctx: SwiftlyCoreContext, from: FilePath, version: ToolchainVersion, verbose: Bool)
85+
func install(_ ctx: SwiftlyCoreContext, from: SystemPackage.FilePath, version: ToolchainVersion, verbose: Bool)
8086
async throws
8187

8288
/// Extract swiftly from the provided downloaded archive and install
8389
/// ourselves from that.
84-
func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: FilePath) async throws
90+
func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: SystemPackage.FilePath) async throws
8591

8692
/// Uninstalls a toolchain associated with the given version.
8793
/// If this version is in use, the next latest version will be used afterwards.
@@ -111,14 +117,14 @@ public protocol Platform: Sendable {
111117
/// Downloads the signature file associated with the archive and verifies it matches the downloaded archive.
112118
/// Throws an error if the signature does not match.
113119
func verifyToolchainSignature(
114-
_ ctx: SwiftlyCoreContext, toolchainFile: ToolchainFile, archive: FilePath, verbose: Bool
120+
_ ctx: SwiftlyCoreContext, toolchainFile: ToolchainFile, archive: SystemPackage.FilePath, verbose: Bool
115121
)
116122
async throws
117123

118124
/// Downloads the signature file associated with the archive and verifies it matches the downloaded archive.
119125
/// Throws an error if the signature does not match.
120126
func verifySwiftlySignature(
121-
_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: FilePath, verbose: Bool
127+
_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: SystemPackage.FilePath, verbose: Bool
122128
) async throws
123129

124130
/// Detect the platform definition for this platform.
@@ -129,10 +135,10 @@ public protocol Platform: Sendable {
129135
func getShell() async throws -> String
130136

131137
/// Find the location where the toolchain should be installed.
132-
func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
138+
func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> SystemPackage.FilePath
133139

134140
/// Find the location of the toolchain binaries.
135-
func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
141+
func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> SystemPackage.FilePath
136142
}
137143

138144
extension Platform {
@@ -149,14 +155,14 @@ extension Platform {
149155
/// -- config.json
150156
/// ```
151157
///
152-
public func swiftlyHomeDir(_ ctx: SwiftlyCoreContext) -> FilePath {
158+
public func swiftlyHomeDir(_ ctx: SwiftlyCoreContext) -> SystemPackage.FilePath {
153159
ctx.mockedHomeDir
154160
?? ProcessInfo.processInfo.environment["SWIFTLY_HOME_DIR"].map { FilePath($0) }
155161
?? self.defaultSwiftlyHomeDir
156162
}
157163

158164
/// The path of the configuration file in swiftly's home directory.
159-
public func swiftlyConfigFile(_ ctx: SwiftlyCoreContext) -> FilePath {
165+
public func swiftlyConfigFile(_ ctx: SwiftlyCoreContext) -> SystemPackage.FilePath {
160166
self.swiftlyHomeDir(ctx) / "config.json"
161167
}
162168

@@ -216,7 +222,7 @@ extension Platform {
216222
}
217223
#endif
218224

219-
try self.runProgram([commandToRun] + arguments, env: newEnv)
225+
try await self.runProgram([commandToRun] + arguments, env: newEnv)
220226
}
221227

222228
/// Proxy the invocation of the provided command to the chosen toolchain and capture the output.
@@ -243,9 +249,9 @@ extension Platform {
243249
/// the exit code and program information.
244250
///
245251
public func runProgram(_ args: String..., quiet: Bool = false, env: [String: String]? = nil)
246-
throws
252+
async throws
247253
{
248-
try self.runProgram([String](args), quiet: quiet, env: env)
254+
try await self.runProgram([String](args), quiet: quiet, env: env)
249255
}
250256

251257
/// Run a program.
@@ -254,39 +260,65 @@ extension Platform {
254260
/// the exit code and program information.
255261
///
256262
public func runProgram(_ args: [String], quiet: Bool = false, env: [String: String]? = nil)
257-
throws
263+
async throws
258264
{
259-
let process = Process()
260-
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
261-
process.arguments = args
265+
if !quiet {
266+
let result = try await run(
267+
.path("/usr/bin/env"),
268+
arguments: .init(args),
269+
environment: env != nil ? .inherit.updating(env ?? [:]) : .inherit,
270+
output: .fileDescriptor(.standardError, closeAfterSpawningProcess: false),
271+
error: .fileDescriptor(.standardError, closeAfterSpawningProcess: false),
272+
)
262273

263-
if let env {
264-
process.environment = env
265-
}
274+
// TODO figure out how to set the process group
275+
// Attach this process to our process group so that Ctrl-C and other signals work
276+
/*let pgid = tcgetpgrp(STDOUT_FILENO)
277+
if pgid != -1 {
278+
tcsetpgrp(STDOUT_FILENO, process.processIdentifier)
279+
}
266280

267-
if quiet {
268-
process.standardOutput = nil
269-
process.standardError = nil
270-
}
281+
defer {
282+
if pgid != -1 {
283+
tcsetpgrp(STDOUT_FILENO, pgid)
284+
}
285+
}
271286

272-
try process.run()
273-
// Attach this process to our process group so that Ctrl-C and other signals work
274-
let pgid = tcgetpgrp(STDOUT_FILENO)
275-
if pgid != -1 {
276-
tcsetpgrp(STDOUT_FILENO, process.processIdentifier)
277-
}
287+
process.waitUntilExit()*/
278288

279-
defer {
289+
if case .exited(let code) = result.terminationStatus, code != 0 {
290+
throw RunProgramError(exitCode: code, program: args.first!, arguments: Array(args.dropFirst()))
291+
}
292+
} else {
293+
let result = try await run(
294+
.path("/usr/bin/env"),
295+
arguments: .init(args),
296+
environment: env != nil ? .inherit.updating(env ?? [:]) : .inherit,
297+
output: .discarded,
298+
error: .discarded,
299+
)
300+
301+
// TODO figure out how to set the process group
302+
// Attach this process to our process group so that Ctrl-C and other signals work
303+
/*let pgid = tcgetpgrp(STDOUT_FILENO)
280304
if pgid != -1 {
281-
tcsetpgrp(STDOUT_FILENO, pgid)
305+
tcsetpgrp(STDOUT_FILENO, process.processIdentifier)
282306
}
283-
}
284307

285-
process.waitUntilExit()
308+
defer {
309+
if pgid != -1 {
310+
tcsetpgrp(STDOUT_FILENO, pgid)
311+
}
312+
}
313+
314+
process.waitUntilExit()*/
286315

287-
guard process.terminationStatus == 0 else {
288-
throw RunProgramError(exitCode: process.terminationStatus, program: args.first!, arguments: Array(args.dropFirst()))
316+
if case .exited(let code) = result.terminationStatus, code != 0 {
317+
throw RunProgramError(exitCode: code, program: args.first!, arguments: Array(args.dropFirst()))
318+
}
289319
}
320+
321+
// TODO handle exits with a signal
290322
}
291323

292324
/// Run a program and capture its output.
@@ -308,22 +340,17 @@ extension Platform {
308340
public func runProgramOutput(_ program: String, _ args: [String], env: [String: String]? = nil)
309341
async throws -> String?
310342
{
311-
let process = Process()
312-
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
313-
process.arguments = [program] + args
314-
315-
if let env {
316-
process.environment = env
317-
}
318-
319-
let outPipe = Pipe()
320-
process.standardInput = FileHandle.nullDevice
321-
process.standardError = FileHandle.nullDevice
322-
process.standardOutput = outPipe
323-
324-
try process.run()
325-
// Attach this process to our process group so that Ctrl-C and other signals work
326-
let pgid = tcgetpgrp(STDOUT_FILENO)
343+
let result = try await run(
344+
.path("/usr/bin/env"),
345+
arguments: .init([program] + args),
346+
environment: env != nil ? .inherit.updating(env ?? [:]) : .inherit,
347+
input: .none,
348+
output: .string(limit: 10 * 1024 * 1024, encoding: UTF8.self),
349+
error: .discarded,
350+
)
351+
352+
// TODO Attach this process to our process group so that Ctrl-C and other signals work
353+
/*let pgid = tcgetpgrp(STDOUT_FILENO)
327354
if pgid != -1 {
328355
tcsetpgrp(STDOUT_FILENO, process.processIdentifier)
329356
}
@@ -332,28 +359,21 @@ extension Platform {
332359
tcsetpgrp(STDOUT_FILENO, pgid)
333360
}
334361
}
362+
*/
335363

336-
let outData = try outPipe.fileHandleForReading.readToEnd()
337-
338-
process.waitUntilExit()
339-
340-
guard process.terminationStatus == 0 else {
341-
throw RunProgramError(exitCode: process.terminationStatus, program: program, arguments: args)
364+
if case .exited(let code) = result.terminationStatus, code != 0 {
365+
throw RunProgramError(exitCode: code, program: args.first!, arguments: Array(args.dropFirst()))
342366
}
343367

344-
if let outData {
345-
return String(data: outData, encoding: .utf8)
346-
} else {
347-
return nil
348-
}
368+
return result.standardOutput
349369
}
350370

351371
// Install ourselves in the final location
352372
public func installSwiftlyBin(_ ctx: SwiftlyCoreContext) async throws {
353373
// First, let's find out where we are.
354374
let cmd = CommandLine.arguments[0]
355375

356-
var cmdAbsolute: FilePath?
376+
var cmdAbsolute: SystemPackage.FilePath?
357377

358378
if cmd.hasPrefix("/") {
359379
cmdAbsolute = FilePath(cmd)
@@ -385,7 +405,7 @@ extension Platform {
385405
// Proceed to installation only if we're in the user home directory, or a non-system location.
386406
let userHome = fs.home
387407

388-
let systemRoots: [FilePath] = ["/usr", "/opt", "/bin"]
408+
let systemRoots: [SystemPackage.FilePath] = ["/usr", "/opt", "/bin"]
389409

390410
guard cmdAbsolute.starts(with: userHome) || systemRoots.filter({ cmdAbsolute.starts(with: $0) }).first == nil else {
391411
return
@@ -421,12 +441,12 @@ extension Platform {
421441
}
422442

423443
// Find the location where swiftly should be executed.
424-
public func findSwiftlyBin(_ ctx: SwiftlyCoreContext) async throws -> FilePath? {
444+
public func findSwiftlyBin(_ ctx: SwiftlyCoreContext) async throws -> SystemPackage.FilePath? {
425445
let swiftlyHomeBin = self.swiftlyBinDir(ctx) / "swiftly"
426446

427447
// First, let's find out where we are.
428448
let cmd = CommandLine.arguments[0]
429-
var cmdAbsolute: FilePath?
449+
var cmdAbsolute: SystemPackage.FilePath?
430450
if cmd.hasPrefix("/") {
431451
cmdAbsolute = FilePath(cmd)
432452
} else {
@@ -457,7 +477,7 @@ extension Platform {
457477
}
458478
}
459479

460-
let systemRoots: [FilePath] = ["/usr", "/opt", "/bin"]
480+
let systemRoots: [SystemPackage.FilePath] = ["/usr", "/opt", "/bin"]
461481

462482
// If we are system managed then we know where swiftly should be.
463483
let userHome = fs.home
@@ -479,7 +499,7 @@ extension Platform {
479499
return try await fs.exists(atPath: swiftlyHomeBin) ? swiftlyHomeBin : nil
480500
}
481501

482-
public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> FilePath
502+
public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) async throws -> SystemPackage.FilePath
483503
{
484504
(try await self.findToolchainLocation(ctx, toolchain)) / "usr/bin"
485505
}

0 commit comments

Comments
 (0)