@@ -42,10 +42,7 @@ func (g *PythonPoetryPlanner) GetPlan(srcDir string) *Plan {
42
42
if buildable , err := g .isBuildable (srcDir ); ! buildable {
43
43
return plan .WithError (err )
44
44
}
45
- entrypoint , err := g .GetEntrypoint (srcDir )
46
- if err != nil {
47
- return plan .WithError (err )
48
- }
45
+
49
46
plan .InstallStage = & Stage {
50
47
// pex is is incompatible with certain less common python versions,
51
48
// but because versions are sometimes expressed open-ended (e.g. ^3.10)
@@ -54,12 +51,8 @@ func (g *PythonPoetryPlanner) GetPlan(srcDir string) *Plan {
54
51
Command : "poetry add pex -n --no-ansi && " +
55
52
"poetry install --no-dev -n --no-ansi" ,
56
53
}
57
- plan .BuildStage = & Stage {
58
- Command : "PEX_ROOT=/tmp/.pex poetry run pex . -o app.pex --script " + entrypoint ,
59
- }
60
- plan .StartStage = & Stage {
61
- Command : "PEX_ROOT=/tmp/.pex python ./app.pex" ,
62
- }
54
+ plan .BuildStage = & Stage {Command : g .buildCommand (srcDir )}
55
+ plan .StartStage = & Stage {Command : "python ./app.pex" }
63
56
return plan
64
57
}
65
58
@@ -78,20 +71,25 @@ func (g *PythonPoetryPlanner) PythonVersion(srcDir string) *version {
78
71
return defaultVersion
79
72
}
80
73
81
- func (g * PythonPoetryPlanner ) GetEntrypoint (srcDir string ) ( string , error ) {
74
+ func (g * PythonPoetryPlanner ) buildCommand (srcDir string ) string {
82
75
project := g .PyProject (srcDir )
83
76
// Assume name follows https://peps.python.org/pep-0508/#names
84
77
// Do simple replacement "-" -> "_" and check if any script matches name.
85
78
// This could be improved.
86
79
moduleName := strings .ReplaceAll (project .Tool .Poetry .Name , "-" , "_" )
87
80
if _ , ok := project .Tool .Poetry .Scripts [moduleName ]; ok {
88
- return moduleName , nil
81
+ // return moduleName, nil
82
+ return g .formatBuildCommand (moduleName , moduleName )
89
83
}
90
84
// otherwise use the first script alphabetically
91
85
// (go-toml doesn't preserve order, we could parse ourselves)
92
86
scripts := maps .Keys (project .Tool .Poetry .Scripts )
93
87
slices .Sort (scripts )
94
- return scripts [0 ], nil
88
+ script := ""
89
+ if len (scripts ) > 0 {
90
+ script = scripts [0 ]
91
+ }
92
+ return g .formatBuildCommand (moduleName , script )
95
93
}
96
94
97
95
type pyProject struct {
@@ -101,6 +99,10 @@ type pyProject struct {
101
99
Dependencies struct {
102
100
Python string `toml:"python"`
103
101
} `toml:"dependencies"`
102
+ Packages []struct {
103
+ Include string `toml:"include"`
104
+ From string `toml:"from"`
105
+ } `toml:"packages"`
104
106
Scripts map [string ]string `toml:"scripts"`
105
107
} `toml:"poetry"`
106
108
} `toml:"tool"`
@@ -124,11 +126,62 @@ func (g *PythonPoetryPlanner) isBuildable(srcDir string) (bool, error) {
124
126
"application. pyproject.toml is missing and needed to install python " +
125
127
"dependencies." )
126
128
}
129
+
130
+ // is this the right way to determine package name?
131
+ packageName := strings .ReplaceAll (project .Tool .Poetry .Name , "-" , "_" )
132
+
133
+ // First try to find a __main__ module as entry point
134
+ if len (project .Tool .Poetry .Packages ) > 0 {
135
+ // If package has custom directory, check that.
136
+ // Using packages disables auto-detection of __main__ module.
137
+ for _ , pkg := range project .Tool .Poetry .Packages {
138
+ if pkg .Include == packageName &&
139
+ fileExists (filepath .Join (srcDir , pkg .From , pkg .Include , "__main__.py" )) {
140
+ return true , nil
141
+ }
142
+ }
143
+
144
+ // Use setup tools auto-detect directory structure
145
+ } else if fileExists (filepath .Join (srcDir , packageName , "__main__.py" )) ||
146
+ fileExists (filepath .Join (srcDir , "src" , packageName , "__main__.py" )) {
147
+
148
+ return true , nil
149
+ }
150
+
151
+ // Fallback to using poetry scripts
127
152
if len (project .Tool .Poetry .Scripts ) == 0 {
128
153
return false ,
129
- usererr .New ("Project is not buildable: no scripts found in " +
130
- "pyproject.toml. Please define a script to use as an entrypoint for " +
131
- "your app:\n \n [tool.poetry.scripts]\n my_app = \" my_app:my_function\" \n " )
154
+ usererr .New (
155
+ "Project is not buildable: no __main__.py file found and " +
156
+ "no scripts defined in pyproject.toml" ,
157
+ )
132
158
}
133
159
return true , nil
134
160
}
161
+
162
+ func (g * PythonPoetryPlanner ) formatBuildCommand (module , script string ) string {
163
+
164
+ // If no scripts, just run the module directly always.
165
+ if script == "" {
166
+ return fmt .Sprintf (
167
+ "poetry run pex . -o app.pex -m %s --validate-entry-point" ,
168
+ module ,
169
+ )
170
+ }
171
+
172
+ entrypointScript := fmt .Sprintf (
173
+ `$(poetry run python -c "import pkgutil;
174
+ import %[1]s;
175
+ modules = [name for _, name, _ in pkgutil.iter_modules(%[1]s.__path__)];
176
+ print('-m %[1]s' if '__main__' in modules else '--script %[2]s');")
177
+ ` ,
178
+ module ,
179
+ script ,
180
+ )
181
+
182
+ return fmt .Sprintf (
183
+ "poetry run pex . -o app.pex %s --validate-entry-point &>/dev/null || " +
184
+ "(echo 'Build failed. Could not find entrypoint' && exit 1)" ,
185
+ strings .TrimSpace (strings .ReplaceAll (entrypointScript , "\n " , "" )),
186
+ )
187
+ }
0 commit comments