From 9601f9d28641158b2d7c1248e3e26bfa6048a5a8 Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Wed, 24 Dec 2025 00:40:43 +0900 Subject: [PATCH 1/2] Fix relative path resolution in entrypoint Resolve relative paths (e.g., ./rustc) in --entrypoint flag by combining them with the working directory. This enables commands like: container run -w /usr/local/cargo/bin --entrypoint ./rustc rust:latest Fixes #962 --- .../ContainerAPIService/Client/Parser.swift | 35 ++++- .../ContainerAPIClientTests/ParserTest.swift | 132 ++++++++++++++++++ 2 files changed, 164 insertions(+), 3 deletions(-) diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index 74319cd7..a79b927d 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -251,16 +251,45 @@ public struct Parser { var hasEntrypointOverride: Bool = false // ensure the entrypoint is honored if it has been explicitly set by the user if let entrypoint = managementFlags.entrypoint, !entrypoint.isEmpty { - result = [entrypoint] + if entrypoint.hasPrefix("/") { + result = [entrypoint] + } else { + let resolved = URL(fileURLWithPath: workingDir) + .appendingPathComponent(entrypoint) + .standardized + .path + result = [resolved] + } hasEntrypointOverride = true } else if let entrypoint = config?.entrypoint, !entrypoint.isEmpty { - result = entrypoint + if let first = entrypoint.first, !first.hasPrefix("/") { + var resolved = entrypoint + resolved[0] = + URL(fileURLWithPath: workingDir) + .appendingPathComponent(first) + .standardized + .path + result = resolved + } else { + result = entrypoint + } } + if !arguments.isEmpty { result.append(contentsOf: arguments) } else { if let cmd = config?.cmd, !hasEntrypointOverride, !cmd.isEmpty { - result.append(contentsOf: cmd) + if let first = cmd.first, !first.hasPrefix("/") { + var resolved = cmd + resolved[0] = + URL(fileURLWithPath: workingDir) + .appendingPathComponent(first) + .standardized + .path + result.append(contentsOf: resolved) + } else { + result.append(contentsOf: cmd) + } } } return result.count > 0 ? result : nil diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index b88b4c5a..3d7acc71 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -14,8 +14,10 @@ // limitations under the License. //===----------------------------------------------------------------------===// +import ArgumentParser import ContainerizationError import ContainerizationExtras +import ContainerizationOCI import Foundation import Testing @@ -845,4 +847,134 @@ struct ParserTest { return error.description.contains("invalid property format") } } + + @Test + func testProcessEntrypointRelativePathWithDotSlash() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/usr/local/cargo/bin"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "./rustc"]) + + let result = try Parser.process( + arguments: ["--version"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/usr/local/cargo/bin/rustc") + #expect(result.arguments == ["--version"]) + #expect(result.workingDirectory == "/usr/local/cargo/bin") + } + + @Test + func testProcessEntrypointRelativePathWithoutPrefix() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/usr/bin"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "python3"]) + + let result = try Parser.process( + arguments: ["-c", "print('hello')"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/usr/bin/python3") + #expect(result.arguments == ["-c", "print('hello')"]) + } + + @Test + func testProcessEntrypointAbsolutePathUnchanged() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/home/user"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "/bin/bash"]) + + let result = try Parser.process( + arguments: ["-c", "echo hello"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/bin/bash") + #expect(result.arguments == ["-c", "echo hello"]) + } + + @Test + func testProcessEntrypointRelativePathNormalization() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/usr/local/bin"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "../lib/node"]) + + let result = try Parser.process( + arguments: ["--version"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/usr/local/lib/node") + } + + @Test + func testProcessEntrypointRelativePathWithDefaultWorkdir() throws { + let processFlags = try Flags.Process.parse([]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "./app"]) + + let result = try Parser.process( + arguments: [], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/app") + } + + @Test + func testProcessEntrypointRelativePathWithComplexPath() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/home/user/project"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "./bin/../scripts/./run.sh"]) + + let result = try Parser.process( + arguments: [], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "/home/user/project/scripts/run.sh") + } + + @Test + func testProcessRelativeEntrypointInImageConfig() throws { + let config = ContainerizationOCI.ImageConfig( + entrypoint: ["./start.sh"], + workingDir: "/app" + ) + + let result = try Parser.process( + arguments: [], + processFlags: try Flags.Process.parse([]), + managementFlags: try Flags.Management.parse([]), + config: config + ) + + #expect(result.executable == "/app/start.sh") + } + + @Test + func testProcessRelativeCmdInImageConfig() throws { + let config = ContainerizationOCI.ImageConfig( + entrypoint: nil, + cmd: ["./run.py", "--fast"], + workingDir: "/scripts" + ) + + let result = try Parser.process( + arguments: [], + processFlags: try Flags.Process.parse([]), + managementFlags: try Flags.Management.parse([]), + config: config + ) + + #expect(result.executable == "/scripts/run.py") + #expect(result.arguments == ["--fast"]) + } } From fde51a5f9e3e00dbddf82415aeddcad39a4f6a0f Mon Sep 17 00:00:00 2001 From: ParkSeongGeun Date: Thu, 8 Jan 2026 20:30:23 +0900 Subject: [PATCH 2/2] Refactor entrypoint and cmd path resolution logic --- .../ContainerAPIService/Client/Parser.swift | 55 +++++++++---------- .../ContainerAPIClientTests/ParserTest.swift | 20 ++++++- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/Sources/Services/ContainerAPIService/Client/Parser.swift b/Sources/Services/ContainerAPIService/Client/Parser.swift index a79b927d..753f9818 100644 --- a/Sources/Services/ContainerAPIService/Client/Parser.swift +++ b/Sources/Services/ContainerAPIService/Client/Parser.swift @@ -251,45 +251,25 @@ public struct Parser { var hasEntrypointOverride: Bool = false // ensure the entrypoint is honored if it has been explicitly set by the user if let entrypoint = managementFlags.entrypoint, !entrypoint.isEmpty { - if entrypoint.hasPrefix("/") { - result = [entrypoint] - } else { - let resolved = URL(fileURLWithPath: workingDir) - .appendingPathComponent(entrypoint) - .standardized - .path - result = [resolved] - } + result = [resolveExecutablePath(entrypoint, workingDir: workingDir)] hasEntrypointOverride = true } else if let entrypoint = config?.entrypoint, !entrypoint.isEmpty { - if let first = entrypoint.first, !first.hasPrefix("/") { - var resolved = entrypoint - resolved[0] = - URL(fileURLWithPath: workingDir) - .appendingPathComponent(first) - .standardized - .path - result = resolved - } else { - result = entrypoint + var resolved = entrypoint + if let first = entrypoint.first { + resolved[0] = resolveExecutablePath(first, workingDir: workingDir) } + result = resolved } if !arguments.isEmpty { result.append(contentsOf: arguments) } else { if let cmd = config?.cmd, !hasEntrypointOverride, !cmd.isEmpty { - if let first = cmd.first, !first.hasPrefix("/") { - var resolved = cmd - resolved[0] = - URL(fileURLWithPath: workingDir) - .appendingPathComponent(first) - .standardized - .path - result.append(contentsOf: resolved) - } else { - result.append(contentsOf: cmd) + var resolved = cmd + if let first = cmd.first { + resolved[0] = resolveExecutablePath(first, workingDir: workingDir) } + result.append(contentsOf: resolved) } } return result.count > 0 ? result : nil @@ -906,4 +886,21 @@ public struct Parser { default: return nil } } + + // MARK: Private + + private static func resolveExecutablePath(_ path: String, workingDir: String) -> String { + if path.hasPrefix("/") { + return path + } + + if path.hasPrefix("./") || path.hasPrefix("../") { + return URL(fileURLWithPath: workingDir) + .appendingPathComponent(path) + .standardized + .path + } + + return path + } } diff --git a/Tests/ContainerAPIClientTests/ParserTest.swift b/Tests/ContainerAPIClientTests/ParserTest.swift index 3d7acc71..6b70c339 100644 --- a/Tests/ContainerAPIClientTests/ParserTest.swift +++ b/Tests/ContainerAPIClientTests/ParserTest.swift @@ -866,7 +866,7 @@ struct ParserTest { } @Test - func testProcessEntrypointRelativePathWithoutPrefix() throws { + func testProcessEntrypointBareCommand() throws { let processFlags = try Flags.Process.parse(["--cwd", "/usr/bin"]) let managementFlags = try Flags.Management.parse(["--entrypoint", "python3"]) @@ -877,10 +877,26 @@ struct ParserTest { config: nil ) - #expect(result.executable == "/usr/bin/python3") + #expect(result.executable == "python3") #expect(result.arguments == ["-c", "print('hello')"]) } + @Test + func testProcessEntrypointBareCommandWithRootWorkdir() throws { + let processFlags = try Flags.Process.parse(["--cwd", "/"]) + let managementFlags = try Flags.Management.parse(["--entrypoint", "ls"]) + + let result = try Parser.process( + arguments: ["-la"], + processFlags: processFlags, + managementFlags: managementFlags, + config: nil + ) + + #expect(result.executable == "ls") + #expect(result.arguments == ["-la"]) + } + @Test func testProcessEntrypointAbsolutePathUnchanged() throws { let processFlags = try Flags.Process.parse(["--cwd", "/home/user"])