Skip to content

Commit 6e1e2df

Browse files
authored
Add support for pydevd and pydevd-pycharm (#66)
Add in-container python launcher to auto-configure PYTHONPATH for the relevant Python debugging backend and Python version, supporting debugpy, ptvsd, and pydevd.
1 parent 00c114f commit 6e1e2df

File tree

12 files changed

+1129
-0
lines changed

12 files changed

+1129
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ install:
2121

2222
script:
2323
- (cd nodejs/helper-image; go test .)
24+
- (cd python/helper-image/launcher; go test .)
2425
# try building before integration tests
2526
- skaffold build -p local
2627

python/helper-image/Dockerfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,54 @@
1+
# Copyright 2021 The Skaffold Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# This Dockerfile creates a debug helper base image for Python.
16+
# It provides installations of debugpy, ptvsd, pydevd, and pydevd-pycharm
17+
# for Python 2.7, 3.7, 3.8, and 3.9.
18+
#
19+
# debugpy and ptvsd are well-structured packages installed in separate
20+
# directories under # /dbg/python/lib/pythonX.Y/site-packages and
21+
# that do not interfere with each other.
22+
#
23+
# pydevd and pydevd-pycharm install a script in .../bin and both install
24+
# .py files directly in .../lib/pythonX.Y/site-packages. To avoid
25+
# interference we install pydevd and pydevd-pycharm under /dbg/python/pydevd/pythonX.Y
26+
# and /dbg/python/pydevd-pycharm/pythonX.Y
27+
128
FROM python:2.7 as python27
229
RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy
30+
RUN PYTHONUSERBASE=/dbgpy/pydevd/python2.7 pip install --user pydevd
31+
RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python2.7 pip install --user pydevd-pycharm
332

433
FROM python:3.7 as python37
534
RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy
35+
RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.7 pip install --user pydevd
36+
RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.7 pip install --user pydevd-pycharm
637

738
FROM python:3.8 as python38
839
RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy
40+
RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.8 pip install --user pydevd
41+
RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.8 pip install --user pydevd-pycharm
942

1043
FROM python:3.9 as python39
1144
RUN PYTHONUSERBASE=/dbgpy pip install --user ptvsd debugpy
45+
RUN PYTHONUSERBASE=/dbgpy/pydevd/python3.9 pip install --user pydevd
46+
RUN PYTHONUSERBASE=/dbgpy/pydevd-pycharm/python3.9 pip install --user pydevd-pycharm
47+
48+
FROM golang:1.14.1 as build
49+
COPY launcher/ .
50+
# Produce an as-static-as-possible wrapper binary to work on musl and glibc
51+
RUN GOPATH="" CGO_ENABLED=0 go build -o launcher -ldflags '-s -w -extldflags "-static"' .
1252

1353
# Now populate the duct-tape image with the language runtime debugging support files
1454
# The debian image is about 95MB bigger
@@ -23,3 +63,4 @@ COPY --from=python27 /dbgpy/ python/
2363
COPY --from=python37 /dbgpy/ python/
2464
COPY --from=python38 /dbgpy/ python/
2565
COPY --from=python39 /dbgpy/ python/
66+
COPY --from=build /go/launcher python/
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Copyright 2021 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"context"
21+
"os"
22+
"os/exec"
23+
24+
"github.com/sirupsen/logrus"
25+
)
26+
27+
// for testing
28+
var newCommand = createCommand
29+
var newConsoleCommand = createConsoleCommand
30+
31+
// commander is a subset of exec.Cmd
32+
type commander interface {
33+
Run() error
34+
Output() ([]byte, error)
35+
CombinedOutput() ([]byte, error)
36+
}
37+
38+
// ensures Cmd satisfies the commander interface
39+
var _ commander = (*exec.Cmd)(nil)
40+
41+
// createCommand creates a normal exec.Cmd object
42+
func createCommand(ctx context.Context, cmdline []string, env env) commander {
43+
logrus.Debugf("command: %v (env: %s)", cmdline, env)
44+
cmd := exec.CommandContext(ctx, cmdline[0], cmdline[1:]...)
45+
cmd.Env = env.AsPairs()
46+
return cmd
47+
}
48+
49+
// createConsoleCommand creates an exec.Cmd object that connects to os.Stdin, os.Stdout, os.Stderr
50+
func createConsoleCommand(ctx context.Context, cmdline []string, env env) commander {
51+
logrus.Debugf("command(stdin/out/err): %v (env: %s)", cmdline, env)
52+
cmd := exec.CommandContext(ctx, cmdline[0], cmdline[1:]...)
53+
cmd.Stdin = os.Stdin
54+
cmd.Stdout = os.Stdout
55+
cmd.Stderr = os.Stderr
56+
cmd.Env = env.AsPairs()
57+
return cmd
58+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
Copyright 2021 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"context"
21+
"os/exec"
22+
"testing"
23+
24+
"github.com/google/go-cmp/cmp"
25+
)
26+
27+
func TestCmd(t *testing.T) {
28+
RunCmdOut([]string{"hello"}, "abc").
29+
AndRunCmdFail([]string{"ls"}, 1).
30+
Setup(t)
31+
32+
if out, err := newCommand(nil, []string{"hello"}, nil).Output(); err != nil {
33+
t.Error("command should not have failed")
34+
} else if string(out) != "abc" {
35+
t.Error("output should have been abc")
36+
}
37+
if newCommand(nil, []string{"ls"}, nil).Run() == nil {
38+
t.Error("command should have failed")
39+
}
40+
}
41+
42+
type fakeCmd struct {
43+
mode string
44+
cmdline []string
45+
exitCode int
46+
output string
47+
}
48+
49+
// ensures fakeCmd satisfies the commander interface
50+
var _ commander = (*fakeCmd)(nil)
51+
52+
func (f *fakeCmd) Run() error {
53+
_t.Helper()
54+
if f.mode != "Run" {
55+
_t.Errorf("Command%v: expected %s() not Run()", f.cmdline, f.mode)
56+
}
57+
if f.exitCode == 0 {
58+
return nil
59+
}
60+
// doesn't seem to be an easy way to set the exitcode
61+
return &exec.ExitError{}
62+
}
63+
64+
func (f *fakeCmd) Output() ([]byte, error) {
65+
_t.Helper()
66+
if f.mode != "Output" {
67+
_t.Errorf("Command%v: expected %v() not Output()", f.cmdline, f.mode)
68+
}
69+
if f.exitCode == 0 {
70+
return []byte(f.output), nil
71+
}
72+
// doesn't seem to be an easy way to set the exitcode
73+
return []byte(f.output), &exec.ExitError{}
74+
}
75+
76+
func (f *fakeCmd) CombinedOutput() ([]byte, error) {
77+
_t.Helper()
78+
if f.mode != "Output" {
79+
_t.Errorf("Command%v: expected %v() not CombinedOutput()", f.cmdline, f.mode)
80+
}
81+
if f.exitCode == 0 {
82+
return []byte(f.output), nil
83+
}
84+
// doesn't seem to be an easy way to set the exitcode
85+
return []byte(f.output), &exec.ExitError{}
86+
}
87+
88+
type commands []*fakeCmd
89+
90+
var (
91+
_cmdStack commands
92+
_t *testing.T
93+
)
94+
95+
func fakeCommand(_ context.Context, cmdline []string, env env) commander {
96+
_t.Helper()
97+
if len(_cmdStack) == 0 {
98+
_t.Fatalf("test expected no further commands: %v", cmdline)
99+
}
100+
current := _cmdStack[0]
101+
_cmdStack = _cmdStack[1:]
102+
if diff := cmp.Diff(current.cmdline, cmdline); diff != "" {
103+
_t.Errorf("cmdlines differ (-got, +want): %s", diff)
104+
}
105+
return current
106+
}
107+
108+
func (c commands) Setup(t *testing.T) {
109+
_t = t
110+
_cmdStack = c
111+
112+
oldCommand := newCommand
113+
oldConsoleCommand := newConsoleCommand
114+
newCommand = fakeCommand
115+
newConsoleCommand = fakeCommand
116+
_t.Cleanup(func() {
117+
newCommand = oldCommand
118+
newConsoleCommand = oldConsoleCommand
119+
})
120+
}
121+
122+
func RunCmd(cmdline []string) commands {
123+
return commands{}.AndRunCmd(cmdline)
124+
}
125+
126+
func RunCmdFail(cmdline []string, exitCode int) commands {
127+
return commands{}.AndRunCmdFail(cmdline, exitCode)
128+
}
129+
130+
func RunCmdOut(cmdline []string, output string) commands {
131+
return commands{}.AndRunCmdOut(cmdline, output)
132+
}
133+
134+
func RunCmdOutFail(cmdline []string, output string, exitCode int) commands {
135+
return commands{}.AndRunCmdOutFail(cmdline, output, exitCode)
136+
}
137+
138+
func (c commands) AndRunCmd(cmdline []string) commands {
139+
c = append(c, &fakeCmd{mode: "Run", cmdline: cmdline})
140+
return c
141+
}
142+
143+
func (c commands) AndRunCmdFail(cmdline []string, exitCode int) commands {
144+
c = append(c, &fakeCmd{mode: "Run", cmdline: cmdline, exitCode: exitCode})
145+
return c
146+
}
147+
148+
func (c commands) AndRunCmdOut(cmdline []string, output string) commands {
149+
c = append(c, &fakeCmd{mode: "Output", cmdline: cmdline, output: output})
150+
return c
151+
}
152+
153+
func (c commands) AndRunCmdOutFail(cmdline []string, output string, exitCode int) commands {
154+
c = append(c, &fakeCmd{mode: "Output", cmdline: cmdline, output: output, exitCode: exitCode})
155+
return c
156+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
Copyright 2021 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"fmt"
21+
"path/filepath"
22+
"sort"
23+
"strings"
24+
)
25+
26+
type env map[string]string
27+
28+
// EnvFromPairs turns a set of VAR=VALUE strings to a map.
29+
func EnvFromPairs(entries []string) env {
30+
e := make(env)
31+
for _, entry := range entries {
32+
kv := strings.SplitN(entry, "=", 2)
33+
e[kv[0]] = kv[1]
34+
}
35+
return e
36+
}
37+
38+
// AsPairs turns a map of variable:value pairs into a set of VAR=VALUE string pairs.
39+
func (e env) AsPairs() []string {
40+
var m []string
41+
for k, v := range e {
42+
m = append(m, k+"="+v)
43+
}
44+
return m
45+
}
46+
47+
// PrependFilepath prepands a path to a environment variable.
48+
func (e env) PrependFilepath(key string, path string) {
49+
v := e[key]
50+
if v != "" {
51+
v = path + string(filepath.ListSeparator) + v
52+
} else {
53+
v = path
54+
}
55+
e[key] = v
56+
}
57+
58+
func (e env) String() string {
59+
var keys []string
60+
for k, _ := range e {
61+
keys = append(keys, k)
62+
}
63+
sort.Strings(keys)
64+
65+
var s string
66+
for _, k := range keys {
67+
s += fmt.Sprintf("%s=%q ", k, e[k])
68+
}
69+
return s
70+
}

0 commit comments

Comments
 (0)