Properly map the source paths from the metadata in the solve to the client side paths. The source path returns is relative to the context that gets uploaded which is usually a subdirectory. The original code noticed this when mapping the paths but made the invalid assumption that the dockerfile would always be in the context path so it combined the dockerfile name with the context path. It is possible for the dockerfile to be in a subdirectory of the context. In which case, we computed the paths incorrectly. This modifies DAP to instead use the `DockerfileMappingDst` and `DockerfileMappingSrc` which are special included variables to the inputs that get filled in during the build for the purpose of mapping the source path to the client side path. Tests have also been added for this functionality to ensure it doesn't break again. This should work with both absolute and relative paths although absolute paths should probably be preferred for usage just because they're less likely to result in weird things happening. The sources are also normalized to always convert the source filenames to absolute paths and DAP itself will accept relative paths but will only ever communicate in absolute paths. When you set a breakpoint, it will convert it to an absolute path and reference it in that way rather than a relative path. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
458 lines
10 KiB
Go
458 lines
10 KiB
Go
package dap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"maps"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/google/go-dap"
|
|
"github.com/moby/buildkit/client/llb"
|
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/tonistiigi/fsutil/types"
|
|
)
|
|
|
|
type frame struct {
|
|
dap.StackFrame
|
|
op *pb.Op
|
|
scopes []dap.Scope
|
|
}
|
|
|
|
func (f *frame) setNameFromMeta(meta llb.OpMetadata) {
|
|
if name, ok := meta.Description["llb.customname"]; ok {
|
|
f.Name = name
|
|
} else if cmd, ok := meta.Description["com.docker.dockerfile.v1.command"]; ok {
|
|
f.Name = cmd
|
|
}
|
|
// TODO: should we infer the name from somewhere else?
|
|
}
|
|
|
|
func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, next *step) {
|
|
for _, l := range loc.Locations {
|
|
for _, r := range l.Ranges {
|
|
if next != nil && f.Line != 0 {
|
|
// We have location information. See if the new location
|
|
// information matches with our location better.
|
|
if !betterLocation(r, f, next) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
f.Line = int(r.Start.Line)
|
|
f.Column = int(r.Start.Character)
|
|
f.EndLine = int(r.End.Line)
|
|
f.EndColumn = int(r.End.Character)
|
|
|
|
info := def.Source.Infos[l.SourceIndex]
|
|
f.Source = &dap.Source{
|
|
Name: path.Base(info.Filename),
|
|
Path: info.Filename,
|
|
}
|
|
|
|
// If we do not have a next operation, then we don't have
|
|
// any information to make a determination about the "best" fit
|
|
// that happens at the beginning of this section. Exit early.
|
|
if next == nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *frame) ExportVars(ctx context.Context, mounts map[string]gateway.Reference, refs *variableReferences) {
|
|
f.fillVarsFromOp(f.op, refs)
|
|
if len(mounts) > 0 {
|
|
f.fillVarsFromResult(ctx, mounts, refs)
|
|
}
|
|
}
|
|
|
|
func (f *frame) ResetVars() {
|
|
f.scopes = nil
|
|
}
|
|
|
|
func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) {
|
|
f.scopes = append(f.scopes, dap.Scope{
|
|
Name: "Arguments",
|
|
PresentationHint: "arguments",
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
var vars []dap.Variable
|
|
if op.Platform != nil {
|
|
vars = append(vars, platformVars(op.Platform, refs))
|
|
}
|
|
|
|
switch op := op.Op.(type) {
|
|
case *pb.Op_Exec:
|
|
vars = append(vars, execOpVars(op.Exec, refs))
|
|
}
|
|
return vars
|
|
}),
|
|
})
|
|
}
|
|
|
|
func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable {
|
|
return dap.Variable{
|
|
Name: "platform",
|
|
Value: fmt.Sprintf("%s/%s", platform.OS, platform.Architecture),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := []dap.Variable{
|
|
{
|
|
Name: "architecture",
|
|
Value: platform.Architecture,
|
|
},
|
|
{
|
|
Name: "os",
|
|
Value: platform.OS,
|
|
},
|
|
}
|
|
|
|
if platform.Variant != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "variant",
|
|
Value: platform.Variant,
|
|
})
|
|
}
|
|
|
|
if platform.OSVersion != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "osversion",
|
|
Value: platform.OSVersion,
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
}
|
|
}
|
|
|
|
func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable {
|
|
return dap.Variable{
|
|
Name: "exec",
|
|
Value: strings.Join(exec.Meta.Args, " "),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := []dap.Variable{
|
|
{
|
|
Name: "args",
|
|
Value: brief(strings.Join(exec.Meta.Args, " ")),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := make([]dap.Variable, 0, len(exec.Meta.Args))
|
|
for i, arg := range exec.Meta.Args {
|
|
vars = append(vars, dap.Variable{
|
|
Name: strconv.Itoa(i),
|
|
Value: arg,
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
},
|
|
{
|
|
Name: "env",
|
|
Value: brief(strings.Join(exec.Meta.Env, " ")),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := make([]dap.Variable, 0, len(exec.Meta.Env))
|
|
for _, envstr := range exec.Meta.Env {
|
|
parts := strings.SplitN(envstr, "=", 2)
|
|
vars = append(vars, dap.Variable{
|
|
Name: parts[0],
|
|
Value: parts[1],
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
},
|
|
}
|
|
|
|
if exec.Meta.Cwd != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "workdir",
|
|
Value: exec.Meta.Cwd,
|
|
})
|
|
}
|
|
|
|
if exec.Meta.User != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "user",
|
|
Value: exec.Meta.User,
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
}
|
|
}
|
|
|
|
func (f *frame) fillVarsFromResult(ctx context.Context, mounts map[string]gateway.Reference, refs *variableReferences) {
|
|
f.scopes = append(f.scopes, dap.Scope{
|
|
Name: "File Explorer",
|
|
PresentationHint: "locals",
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
return fileExplorer(ctx, mounts, refs)
|
|
}),
|
|
Expensive: true,
|
|
})
|
|
}
|
|
|
|
func fileExplorer(ctx context.Context, mounts map[string]gateway.Reference, vars *variableReferences) []dap.Variable {
|
|
keys := slices.Collect(maps.Keys(mounts))
|
|
sort.Strings(keys)
|
|
|
|
outVars := make([]dap.Variable, 0, len(keys))
|
|
for _, k := range keys {
|
|
outVars = append(outVars, dap.Variable{
|
|
Name: k,
|
|
VariablesReference: vars.New(func() []dap.Variable {
|
|
return fsVars(ctx, mounts[k], "/", vars)
|
|
}),
|
|
})
|
|
}
|
|
return outVars
|
|
}
|
|
|
|
func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable {
|
|
files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{
|
|
Path: path,
|
|
})
|
|
if err != nil {
|
|
return []dap.Variable{
|
|
{
|
|
Name: "error",
|
|
Value: err.Error(),
|
|
},
|
|
}
|
|
}
|
|
|
|
paths := make([]dap.Variable, len(files))
|
|
for i, file := range files {
|
|
stat := statf(file)
|
|
fv := dap.Variable{
|
|
Name: file.Path,
|
|
}
|
|
|
|
fullpath := filepath.ToSlash(filepath.Join(path, file.Path))
|
|
if file.IsDir() {
|
|
fv.Name += "/"
|
|
fv.VariablesReference = vars.New(func() []dap.Variable {
|
|
dvar := dap.Variable{
|
|
Name: ".",
|
|
Value: statf(file),
|
|
VariablesReference: vars.New(func() []dap.Variable {
|
|
return statVars(file)
|
|
}),
|
|
}
|
|
return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...)
|
|
})
|
|
fv.Value = ""
|
|
} else {
|
|
fv.Value = stat
|
|
fv.VariablesReference = vars.New(func() (dvars []dap.Variable) {
|
|
if fs.FileMode(file.Mode).IsRegular() {
|
|
// Regular file so display a small blurb of the file.
|
|
dvars = append(dvars, fileVars(ctx, ref, fullpath)...)
|
|
}
|
|
return append(dvars, statVars(file)...)
|
|
})
|
|
}
|
|
paths[i] = fv
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func statf(st *types.Stat) string {
|
|
mode := fs.FileMode(st.Mode)
|
|
modTime := time.Unix(0, st.ModTime).UTC()
|
|
return fmt.Sprintf("%s %d:%d %s", mode, st.Uid, st.Gid, modTime.Format("Jan 2 15:04:05 2006"))
|
|
}
|
|
|
|
func fileVars(ctx context.Context, ref gateway.Reference, fullpath string) []dap.Variable {
|
|
b, err := ref.ReadFile(ctx, gateway.ReadRequest{
|
|
Filename: fullpath,
|
|
Range: &gateway.FileRange{Length: 512},
|
|
})
|
|
|
|
var (
|
|
data string
|
|
dataErr error
|
|
)
|
|
if err != nil {
|
|
data = err.Error()
|
|
} else if isBinaryData(b) {
|
|
data = "binary data"
|
|
} else {
|
|
if len(b) == 512 {
|
|
// Get the remainder of the file.
|
|
remaining, err := ref.ReadFile(ctx, gateway.ReadRequest{
|
|
Filename: fullpath,
|
|
Range: &gateway.FileRange{Offset: 512},
|
|
})
|
|
if err != nil {
|
|
dataErr = err
|
|
} else {
|
|
b = append(b, remaining...)
|
|
}
|
|
}
|
|
data = string(b)
|
|
}
|
|
|
|
dvars := []dap.Variable{
|
|
{
|
|
Name: "data",
|
|
Value: data,
|
|
},
|
|
}
|
|
if dataErr != nil {
|
|
dvars = append(dvars, dap.Variable{
|
|
Name: "dataError",
|
|
Value: dataErr.Error(),
|
|
})
|
|
}
|
|
return dvars
|
|
}
|
|
|
|
func statVars(st *types.Stat) (vars []dap.Variable) {
|
|
if st.Linkname != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "linkname",
|
|
Value: st.Linkname,
|
|
})
|
|
}
|
|
|
|
mode := fs.FileMode(st.Mode)
|
|
modTime := time.Unix(0, st.ModTime).UTC()
|
|
vars = append(vars, []dap.Variable{
|
|
{
|
|
Name: "mode",
|
|
Value: mode.String(),
|
|
},
|
|
{
|
|
Name: "uid",
|
|
Value: strconv.FormatUint(uint64(st.Uid), 10),
|
|
},
|
|
{
|
|
Name: "gid",
|
|
Value: strconv.FormatUint(uint64(st.Gid), 10),
|
|
},
|
|
{
|
|
Name: "mtime",
|
|
Value: modTime.Format("Jan 2 15:04:05 2006"),
|
|
},
|
|
}...)
|
|
return vars
|
|
}
|
|
|
|
func (f *frame) Scopes() []dap.Scope {
|
|
if f.scopes == nil {
|
|
return []dap.Scope{}
|
|
}
|
|
return f.scopes
|
|
}
|
|
|
|
type variableReferences struct {
|
|
refs map[int32]func() []dap.Variable
|
|
nextID atomic.Int32
|
|
mask int32
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func newVariableReferences() *variableReferences {
|
|
v := new(variableReferences)
|
|
v.Reset()
|
|
return v
|
|
}
|
|
|
|
func (v *variableReferences) New(fn func() []dap.Variable) int {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
id := v.nextID.Add(1) | v.mask
|
|
v.refs[id] = sync.OnceValue(fn)
|
|
return int(id)
|
|
}
|
|
|
|
func (v *variableReferences) Get(id int) []dap.Variable {
|
|
v.mu.RLock()
|
|
fn := v.refs[int32(id)]
|
|
v.mu.RUnlock()
|
|
|
|
var vars []dap.Variable
|
|
if fn != nil {
|
|
vars = fn()
|
|
}
|
|
|
|
if vars == nil {
|
|
vars = []dap.Variable{}
|
|
}
|
|
return vars
|
|
}
|
|
|
|
func (v *variableReferences) Reset() {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
v.refs = make(map[int32]func() []dap.Variable)
|
|
v.nextID.Store(0)
|
|
}
|
|
|
|
// isBinaryData uses heuristics to determine if the file
|
|
// is binary. Algorithm taken from this blog post:
|
|
// https://eli.thegreenplace.net/2011/10/19/perls-guess-if-file-is-text-or-binary-implemented-in-python/
|
|
func isBinaryData(b []byte) bool {
|
|
odd := 0
|
|
for i := 0; i < len(b); i++ {
|
|
c := b[i]
|
|
if c == 0 {
|
|
return true
|
|
}
|
|
|
|
isHighBit := c&128 > 0
|
|
if !isHighBit {
|
|
if c < 32 && c != '\n' && c != '\t' {
|
|
odd++
|
|
}
|
|
} else {
|
|
r, sz := utf8.DecodeRune(b)
|
|
if r != utf8.RuneError && sz > 1 {
|
|
i += sz - 1
|
|
continue
|
|
}
|
|
odd++
|
|
}
|
|
}
|
|
return float64(odd)/float64(len(b)) > .3
|
|
}
|
|
|
|
func brief(s string) string {
|
|
if len(s) >= 64 {
|
|
return s[:60] + " ..."
|
|
}
|
|
return s
|
|
}
|
|
|
|
func betterLocation(r *pb.Range, f *frame, next *step) bool {
|
|
// Ideal guess is one that is before the next frame.
|
|
if int(r.Start.Line) <= next.frame.Line {
|
|
// And is later than our current guess.
|
|
if int(r.Start.Line) > f.Line {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// We're after the next frame so this is a bad guess.
|
|
// Was our original one even worse?
|
|
if int(r.Start.Line) < f.Line {
|
|
// Yes it was. We'll consider this a better location.
|
|
return true
|
|
}
|
|
|
|
// Doesn't seem to be a better location.
|
|
return false
|
|
}
|