Skip to content

Commit ebbf238

Browse files
authored
Merge pull request #41 from AgoraIO-Community/feat/design-update
fix(macos): launch node with login-shell PATH so codex finds node
2 parents 7046fc9 + 1aa5def commit ebbf238

4 files changed

Lines changed: 60 additions & 1 deletion

File tree

macos/Sources/NewbroExecutor/AppModel.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ final class AppModel: ObservableObject {
2121
// so they never freeze the main actor / menu.
2222
private let controlQueue = DispatchQueue(label: "newbro.ui.control")
2323

24+
/// Login-shell PATH so node subprocesses (and the `node`-based `codex`
25+
/// they exec) resolve under the app's otherwise-minimal launchd env.
26+
private let childEnv = RuntimeLocator.childEnvironment()
27+
2428
init() {
2529
let loc = locator
30+
let env = RuntimeLocator.childEnvironment()
2631
self.loginItem = LoginItem(appPath: Bundle.main.bundlePath)
2732
self.supervisor = ProfileSupervisor(
2833
processFactory: .init(make: { argv, onLine, onExit in
29-
NodeProcess(argv: argv, onLine: onLine, onExit: onExit)
34+
NodeProcess(argv: argv, environment: env, onLine: onLine, onExit: onExit)
3035
}),
3136
argvBuilder: { profile in
3237
loc.nodeArgv(for: profile) ?? []
@@ -102,6 +107,7 @@ final class AppModel: ObservableObject {
102107
let argv = locator.installCommandArgv()
103108
updateInstallProcess = NodeProcess(
104109
argv: argv,
110+
environment: childEnv,
105111
onLine: { _ in },
106112
onExit: { [weak self] code in
107113
Task { @MainActor in
@@ -213,6 +219,7 @@ final class AppModel: ObservableObject {
213219
// Retain the process; otherwise it is deallocated before it runs.
214220
installProcess = NodeProcess(
215221
argv: argv,
222+
environment: childEnv,
216223
onLine: { [weak self] line in
217224
Task { @MainActor in self?.installLog += line + "\n" }
218225
},

macos/Sources/NewbroExecutorCore/NodeProcess.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@ public protocol NodeProcessProtocol: AnyObject {
88

99
public final class NodeProcess: NodeProcessProtocol {
1010
private let argv: [String]
11+
private let environment: [String: String]?
1112
private let onLine: (String) -> Void
1213
private let onExit: (Int32) -> Void
1314
private var process: Process?
1415
private let queue = DispatchQueue(label: "newbro.node-process")
1516
private var buffer = Data()
1617

1718
public init(argv: [String],
19+
environment: [String: String]? = nil,
1820
onLine: @escaping (String) -> Void,
1921
onExit: @escaping (Int32) -> Void) {
2022
self.argv = argv
23+
self.environment = environment
2124
self.onLine = onLine
2225
self.onExit = onExit
2326
}
@@ -29,6 +32,7 @@ public final class NodeProcess: NodeProcessProtocol {
2932
let proc = Process()
3033
proc.executableURL = URL(fileURLWithPath: argv[0])
3134
proc.arguments = Array(argv.dropFirst())
35+
if let environment { proc.environment = environment }
3236
let pipe = Pipe()
3337
proc.standardOutput = pipe
3438
proc.standardError = pipe

macos/Sources/NewbroExecutorCore/RuntimeLocator.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,36 @@ public struct RuntimeLocator {
7676
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
7777
return output.isEmpty ? nil : output
7878
}
79+
80+
/// The login shell's PATH. A menu-bar/login-item app inherits a minimal
81+
/// launchd PATH, so node subprocesses (and tools they exec, like the
82+
/// `node`-based `codex` binary) must run with the user's real PATH.
83+
public static func loginShellPath() -> String? {
84+
let proc = Process()
85+
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
86+
proc.arguments = ["-lc", "printf %s \"$PATH\""]
87+
let pipe = Pipe()
88+
proc.standardOutput = pipe
89+
proc.standardError = FileHandle.nullDevice
90+
do {
91+
try proc.run()
92+
} catch {
93+
return nil
94+
}
95+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
96+
proc.waitUntilExit()
97+
let output = String(data: data, encoding: .utf8)?
98+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
99+
return output.isEmpty ? nil : output
100+
}
101+
102+
/// The current process environment with PATH replaced by the login-shell
103+
/// PATH (when available), suitable for launching node subprocesses.
104+
public static func childEnvironment() -> [String: String] {
105+
var env = ProcessInfo.processInfo.environment
106+
if let shellPath = loginShellPath(), !shellPath.isEmpty {
107+
env["PATH"] = shellPath
108+
}
109+
return env
110+
}
79111
}

macos/Tests/NewbroExecutorCoreTests/NodeProcessTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ final class NodeProcessTests: XCTestCase {
7676
XCTAssertFalse(proc.isRunning)
7777
}
7878

79+
func testHonorsCustomEnvironment() {
80+
// The menu-bar app launches with a minimal PATH; it must be able to
81+
// override the child's environment (so e.g. `node`/`codex` resolve).
82+
let lines = Box<[String]>([])
83+
let exited = expectation(description: "exited")
84+
let proc = NodeProcess(
85+
argv: ["/bin/sh", "-c", "printf 'PATHIS=%s\\n' \"$PATH\""],
86+
environment: ["PATH": "/custom/bin:/usr/bin"],
87+
onLine: { line in lines.mutate { $0.append(line) } },
88+
onExit: { _ in exited.fulfill() }
89+
)
90+
proc.start()
91+
wait(for: [exited], timeout: 10)
92+
XCTAssertTrue(lines.value.contains("PATHIS=/custom/bin:/usr/bin"))
93+
}
94+
7995
func testStopTerminatesLongRunner() {
8096
let started = expectation(description: "started")
8197
started.assertForOverFulfill = false

0 commit comments

Comments
 (0)