Skip to content

Commit b326647

Browse files
authored
Merge pull request #64 from zero-ide/feature/dockerfile-priority
fix(execution): add Dockerfile-first detection with safe fallback
2 parents e7db238 + 0bb0469 commit b326647

2 files changed

Lines changed: 65 additions & 9 deletions

File tree

Sources/Zero/Services/ExecutionService.swift

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,17 @@ class ExecutionService: ObservableObject {
105105

106106
func detectRunCommand(container: String) async throws -> String {
107107
let config = try buildConfigService.load()
108-
109-
// 순서대로 체크 (우선순위)
110-
// 1. Swift
108+
109+
if try dockerService.fileExists(container: container, path: "Dockerfile") {
110+
if canUseDockerfileStrategy(container: container) {
111+
return "docker build -t zero-runner . && docker run --rm zero-runner"
112+
}
113+
}
114+
111115
if try dockerService.fileExists(container: container, path: "Package.swift") {
112116
return "swift run"
113117
}
114118

115-
// 2. Maven (pom.xml)
116119
if try dockerService.fileExists(container: container, path: "pom.xml") {
117120
if config.buildTool == .maven {
118121
// Spring Boot 플러그인 확인
@@ -124,7 +127,6 @@ class ExecutionService: ObservableObject {
124127
return "mvn clean install"
125128
}
126129

127-
// 3. Gradle (build.gradle 또는 build.gradle.kts)
128130
let hasBuildGradle = try dockerService.fileExists(container: container, path: "build.gradle")
129131
let hasBuildGradleKts = try dockerService.fileExists(container: container, path: "build.gradle.kts")
130132
if hasBuildGradle || hasBuildGradleKts {
@@ -138,28 +140,35 @@ class ExecutionService: ObservableObject {
138140
return "gradle build"
139141
}
140142

141-
// 4. Node.js
142143
if try dockerService.fileExists(container: container, path: "package.json") {
143144
return "npm start"
144145
}
145146

146-
// 5. Python
147147
if try dockerService.fileExists(container: container, path: "main.py") {
148148
return "python3 main.py"
149149
}
150150

151-
// 6. Java (단일 파일)
152151
if try dockerService.fileExists(container: container, path: "Main.java") {
153152
return "javac Main.java && java Main"
154153
}
155154

156-
// 7. Go
157155
if try dockerService.fileExists(container: container, path: "go.mod") {
158156
return "go run ."
159157
}
160158

161159
throw NSError(domain: "ExecutionService", code: 404, userInfo: [NSLocalizedDescriptionKey: "Cannot detect project type"])
162160
}
161+
162+
private func canUseDockerfileStrategy(container: String) -> Bool {
163+
guard let output = try? dockerService.executeShell(
164+
container: container,
165+
script: "command -v docker >/dev/null 2>&1 && echo yes || echo no"
166+
) else {
167+
return false
168+
}
169+
170+
return output.contains("yes")
171+
}
163172

164173
/// Spring Boot 프로젝트 여부 확인
165174
private func isSpringBootProject(container: String, buildTool: BuildConfiguration.BuildTool) async throws -> Bool {

Tests/ZeroTests/ExecutionServiceTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,48 @@ final class ExecutionServiceTests: XCTestCase {
3737
// Then
3838
XCTAssertEqual(command, "npm start")
3939
}
40+
41+
func testDetectRunCommand_DockerfileTakesPriorityOverSwift() async throws {
42+
// Given
43+
mockDocker.dockerCommandAvailable = true
44+
mockDocker.fileExistenceResults = [
45+
"Dockerfile": true,
46+
"Package.swift": true
47+
]
48+
49+
// When
50+
let command = try await service.detectRunCommand(container: "test-container")
51+
52+
// Then
53+
XCTAssertEqual(command, "docker build -t zero-runner . && docker run --rm zero-runner")
54+
}
55+
56+
func testDetectRunCommand_DockerfileStrategyWhenOnlyDockerfileExists() async throws {
57+
// Given
58+
mockDocker.dockerCommandAvailable = true
59+
mockDocker.fileExistenceResults = ["Dockerfile": true]
60+
61+
// When
62+
let command = try await service.detectRunCommand(container: "test-container")
63+
64+
// Then
65+
XCTAssertEqual(command, "docker build -t zero-runner . && docker run --rm zero-runner")
66+
}
67+
68+
func testDetectRunCommand_DockerfileFallsBackWhenDockerUnavailable() async throws {
69+
// Given
70+
mockDocker.dockerCommandAvailable = false
71+
mockDocker.fileExistenceResults = [
72+
"Dockerfile": true,
73+
"Package.swift": true
74+
]
75+
76+
// When
77+
let command = try await service.detectRunCommand(container: "test-container")
78+
79+
// Then
80+
XCTAssertEqual(command, "swift run")
81+
}
4082

4183
func testExecute_Success() async {
4284
// Given
@@ -139,6 +181,7 @@ class MockExecutionDockerService: DockerServiceProtocol {
139181
var cancelCurrentExecutionCalled = false
140182
var streamingChunks: [String] = []
141183
var interChunkDelayNanoseconds: UInt64 = 0
184+
var dockerCommandAvailable = true
142185

143186
func checkInstallation() throws -> Bool { return true }
144187

@@ -154,6 +197,10 @@ class MockExecutionDockerService: DockerServiceProtocol {
154197
func runContainer(image: String, name: String) throws -> String { return "" }
155198
func executeCommand(container: String, command: String) throws -> String { return "" }
156199
func executeShell(container: String, script: String) throws -> String {
200+
if script.contains("command -v docker") {
201+
return dockerCommandAvailable ? "yes" : "no"
202+
}
203+
157204
if shouldBlockUntilCancelled {
158205
while !cancelCurrentExecutionCalled {
159206
usleep(10_000)

0 commit comments

Comments
 (0)