Skip to content

Commit 6b06a79

Browse files
Merge pull request swiftwasm#415 from swiftwasm/yt/rename-which-override-env-format
BridgeJS: Rename `which` override env-var format to `JAVASCRIPTKIT_<EXECUTABLE>_EXEC`
2 parents 64fe674 + f0624bb commit 6b06a79

File tree

4 files changed

+205
-11
lines changed

4 files changed

+205
-11
lines changed

Plugins/BridgeJS/Sources/TS2Skeleton/TS2Skeleton.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@ import BridgeJSCore
1919
import BridgeJSSkeleton
2020
#endif
2121

22-
internal func which(_ executable: String) throws -> URL {
22+
internal func which(
23+
_ executable: String,
24+
environment: [String: String] = ProcessInfo.processInfo.environment
25+
) throws -> URL {
2326
func checkCandidate(_ candidate: URL) -> Bool {
2427
var isDirectory: ObjCBool = false
2528
let fileExists = FileManager.default.fileExists(atPath: candidate.path, isDirectory: &isDirectory)
2629
return fileExists && !isDirectory.boolValue && FileManager.default.isExecutableFile(atPath: candidate.path)
2730
}
2831
do {
2932
// Check overriding environment variable
30-
let envVariable = executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_PATH"
31-
if let path = ProcessInfo.processInfo.environment[envVariable] {
32-
let url = URL(fileURLWithPath: path).appendingPathComponent(executable)
33+
let envVariable = "JAVASCRIPTKIT_" + executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_EXEC"
34+
if let executablePath = environment[envVariable] {
35+
let url = URL(fileURLWithPath: executablePath)
3336
if checkCandidate(url) {
3437
return url
3538
}
@@ -41,7 +44,7 @@ internal func which(_ executable: String) throws -> URL {
4144
#else
4245
pathSeparator = ":"
4346
#endif
44-
let paths = ProcessInfo.processInfo.environment["PATH"]!.split(separator: pathSeparator)
47+
let paths = environment["PATH"]?.split(separator: pathSeparator) ?? []
4548
for path in paths {
4649
let url = URL(fileURLWithPath: String(path)).appendingPathComponent(executable)
4750
if checkCandidate(url) {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import Testing
2+
import Foundation
3+
@testable import TS2Skeleton
4+
@testable import BridgeJSCore
5+
6+
@Suite struct WhichTests {
7+
8+
// MARK: - Helper Functions
9+
10+
private static var pathSeparator: String {
11+
#if os(Windows)
12+
return ";"
13+
#else
14+
return ":"
15+
#endif
16+
}
17+
18+
// MARK: - Successful Path Resolution Tests
19+
20+
@Test func whichFindsExecutableInPath() throws {
21+
try withTemporaryDirectory { tempDir, _ in
22+
let execFile = tempDir.appendingPathComponent("testexec")
23+
try "#!/bin/sh\necho 'test'".write(to: execFile, atomically: true, encoding: .utf8)
24+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: execFile.path)
25+
26+
let environment = ["PATH": tempDir.path]
27+
28+
let result = try which("testexec", environment: environment)
29+
30+
#expect(result.path == execFile.path)
31+
}
32+
}
33+
34+
@Test func whichReturnsFirstMatchInPath() throws {
35+
try withTemporaryDirectory { tempDir1, _ in
36+
try withTemporaryDirectory { tempDir2, _ in
37+
let exec1 = tempDir1.appendingPathComponent("testexec")
38+
let exec2 = tempDir2.appendingPathComponent("testexec")
39+
40+
// Create executable files in both directories
41+
try "#!/bin/sh\necho 'first'".write(to: exec1, atomically: true, encoding: .utf8)
42+
try "#!/bin/sh\necho 'second'".write(to: exec2, atomically: true, encoding: .utf8)
43+
44+
// Make files executable
45+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: exec1.path)
46+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: exec2.path)
47+
48+
let pathEnv = "\(tempDir1.path)\(Self.pathSeparator)\(tempDir2.path)"
49+
let environment = ["PATH": pathEnv]
50+
51+
let result = try which("testexec", environment: environment)
52+
53+
// Should return the first one found
54+
#expect(result.path == exec1.path)
55+
}
56+
}
57+
}
58+
59+
// MARK: - Environment Variable Override Tests
60+
61+
@Test func whichUsesEnvironmentVariableOverride() throws {
62+
try withTemporaryDirectory { tempDir, _ in
63+
let customExec = tempDir.appendingPathComponent("mynode")
64+
try "#!/bin/sh\necho 'custom node'".write(to: customExec, atomically: true, encoding: .utf8)
65+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: customExec.path)
66+
67+
let environment = [
68+
"PATH": "/nonexistent/path",
69+
"JAVASCRIPTKIT_NODE_EXEC": customExec.path,
70+
]
71+
72+
let result = try which("node", environment: environment)
73+
74+
#expect(result.path == customExec.path)
75+
}
76+
}
77+
78+
@Test func whichHandlesHyphenatedExecutableNames() throws {
79+
try withTemporaryDirectory { tempDir, _ in
80+
let customExec = tempDir.appendingPathComponent("my-exec")
81+
try "#!/bin/sh\necho 'hyphenated'".write(to: customExec, atomically: true, encoding: .utf8)
82+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: customExec.path)
83+
84+
let environment = [
85+
"PATH": "/nonexistent/path",
86+
"JAVASCRIPTKIT_MY_EXEC_EXEC": customExec.path,
87+
]
88+
89+
let result = try which("my-exec", environment: environment)
90+
91+
#expect(result.path == customExec.path)
92+
}
93+
}
94+
95+
@Test func whichPrefersEnvironmentOverridePath() throws {
96+
try withTemporaryDirectory { tempDir1, _ in
97+
try withTemporaryDirectory { tempDir2, _ in
98+
let pathExec = tempDir1.appendingPathComponent("testexec")
99+
let envExec = tempDir2.appendingPathComponent("testexec")
100+
101+
try "#!/bin/sh\necho 'from path'".write(to: pathExec, atomically: true, encoding: .utf8)
102+
try "#!/bin/sh\necho 'from env'".write(to: envExec, atomically: true, encoding: .utf8)
103+
104+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: pathExec.path)
105+
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: envExec.path)
106+
107+
let environment = [
108+
"PATH": tempDir1.path,
109+
"JAVASCRIPTKIT_TESTEXEC_EXEC": envExec.path,
110+
]
111+
112+
let result = try which("testexec", environment: environment)
113+
114+
// Should prefer environment variable over PATH
115+
#expect(result.path == envExec.path)
116+
}
117+
}
118+
}
119+
120+
// MARK: - Error Handling Tests
121+
122+
@Test func whichThrowsWhenExecutableNotFound() throws {
123+
let environment = ["PATH": "/nonexistent\(Self.pathSeparator)/also/nonexistent"]
124+
125+
#expect(throws: BridgeJSCoreError.self) {
126+
_ = try which("nonexistent_executable_12345", environment: environment)
127+
}
128+
}
129+
130+
@Test func whichThrowsWhenEnvironmentPathIsInvalid() throws {
131+
try withTemporaryDirectory { tempDir, _ in
132+
let nonExecFile = tempDir.appendingPathComponent("notexecutable")
133+
try "not executable".write(to: nonExecFile, atomically: true, encoding: .utf8)
134+
135+
let environment = [
136+
"PATH": tempDir.path,
137+
"JAVASCRIPTKIT_NOTEXECUTABLE_EXEC": nonExecFile.path,
138+
]
139+
140+
#expect(throws: BridgeJSCoreError.self) {
141+
_ = try which("notexecutable", environment: environment)
142+
}
143+
}
144+
}
145+
146+
@Test func whichThrowsWhenPathPointsToDirectory() throws {
147+
try withTemporaryDirectory { tempDir, _ in
148+
let environment = [
149+
"PATH": "/nonexistent/path",
150+
"JAVASCRIPTKIT_TESTEXEC_EXEC": tempDir.path,
151+
]
152+
153+
#expect(throws: BridgeJSCoreError.self) {
154+
_ = try which("testexec", environment: environment)
155+
}
156+
}
157+
}
158+
159+
// MARK: - Edge Case Tests
160+
161+
@Test func whichHandlesEmptyPath() throws {
162+
let environment = ["PATH": ""]
163+
164+
#expect(throws: BridgeJSCoreError.self) {
165+
_ = try which("anyexec", environment: environment)
166+
}
167+
}
168+
169+
@Test func whichHandlesMissingPathEnvironment() throws {
170+
let environment: [String: String] = [:]
171+
172+
#expect(throws: BridgeJSCoreError.self) {
173+
_ = try which("anyexec", environment: environment)
174+
}
175+
}
176+
177+
@Test func whichIgnoresNonExecutableFiles() throws {
178+
try withTemporaryDirectory { tempDir, _ in
179+
let nonExecFile = tempDir.appendingPathComponent("testfile")
180+
try "content".write(to: nonExecFile, atomically: true, encoding: .utf8)
181+
// Don't set executable permissions
182+
183+
let environment = ["PATH": tempDir.path]
184+
185+
#expect(throws: BridgeJSCoreError.self) {
186+
_ = try which("testfile", environment: environment)
187+
}
188+
}
189+
}
190+
}

