@@ -20,7 +20,7 @@ import (
2020 "fmt"
2121 "os"
2222 "path/filepath"
23- "slices "
23+ "sort "
2424 "strings"
2525 "text/template"
2626
@@ -49,115 +49,134 @@ func HasDockerfile(dir string) (bool, error) {
4949}
5050
5151func CreateDockerfile (dir string , projectType ProjectType , settingsMap map [string ]string ) error {
52- if len (settingsMap ) == 0 {
53- return fmt .Errorf ("unable to fetch client settings from server, please try again later" )
52+ dockerfileContent , dockerIgnoreContent , err := GenerateDockerArtifacts (dir , projectType , settingsMap )
53+ if err != nil {
54+ return err
5455 }
5556
56- var dockerfileContent [] byte
57- var dockerIgnoreContent [] byte
58- var err error
57+ if err := os . WriteFile ( filepath . Join ( dir , "Dockerfile" ), dockerfileContent , 0644 ); err != nil {
58+ return err
59+ }
5960
60- dockerfileContent , err = fs .ReadFile ("examples/" + string (projectType ) + ".Dockerfile" )
61- if err != nil {
61+ if err := os .WriteFile (filepath .Join (dir , ".dockerignore" ), dockerIgnoreContent , 0644 ); err != nil {
6262 return err
6363 }
6464
65- dockerIgnoreContent , err = fs .ReadFile ("examples/" + string (projectType ) + ".dockerignore" )
65+ return nil
66+ }
67+
68+ // GenerateDockerArtifacts returns the Dockerfile and .dockerignore contents for the
69+ // provided project type without writing them to disk. The Dockerfile content may be
70+ // templated/validated (e.g., Python entrypoint).
71+ func GenerateDockerArtifacts (dir string , projectType ProjectType , settingsMap map [string ]string ) ([]byte , []byte , error ) {
72+ if len (settingsMap ) == 0 {
73+ return nil , nil , fmt .Errorf ("unable to fetch client settings from server, please try again later" )
74+ }
75+
76+ dockerfileContent , err := fs .ReadFile ("examples/" + string (projectType ) + ".Dockerfile" )
6677 if err != nil {
67- return err
78+ return nil , nil , err
79+ }
80+
81+ dockerIgnoreContent , err := fs .ReadFile ("examples/" + string (projectType ) + ".dockerignore" )
82+ if err != nil {
83+ return nil , nil , err
6884 }
6985
7086 // TODO: (@rektdeckard) support Node entrypoint validation
7187 if projectType .IsPython () {
72- dockerfileContent , err = validateEntrypoint (dir , dockerfileContent , dockerIgnoreContent , projectType , settingsMap )
88+ dockerfileContent , err = validateEntrypoint (dir , dockerfileContent , dockerIgnoreContent , projectType )
7389 if err != nil {
74- return err
90+ return nil , nil , err
7591 }
7692 }
7793
78- err = os .WriteFile (filepath .Join (dir , "Dockerfile" ), dockerfileContent , 0644 )
94+ return dockerfileContent , dockerIgnoreContent , nil
95+ }
96+
97+ func validateEntrypoint (dir string , dockerfileContent []byte , dockerignoreContent []byte , projectType ProjectType ) ([]byte , error ) {
98+ // Build matcher from the Dockerignore content so we don't consider ignored files
99+ reader := bytes .NewReader (dockerignoreContent )
100+ patterns , err := ignorefile .ReadAll (reader )
79101 if err != nil {
80- return err
102+ return nil , err
81103 }
82-
83- err = os .WriteFile (filepath .Join (dir , ".dockerignore" ), dockerIgnoreContent , 0644 )
104+ matcher , err := patternmatcher .New (patterns )
84105 if err != nil {
85- return err
106+ return nil , err
86107 }
87108
88- return nil
89- }
90-
91- func validateEntrypoint (dir string , dockerfileContent []byte , dockerignoreContent []byte , projectType ProjectType , settingsMap map [string ]string ) ([]byte , error ) {
92- valFile := func (fileName string ) (string , error ) {
93- // NOTE: we need to recurse to find entrypoints which may exist in src/ or some other directory.
94- // This could be a lot of files, so we omit any files in .dockerignore, since they cannot be
95- // used as entrypoints.
96-
97- reader := bytes .NewReader (dockerignoreContent )
98- patterns , err := ignorefile .ReadAll (reader )
109+ var fileList []string
110+ if err := filepath .WalkDir (dir , func (path string , d os.DirEntry , err error ) error {
99111 if err != nil {
100- return "" , err
112+ return err
101113 }
102- matcher , err := patternmatcher .New (patterns )
103- if err != nil {
104- return "" , err
114+ if ignored , err := matcher .MatchesOrParentMatches (path ); ignored {
115+ return nil
116+ } else if err != nil {
117+ return err
105118 }
106-
107- var fileList []string
108- if err := filepath .WalkDir (dir , func (path string , d os.DirEntry , err error ) error {
109- if err != nil {
110- return err
111- }
112- if ignored , err := matcher .MatchesOrParentMatches (path ); ignored {
119+ if ! d .IsDir () && strings .HasSuffix (d .Name (), projectType .FileExt ()) {
120+ // Exclude double-underscore files (e.g., __init__.py) which cannot be entrypoint
121+ // except for __main__.py, which is the default entrypoint for Python.
122+ if strings .HasPrefix (d .Name (), "__" ) && d .Name () != "__main__.py" {
113123 return nil
114- } else if err != nil {
115- return err
116- }
117- if ! d .IsDir () && strings .HasSuffix (d .Name (), projectType .FileExt ()) {
118- fileList = append (fileList , path )
119124 }
120- return nil
121- }); err != nil {
122- return "" , fmt .Errorf ("error walking directory %s: %w" , dir , err )
123- }
124-
125- if slices .Contains (fileList , fileName ) {
126- return fileName , nil
125+ fileList = append (fileList , path )
127126 }
127+ return nil
128+ }); err != nil {
129+ return nil , fmt .Errorf ("error walking directory %s: %w" , dir , err )
130+ }
128131
129- // If no matching files found, return early
130- if len (fileList ) == 0 {
131- return "" , nil
132+ // Prioritize common entrypoint filenames at the top of the list
133+ if len (fileList ) > 1 {
134+ priority := func (p string ) int {
135+ name := filepath .Base (p )
136+ switch name {
137+ case "__main__.py" :
138+ return 0
139+ case "main.py" :
140+ return 1
141+ case "agent.py" :
142+ return 2
143+ default :
144+ return 3
145+ }
132146 }
147+ sort .SliceStable (fileList , func (i , j int ) bool {
148+ pi := priority (fileList [i ])
149+ pj := priority (fileList [j ])
150+ if pi != pj {
151+ return pi < pj
152+ }
153+ return fileList [i ] < fileList [j ]
154+ })
155+ }
133156
134- var selected string
157+ var newEntrypoint string
158+ if len (fileList ) == 0 {
159+ newEntrypoint = "main.py"
160+ } else if len (fileList ) == 1 {
161+ newEntrypoint = fileList [0 ]
162+ } else {
163+ selected := fileList [0 ]
135164 form := huh .NewForm (
136165 huh .NewGroup (
137166 huh .NewSelect [string ]().
138- Title (fmt .Sprintf ("Select %s file to use as entrypoint" , projectType .Lang ())).
167+ Title (fmt .Sprintf ("Select the %s file which contains your agent's entrypoint" , projectType .Lang ())).
139168 Options (huh .NewOptions (fileList ... )... ).
140169 Value (& selected ).
141170 WithTheme (util .Theme ),
142171 ),
143172 )
144-
145173 if err := form .Run (); err != nil {
146- return "" , err
174+ return nil , err
147175 }
148-
149- return selected , nil
176+ newEntrypoint = selected
150177 }
151178
152- if err := validateSettingsMap (settingsMap , []string {"python_entrypoint" }); err != nil {
153- return nil , err
154- }
155-
156- pythonEntrypoint := settingsMap ["python_entrypoint" ]
157- newEntrypoint , err := valFile (pythonEntrypoint )
158- if err != nil {
159- return nil , err
160- }
179+ fmt .Printf ("Using entrypoint file [%s]\n " , util .Accented (newEntrypoint ))
161180
162181 tpl := template .Must (template .New ("Dockerfile" ).Parse (string (dockerfileContent )))
163182 buf := & bytes.Buffer {}
0 commit comments