Skip to content

Commit c53a388

Browse files
authored
fix(agents): correctly expand PROGRAM_MAIN agrument (#643)
1 parent 3e63f1f commit c53a388

File tree

3 files changed

+17
-101
lines changed

3 files changed

+17
-101
lines changed

pkg/agentfs/docker.go

Lines changed: 8 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,18 @@ package agentfs
1717
import (
1818
"bytes"
1919
"embed"
20-
"encoding/json"
2120
"fmt"
2221
"os"
2322
"path/filepath"
2423
"slices"
2524
"strings"
25+
"text/template"
2626

2727
"github.com/charmbracelet/huh"
2828
"github.com/moby/patternmatcher"
2929
"github.com/moby/patternmatcher/ignorefile"
3030

3131
"github.com/livekit/livekit-cli/v2/pkg/util"
32-
"github.com/livekit/protocol/logger"
3332
)
3433

3534
//go:embed examples/*
@@ -62,6 +61,7 @@ func CreateDockerfile(dir string, projectType ProjectType, settingsMap map[strin
6261
if err != nil {
6362
return err
6463
}
64+
6565
dockerIgnoreContent, err = fs.ReadFile("examples/" + string(projectType) + ".dockerignore")
6666
if err != nil {
6767
return err
@@ -159,95 +159,11 @@ func validateEntrypoint(dir string, dockerfileContent []byte, dockerignoreConten
159159
return nil, err
160160
}
161161

162-
lines := bytes.Split(dockerfileContent, []byte("\n"))
163-
var result bytes.Buffer
164-
for i := range lines {
165-
line := lines[i]
166-
trimmedLine := bytes.TrimSpace(line)
167-
168-
if bytes.HasPrefix(trimmedLine, []byte("ARG PROGRAM_MAIN")) {
169-
result.WriteString(fmt.Sprintf("ARG PROGRAM_MAIN=\"%s\"", newEntrypoint))
170-
} else if bytes.HasPrefix(trimmedLine, []byte("ENTRYPOINT")) {
171-
// Extract the current entrypoint file
172-
parts := bytes.Fields(trimmedLine)
173-
if len(parts) < 2 {
174-
return nil, fmt.Errorf("invalid ENTRYPOINT format")
175-
}
176-
177-
// Handle both JSON array and shell format
178-
var currentEntrypoint string
179-
if bytes.HasPrefix(parts[1], []byte("[")) {
180-
// JSON array format: ENTRYPOINT ["python", "app.py"]
181-
// Get the last element before the closing bracket
182-
jsonStr := bytes.Join(parts[1:], []byte(" "))
183-
var entrypointArray []string
184-
if err := json.Unmarshal(jsonStr, &entrypointArray); err != nil {
185-
return nil, fmt.Errorf("invalid ENTRYPOINT JSON format: %v", err)
186-
}
187-
if len(entrypointArray) > 0 {
188-
currentEntrypoint = entrypointArray[len(entrypointArray)-1]
189-
}
190-
} else {
191-
// Shell format: ENTRYPOINT python app.py
192-
currentEntrypoint = string(parts[len(parts)-1])
193-
}
194-
195-
logger.Debugw("found entrypoint", "entrypoint", currentEntrypoint)
196-
197-
// Preserve the original format
198-
if bytes.HasPrefix(parts[1], []byte("[")) {
199-
// Replace the last element in the JSON array
200-
var entrypointArray []string
201-
jsonStr := bytes.Join(parts[1:], []byte(" "))
202-
if err := json.Unmarshal(jsonStr, &entrypointArray); err != nil {
203-
return nil, err
204-
}
205-
entrypointArray[len(entrypointArray)-1] = newEntrypoint
206-
newJSON, err := json.Marshal(entrypointArray)
207-
if err != nil {
208-
return nil, err
209-
}
210-
fmt.Fprintf(&result, "ENTRYPOINT %s\n", newJSON)
211-
} else {
212-
// Preserve the original command but replace the last part
213-
parts[len(parts)-1] = []byte(newEntrypoint)
214-
result.Write(bytes.Join(parts, []byte(" ")))
215-
result.WriteByte('\n')
216-
}
217-
} else if bytes.HasPrefix(trimmedLine, []byte("CMD")) {
218-
// Handle CMD JSON array format: CMD ["python", "main.py", "start"]
219-
parts := bytes.Fields(trimmedLine)
220-
if len(parts) >= 2 && bytes.HasPrefix(parts[1], []byte("[")) {
221-
jsonStr := bytes.Join(parts[1:], []byte(" "))
222-
var cmdArray []string
223-
if err := json.Unmarshal(jsonStr, &cmdArray); err != nil {
224-
return nil, err
225-
}
226-
for i, arg := range cmdArray {
227-
if strings.HasSuffix(arg, projectType.FileExt()) {
228-
cmdArray[i] = newEntrypoint
229-
break
230-
}
231-
}
232-
newJSON, err := json.Marshal(cmdArray)
233-
if err != nil {
234-
return nil, err
235-
}
236-
fmt.Fprintf(&result, "CMD %s\n", newJSON)
237-
}
238-
} else if bytes.HasPrefix(trimmedLine, fmt.Appendf(nil, "RUN python %s", pythonEntrypoint)) {
239-
line = bytes.ReplaceAll(line, []byte(pythonEntrypoint), []byte(newEntrypoint))
240-
result.Write(line)
241-
if i < len(lines)-1 {
242-
result.WriteByte('\n')
243-
}
244-
} else {
245-
result.Write(line)
246-
if i < len(lines)-1 {
247-
result.WriteByte('\n')
248-
}
249-
}
250-
}
162+
tpl := template.Must(template.New("Dockerfile").Parse(string(dockerfileContent)))
163+
buf := &bytes.Buffer{}
164+
tpl.Execute(buf, map[string]string{
165+
"ProgramMain": newEntrypoint,
166+
})
251167

252-
return result.Bytes(), nil
168+
return buf.Bytes(), nil
253169
}

pkg/agentfs/examples/python.pip.Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ FROM python:${PYTHON_VERSION}-slim
77
# the application crashes without emitting any logs due to buffering.
88
ENV PYTHONUNBUFFERED=1
99

10-
# Define the program entrypoint file where your agent is started
11-
ARG PROGRAM_MAIN="src/agent.py"
10+
# Define the program entrypoint file where your agent is started.
11+
ARG PROGRAM_MAIN="{{.ProgramMain}}"
1212

1313
# Create a non-privileged user that the app will run under.
1414
# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
@@ -48,5 +48,5 @@ RUN python "$PROGRAM_MAIN" download-files
4848
EXPOSE 8081
4949

5050
# Run the application.
51-
CMD ["python", "$PROGRAM_MAIN", "start"]
52-
51+
# The "start" command tells the worker to connect to LiveKit and begin waiting for jobs.
52+
CMD ["python", "{{.ProgramMain}}", "start"]

pkg/agentfs/examples/python.uv.Dockerfile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim
1010
# the application crashes without emitting any logs due to buffering.
1111
ENV PYTHONUNBUFFERED=1
1212

13-
# Define the program entrypoint file where your agent is started
14-
ARG PROGRAM_MAIN="src/agent.py"
13+
# Define the program entrypoint file where your agent is started.
14+
ARG PROGRAM_MAIN="{{.ProgramMain}}"
1515

1616
# Create a non-privileged user that the app will run under.
1717
# See https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
@@ -71,7 +71,7 @@ RUN uv run "$PROGRAM_MAIN" download-files
7171
EXPOSE 8081
7272

7373
# Run the application using UV
74-
# UV will activate the virtual environment and run the agent
75-
# The "start" command tells the worker to connect to LiveKit and begin waiting for jobs
76-
CMD ["uv", "run", "$PROGRAM_MAIN", "start"]
74+
# UV will activate the virtual environment and run the agent.
75+
# The "start" command tells the worker to connect to LiveKit and begin waiting for jobs.
76+
CMD ["uv", "run", "{{.ProgramMain}}", "start"]
7777

0 commit comments

Comments
 (0)