Plugins/PackageToJS/Sources/PackageToJS.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ final class DefaultPackagingSystem: PackagingSystem {
295295
private let printWarning: (String) -> Void
296296
private let which: (String) throws -> URL
297297

298-
init(printWarning: @escaping (String) -> Void, which: @escaping (String) throws -> URL = which(_:)) {
298+
init(printWarning: @escaping (String) -> Void, which: @escaping (String) throws -> URL) {
299299
self.printWarning = printWarning
300300
self.which = which
301301
}
@@ -323,16 +323,17 @@ final class DefaultPackagingSystem: PackagingSystem {
323323
}
324324

325325
internal func which(_ executable: String) throws -> URL {
326+
let environment = ProcessInfo.processInfo.environment
326327
func checkCandidate(_ candidate: URL) -> Bool {
327328
var isDirectory: ObjCBool = false
328329
let fileExists = FileManager.default.fileExists(atPath: candidate.path, isDirectory: &isDirectory)
329330
return fileExists && !isDirectory.boolValue && FileManager.default.isExecutableFile(atPath: candidate.path)
330331
}
331332
do {
332333
// Check overriding environment variable
333-
let envVariable = executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_PATH"
334-
if let path = ProcessInfo.processInfo.environment[envVariable] {
335-
let url = URL(fileURLWithPath: path).appendingPathComponent(executable)
334+
let envVariable = "JAVASCRIPTKIT_" + executable.uppercased().replacingOccurrences(of: "-", with: "_") + "_EXEC"
335+
if let executablePath = environment[envVariable] {
336+
let url = URL(fileURLWithPath: executablePath)
336337
if checkCandidate(url) {
337338
return url
338339
}
@@ -344,7 +345,7 @@ internal func which(_ executable: String) throws -> URL {
344345
#else
345346
pathSeparator = ":"
346347
#endif
347-
let paths = ProcessInfo.processInfo.environment["PATH"]!.split(separator: pathSeparator)
348+
let paths = environment["PATH"]?.split(separator: pathSeparator) ?? []
348349
for path in paths {
349350
let url = URL(fileURLWithPath: String(path)).appendingPathComponent(executable)
350351
if checkCandidate(url) {

Plugins/PackageToJS/Sources/PackageToJSPlugin.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ extension PackagingPlanner {
762762
) {
763763
let outputBaseName = outputDir.lastPathComponent
764764
let (configuration, triple) = PackageToJS.deriveBuildConfiguration(wasmProductArtifact: wasmProductArtifact)
765-
let system = DefaultPackagingSystem(printWarning: printStderr)
765+
let system = DefaultPackagingSystem(printWarning: printStderr, which: which(_:))
766766
self.init(
767767
options: options,
768768
packageId: context.package.id,

0 commit comments

Comments
 (0)