@@ -3,9 +3,9 @@ package main
33import (
44 "context"
55 "encoding/json"
6- "fmt"
76 "log"
87 "os"
8+ "path/filepath"
99 "strings"
1010
1111 "github.com/containerd/platforms"
@@ -20,7 +20,7 @@ import (
2020 "github.com/pkg/errors"
2121)
2222
23- func getDockerfileDeps (dockerfile string , targetArch string ) string {
23+ func getDockerfileDeps (dockerfile string , targetArch string ) [] string {
2424 ctx := context .Background ()
2525 data := noErr2 (os .ReadFile (dockerfile ))
2626
@@ -47,42 +47,69 @@ func getDockerfileDeps(dockerfile string, targetArch string) string {
4747 return getOpSourceFollowPaths (definition )
4848}
4949
50- func getOpSourceFollowPaths (definition * llb.Definition ) string {
50+ func getOpSourceFollowPaths (definition * llb.Definition ) [] string {
5151 // https://earthly.dev/blog/compiling-containers-dockerfiles-llvm-and-buildkit/
5252 // https://stackoverflow.com/questions/73067660/what-exactly-is-the-frontend-and-backend-of-docker-buildkit
5353
54- ops := make ([ ]llbOp , 0 )
54+ opsByDigest := make (map [digest. Digest ]llbOp , len ( definition . Def ) )
5555 for _ , dt := range definition .Def {
5656 var op pb.Op
5757 if err := op .UnmarshalVT (dt ); err != nil {
5858 panic ("failed to parse op" )
5959 }
6060 dgst := digest .FromBytes (dt )
61- ent := llbOp {Op : & op , Digest : dgst , OpMetadata : definition .Metadata [dgst ].ToPB ()}
62- ops = append (ops , ent )
61+ ent := llbOp {
62+ Op : & op ,
63+ Digest : dgst ,
64+ OpMetadata : definition .Metadata [dgst ].ToPB (),
65+ }
66+ opsByDigest [dgst ] = ent
6367 }
6468
65- var result string = ""
66- for _ , op := range ops {
67- switch op := op .Op .Op .(type ) {
68- case * pb.Op_Source :
69- if strings .HasPrefix (op .Source .Identifier , "docker-image://" ) {
70- // no-op
71- } else if strings .HasPrefix (op .Source .Identifier , "local://" ) {
72- paths := op .Source .Attrs [pb .AttrFollowPaths ]
73- // TODO treat result as a set of strings to get unique set across all layers
74- // this code "works" as is because it seems the terminal layer is the last one processed - which
75- // contains all the referenced files - but treating this as a set seems safer (?)
76- result = paths
77- log .Printf ("found paths: %s" , paths )
78- } else {
79- panic (fmt .Errorf ("unexpected prefix %v" , op .Source .Identifier ))
69+ var result []string
70+ for _ , opDef := range opsByDigest {
71+ switch top := opDef .Op .Op .(type ) {
72+ // https://github.com/moby/buildkit/blob/v0.24/solver/pb/ops.proto#L308-L325
73+ case * pb.Op_File :
74+ for _ , a := range top .File .Actions {
75+ // NOTE CAREFULLY: FileActionCopy copies files from secondaryInput on top of input
76+ if cpy := a .GetCopy (); cpy != nil {
77+ if inputIsFromLocalContext (a .SecondaryInput , opDef .Op .Inputs , opsByDigest ) {
78+ result = append (result , cleanPath (cpy .Src ))
79+ }
80+ }
81+ }
82+ case * pb.Op_Exec :
83+ for _ , m := range top .Exec .Mounts {
84+ if inputIsFromLocalContext (m .Input , opDef .Op .Inputs , opsByDigest ) {
85+ result = append (result , cleanPath (m .Selector ))
86+ }
8087 }
8188 }
8289 }
90+
8391 return result
8492}
8593
94+ func cleanPath (path string ) string {
95+ return noErr2 (filepath .Rel ("/" , filepath .Clean (path )))
96+ }
97+
98+ func inputIsFromLocalContext (input int64 , inputs []* pb.Input , opsByDigest map [digest.Digest ]llbOp ) bool {
99+ // input is -1 if the input is a FROM scratch or equivalent
100+ if input == - 1 {
101+ return false
102+ }
103+
104+ srcDigest := digest .Digest (inputs [input ].Digest )
105+ sourceOp := opsByDigest [srcDigest ]
106+ if src , ok := sourceOp .Op .Op .(* pb.Op_Source ); ok {
107+ // local://context is the primary context, but there may be multiple named contexts
108+ return strings .HasPrefix (src .Source .Identifier , "local://" )
109+ }
110+ return false
111+ }
112+
86113// llbOp holds data for a single loaded LLB op
87114type llbOp struct {
88115 Op * pb.Op
0 commit comments