@@ -45,13 +45,33 @@ limitations under the License.
4545// This launcher will determine the python executable based on the `original-command-line`.
4646// The launcher will configure the PYTHONPATH to point to the appropriate installation
4747// of pydevd/debugpy/ptvsd for the corresponding python binary.
48+ //
49+ // debugpy and ptvsd are pretty straightforward translations of the
50+ // launcher command-line `python -m debugpy`.
51+ //
52+ // pydevd is more involved as pydevd does not support loading modules
53+ // from the command-line (e.g., `python -m flask`). This launcher
54+ // instead creates a small module-loader script using runpy.
55+ // So `launcher --mode pydevd --port 5678 -- python -m flask app.py`
56+ // will create a temp file named `skaffold_pydevd_launch.py`:
57+ // ```
58+ // import sys
59+ // import runpy
60+ // runpy.run_module('flask', run_name="__main__",alter_sys=True)
61+ // ```
62+ // and will then invoke:
63+ // ```
64+ // python -m pydevd --server --port 5678 --DEBUG --continue \
65+ // --file /tmp/pydevd716531212/skaffold_pydevd_launch.py
66+ // ```
4867package main
4968
5069import (
5170 "context"
5271 "flag"
5372 "fmt"
5473 "io"
74+ "io/ioutil"
5575 "os"
5676 "os/exec"
5777 "path/filepath"
@@ -306,24 +326,24 @@ func (pc *pythonContext) updateCommandLine(ctx context.Context) error {
306326
307327 case ModePydevd , ModePydevdPycharm :
308328 // Appropriate location to resolve pydevd is set in updateEnv
309- // TODO: check for modules (and fail?)
310329 cmdline = append (cmdline , pc .args [0 ])
311330 cmdline = append (cmdline , "-m" , "pydevd" , "--server" , "--port" , strconv .Itoa (int (pc .port )))
312331 if pc .env ["WRAPPER_VERBOSE" ] != "" {
313332 cmdline = append (cmdline , "--DEBUG" )
314333 }
315- if pc .debugMode == ModePydevdPycharm {
316- // From the pydevd source, PyCharm wants multiproc
317- cmdline = append (cmdline , "--multiproc" )
318- }
319334 if ! pc .wait {
320335 cmdline = append (cmdline , "--continue" )
321336 }
322- cmdline = append (cmdline , "--file" ) // --file is expected as last argument
323- cmdline = append (cmdline , pc .args [1 :]... )
324- if pc .wait {
325- logrus .Warn ("pydevd does not support wait-for-client" )
337+
338+ // --file is expected as last pydev argument, but it must be a file, and so launching with
339+ // a module requires some special handling.
340+ cmdline = append (cmdline , "--file" )
341+ file , args , err := handlePydevModule (pc .args [1 :])
342+ if err != nil {
343+ return err
326344 }
345+ cmdline = append (cmdline , file )
346+ cmdline = append (cmdline , args ... )
327347 pc .args = cmdline
328348 }
329349 return nil
@@ -356,6 +376,47 @@ func determinePythonMajorMinor(ctx context.Context, launcherBin string, env env)
356376 return
357377}
358378
379+ // handlePydevModule applies special pydevd handling for a python module. When a module is
380+ // found, we write out a python script that uses runpy to invoke the module.
381+ func handlePydevModule (args []string ) (string , []string , error ) {
382+ switch {
383+ case len (args ) == 0 :
384+ return "" , nil , fmt .Errorf ("no python command-line specified" ) // shouldn't happen
385+ case ! strings .HasPrefix (args [0 ], "-" ):
386+ // this is a file
387+ return args [0 ], args [1 :], nil
388+ case ! strings .HasPrefix (args [0 ], "-m" ):
389+ // this is some other command-line flag
390+ return "" , nil , fmt .Errorf ("expected python module: %q" , args )
391+ }
392+ module := args [0 ][2 :]
393+ remaining := args [1 :]
394+ if module == "" {
395+ if len (args ) == 1 {
396+ return "" , nil , fmt .Errorf ("missing python module: %q" , args )
397+ }
398+ module = args [1 ]
399+ remaining = args [2 :]
400+ }
401+
402+ snippet := strings .ReplaceAll (`import sys
403+ import runpy
404+ runpy.run_module('{module}', run_name="__main__",alter_sys=True)
405+ ` , `{module}` , module )
406+
407+ // write out the temp location as other locations may not be writable
408+ d , err := ioutil .TempDir ("" , "pydevd*" )
409+ if err != nil {
410+ return "" , nil , err
411+ }
412+ // use a skaffold-specific file name to ensure no possibility of it matching a user import
413+ f := filepath .Join (d , "skaffold_pydevd_launch.py" )
414+ if err := ioutil .WriteFile (f , []byte (snippet ), 0755 ); err != nil {
415+ return "" , nil , err
416+ }
417+ return f , remaining , nil
418+ }
419+
359420func isEnabled (env env ) bool {
360421 v , found := env ["WRAPPER_ENABLED" ]
361422 return ! found || (v != "0" && v != "false" && v != "no" )
0 commit comments