├── .gitignore ├── Makefile ├── Qiita.md ├── README.md ├── _config.yml ├── docker2dot ├── docker2dot.go └── docker2dot_test.go ├── dockerfile2llb ├── convert.go ├── convert_norunmount.go ├── convert_nosecrets.go ├── convert_nossh.go ├── convert_runmount.go ├── convert_secrets.go ├── convert_ssh.go ├── convert_test.go ├── defaultshell_unix.go ├── defaultshell_windows.go ├── directives.go ├── directives_test.go ├── image.go ├── platform.go └── platform_test.go ├── full.render.js ├── go.mod ├── go.sum ├── index.html ├── main.css ├── main.go ├── main.wasm ├── static ├── all.png ├── dockerdot.png ├── err.png ├── github.png └── sp.gif ├── viz.js └── wasm_exec.js /.gitignore: -------------------------------------------------------------------------------- 1 | main.wasm 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | GOOS=js GOARCH=wasm go build -o main.wasm 3 | exec: 4 | goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))' -------------------------------------------------------------------------------- /Qiita.md: -------------------------------------------------------------------------------- 1 | # 1日で Go + WebAssembly に入門して Dockerfile の依存グラフを図にしてくれる君を作ったので、知見とハマりポイントを共有します。 2 | 3 | こんにちはpo3rinです。Go + WebAssembly + BuildKit で Dockerfile の依存グラフを図にしてくれる誰得サイトを作ったので紹介します。その名も「Dockerfile Dependency graph」 4 | 」! 5 | 6 | :whale: https://po3rin.github.io/dockerdot/ :whale: 7 | (PCブラウザだけ対応してます) 8 | 9 |

10 | 11 |

12 | 13 | 今回はこれをどのように作ったのかとハマった点を紹介します。リポジトリはこちら!! 14 | 15 | 16 | 17 | ## どのように作ったか 18 | 19 | 全体像はこちらになります。 20 | 21 | 22 | 23 | 内部では Dockerfile から LLB(プロセスの依存関係グラフを定義するために使用されるバイナリ中間言語)を取得して、それをdot言語(データ記述言語)に変換しています。今回はその処理を Go + WebAssembly で書いています。 24 | 25 | WebAssemblyの基本的な使い方に関してはこちらをご覧ください!Hello Worldから解説してくれます!! 26 | https://github.com/golang/go/wiki/WebAssembly 27 | 28 | 29 | もちろん内部ではBuildKitのパッケージを利用しています。 30 | https://github.com/moby/buildkit 31 | 32 | buildkitの内部でDockerfileをdot言語に変換する関数を記述しています。実装はBuildKitの非公開関数を使えるようにしただけです。 33 | ```go 34 | // Docker2Dot convert dockerfile to llb Expressed in DOT language. 35 | func Docker2Dot(df []byte) ([]byte, error) 36 | ``` 37 | 38 | Dockerfileのバイト列を渡せばそのDockerfileから生成した依存を記述したdot言語が所得できます。 39 | 40 | Go + WebAssembly が Dockerfile を dot言語に変換したら後は JavaScript 側で viz.js を使ってグラフにしています。viz.jsは、dot言語で記述された構造からグラフを作成するためのパッケージです。下記のように使います。 41 | 42 | ```js 43 | let viz = new Viz() 44 | graph = document.getElementById("graph") 45 | showGraph = (dot) => { 46 | viz.renderSVGElement(dot).then((element)=> { 47 | if (graph.lastChild){ 48 | graph.removeChild(graph.lastChild) 49 | } 50 | graph.appendChild(element) 51 | }) 52 | } 53 | ``` 54 | 55 | これでshowGraph関数が、引数の```dot```にdot言語で記載されたLLBを渡されることで依存グラフにしてくれます。 56 | 57 | ## ハマったところ 58 | 59 | ### OSやアーキテクチャ固有の機能に依存しているパッケージは使えない。 60 | 61 | BuildKitは内部で```http://golang.org/x/sys/unix```を使っていたので、最初、wasmのビルドに失敗しました。 62 | 63 | 64 | 65 | Twitterでボヤいていたところ、wasmの鬼の @syuumai さんとGoの鬼の @tenntenn さんにアドバイスいただきました。wasmをビルドするときは```GOOS=js GOARCH=wasm```なのでビルドタグでビルド対象から外されてしますようです。 66 | 67 | Twitterでの会話はこちら 68 | https://twitter.com/po3rin/status/1139568570239635456 69 | 70 | よって今回は moby/buildkit の中からOSやアーキテクチャ固有の機能に依存している処理を使わないようにmoby/buildkitのコードから必要部分だけをmirrorして使っています。 71 | 72 | ### 内部でゴールーチンを読んでいるパッケージの処理をコールバック関数で呼ぶとデッドロックが起きる 73 | 74 | こちらのコードラボで注釈されている問題に見事ハマりました。 75 | 76 | > コールバック関数は1つのゴールーチンの中で1つずつ処理されます。そのため、コールバック関数の中で、コールバック関数によって結果を受け取るような処理があるとデッドロックが起きてしまいます。 77 | 78 | ```moby/buildkit``` では sync.ErrGroup でバリバリ並行処理が行われていたのでデッドロックが起きていました。そのため別のゴールーチンを起動して呼び出す必要がありました。 79 | 80 | ```go 81 | func registerCallbacks() { 82 | var cb js.Func 83 | document := js.Global().Get("document") 84 | element := document.Call("getElementById", "textarea") 85 | 86 | cb = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 87 | text := element.Get("value").String() 88 | dockerfile := []byte(text) 89 | 90 | // https://github.com/golang/go/issues/26382 91 | // should wrap func with gorutine. 92 | go func() { 93 | dot, err := docker2dot.Docker2Dot(dockerfile) 94 | if err != nil { 95 | fmt.Println(err) 96 | } 97 | showGraph := js.Global().Get("showGraph") 98 | showGraph.Invoke(string(dot)) 99 | }() 100 | return nil 101 | }) 102 | 103 | js.Global().Get("document").Call("getElementById", "button").Call("addEventListener", "click", cb) 104 | } 105 | ``` 106 | 107 | ## 初めて触った所感 108 | 109 | 最初はハマってましたが、慣れてきたらJavaScriptからGoの処理を呼ぶ簡単さに感動を覚えます。そしてGoで書いたのにデプロイが楽チンというのも気持ちよかったです。 110 | 111 | ## 今回の開発で参考にした記事 112 | 113 | だいたいここ読んでおけば良い 114 | 115 | go wiki: WebAssembly 116 | https://github.com/golang/go/wiki/WebAssembly 117 | (公式による解説) 118 | 119 | Go 1.11: WebAssembly for the gophers 120 | https://medium.zenika.com/go-1-11-webassembly-for-the-gophers-ae4bb8b1ee03 121 | (syscall/jsの解説が充実している) 122 | 123 | GoでWebAssemblyに触れよう 124 | https://golangtokyo.github.io/codelab/go-webassembly/?index=codelab#0 125 | (ハンズオンとして最適) 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dockerdot 2 | 3 | 4 | 5 | --- 6 | 7 | dockerdot shows dockerfile dependenciy graph. This is useful to understand how build dockerfile. 8 | This uses Go WebAssembly + BuildKit package. 9 | 10 | :whale: https://po3rin.github.io/dockerdot/ :whale: 11 | (not support smart phone ...) 12 | 13 |

14 | 15 |

16 | 17 | ## How to develop 18 | 19 | ```bash 20 | ## build wasm 21 | make build 22 | 23 | ## run file server 24 | make exec 25 | ``` 26 | 27 | ## Go + WebAssembly 28 | https://github.com/golang/go/wiki/WebAssembly 29 | 30 | ## DOT language 31 | https://medium.com/@dinis.cruz/dot-language-graph-based-diagrams-c3baf4c0decc 32 | 33 | ## BuildKit 34 | https://github.com/moby/buildkit 35 | 36 | ## Warn 37 | 38 | dockerbot/dockerfile2llb package is almost mirror from moby/buildkit. but sygnal package is not used. 39 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docker2dot/docker2dot.go: -------------------------------------------------------------------------------- 1 | package docker2dot 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | "github.com/moby/buildkit/client/llb" 11 | "github.com/moby/buildkit/client/llb/imagemetaresolver" 12 | "github.com/moby/buildkit/solver/pb" 13 | digest "github.com/opencontainers/go-digest" 14 | "github.com/pkg/errors" 15 | "github.com/po3rin/dockerdot/dockerfile2llb" 16 | ) 17 | 18 | // Docker2Dot convert dockerfile to llb Expressed in DOT language. 19 | func Docker2Dot(df []byte) ([]byte, error) { 20 | caps := pb.Caps.CapSet(pb.Caps.All()) 21 | 22 | st, img, err := dockerfile2llb.Dockerfile2LLB( 23 | context.Background(), 24 | df, 25 | dockerfile2llb.ConvertOpt{ 26 | MetaResolver: imagemetaresolver.Default(), 27 | LLBCaps: &caps, 28 | }, 29 | ) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // ignore image 35 | _ = img 36 | 37 | def, err := st.Marshal() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | ops, err := loadLLB(def) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | var b bytes.Buffer 48 | writeDot(ops, &b) 49 | result := b.Bytes() 50 | 51 | return result, nil 52 | } 53 | 54 | type llbOp struct { 55 | Op pb.Op 56 | Digest digest.Digest 57 | OpMetadata pb.OpMetadata 58 | } 59 | 60 | // loadLLB load llbOp from llb.Definition. 61 | func loadLLB(def *llb.Definition) ([]llbOp, error) { 62 | var ops []llbOp 63 | for _, dt := range def.Def { 64 | var op pb.Op 65 | if err := (&op).Unmarshal(dt); err != nil { 66 | return nil, errors.Wrap(err, "failed to parse op") 67 | } 68 | dgst := digest.FromBytes(dt) 69 | ent := llbOp{Op: op, Digest: dgst, OpMetadata: def.Metadata[dgst]} 70 | ops = append(ops, ent) 71 | } 72 | return ops, nil 73 | } 74 | 75 | func writeDot(ops []llbOp, w io.Writer) { 76 | // TODO: print OpMetadata 77 | fmt.Fprintln(w, "digraph {") 78 | defer fmt.Fprintln(w, "}") 79 | for _, op := range ops { 80 | name, shape := attr(op.Digest, op.Op) 81 | fmt.Fprintf(w, " %q [label=%q shape=%q];\n", op.Digest, name, shape) 82 | } 83 | for _, op := range ops { 84 | for i, inp := range op.Op.Inputs { 85 | label := "" 86 | if eo, ok := op.Op.Op.(*pb.Op_Exec); ok { 87 | for _, m := range eo.Exec.Mounts { 88 | if int(m.Input) == i && m.Dest != "/" { 89 | label = m.Dest 90 | } 91 | } 92 | } 93 | fmt.Fprintf(w, " %q -> %q [label=%q];\n", inp.Digest, op.Digest, label) 94 | } 95 | } 96 | } 97 | 98 | func attr(dgst digest.Digest, op pb.Op) (string, string) { 99 | switch op := op.Op.(type) { 100 | case *pb.Op_Source: 101 | return op.Source.Identifier, "ellipse" 102 | case *pb.Op_Exec: 103 | return strings.Join(op.Exec.Meta.Args, " "), "box" 104 | case *pb.Op_Build: 105 | return "build", "box3d" 106 | case *pb.Op_File: 107 | names := []string{} 108 | 109 | for _, action := range op.File.Actions { 110 | var name string 111 | 112 | switch act := action.Action.(type) { 113 | case *pb.FileAction_Copy: 114 | name = fmt.Sprintf("copy{src=%s, dest=%s}", act.Copy.Src, act.Copy.Dest) 115 | case *pb.FileAction_Mkfile: 116 | name = fmt.Sprintf("mkfile{path=%s}", act.Mkfile.Path) 117 | case *pb.FileAction_Mkdir: 118 | name = fmt.Sprintf("mkdir{path=%s}", act.Mkdir.Path) 119 | case *pb.FileAction_Rm: 120 | name = fmt.Sprintf("rm{path=%s}", act.Rm.Path) 121 | } 122 | 123 | names = append(names, name) 124 | } 125 | return strings.Join(names, ","), "note" 126 | default: 127 | return dgst.String(), "plaintext" 128 | } 129 | } 130 | 131 | func getCustomString(actions []*pb.FileAction) string { 132 | // set custom messages from fileOp actions 133 | // https://github.com/po3rin/dockerdot/issues/3 134 | for _, v := range actions { 135 | switch action := v.Action.(type) { 136 | case *pb.FileAction_Copy: 137 | return fmt.Sprintf("copy src='%v' dest='%v'", action.Copy.Src, action.Copy.Dest) 138 | case *pb.FileAction_Mkfile: 139 | return fmt.Sprintf("mkfile %+v", action.Mkfile.Path) 140 | case *pb.FileAction_Mkdir: 141 | return fmt.Sprintf("mkdir: %+v\n", action.Mkdir.Path) 142 | case *pb.FileAction_Rm: 143 | return fmt.Sprintf("rm: %+v\n", action.Rm.Path) 144 | default: 145 | return "" 146 | } 147 | } 148 | return "" 149 | } 150 | -------------------------------------------------------------------------------- /docker2dot/docker2dot_test.go: -------------------------------------------------------------------------------- 1 | package docker2dot_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/po3rin/dockerdot/docker2dot" 8 | ) 9 | 10 | // TODO: ignore hash. 11 | func TestDocker2Dot(t *testing.T) { 12 | tests := []struct { 13 | input []byte 14 | want []byte 15 | }{ 16 | { 17 | input: []byte( 18 | `FROM golang:1.12 AS stage0 19 | WORKDIR /go 20 | ADD ./ /go 21 | RUN go build -o stage0_bin 22 | FROM golang:1.12 AS stage1 23 | WORKDIR /go 24 | ADD ./ /go 25 | RUN go build -o stage1_bin 26 | FROM golang:1.12 27 | COPY --from=stage0 /go/stage0_bin / 28 | COPY --from=stage1 /go/stage1_bin / `, 29 | ), 30 | want: []byte( 31 | `digraph { 32 | "sha256:1c5320070ff30eecf3265f227b2646e54428945092b0866a55da4bb20415f066" [label="docker-image://docker.io/docker/dockerfile-copy:v0.1.9" shape="ellipse"]; 33 | "sha256:73f494e3e5baaa46b1a4cc3a4d1c59c049e82cd5cd70b3c166fe09cbe4cb143e" [label="local://context" shape="ellipse"]; 34 | "sha256:8f440bbee7e64fd9a1846d02a7e195458de7f91994244c95683866804fed65d6" [label="docker-image://docker.io/library/golang:1.12" shape="ellipse"]; 35 | "sha256:1d9bc5098154416cf2d5ba0b0aaba6ab88348e4cc0a728bf0f37a7db32c36426" [label="copy --unpack /src-0 go" shape="box"]; 36 | "sha256:0f1183cf8ee0399b25b89d75d371ff7dd2cf4b9f10c73a0761ecfd29ef4a9120" [label="/bin/sh -c go build -o stage1_bin" shape="box"]; 37 | "sha256:348c2dded9e336b03cea79e2cfebbe0f4a7189cf306fa283380ed4b7e5e51d32" [label="/bin/sh -c go build -o stage0_bin" shape="box"]; 38 | "sha256:1b2db4270fb36ed9837304839bbaa1d532bd3893fbd2bfd57863d6dbe85e0e7c" [label="copy /src-0/stage0_bin ./" shape="box"]; 39 | "sha256:2026e0c8c202590a664fc4e03de5d236045bea576d68fd7078d76e86cdecf5ba" [label="copy /src-0/stage1_bin ./" shape="box"]; 40 | "sha256:4b0a5cf7b4ce98d7862bf8cbb594ce3527717bedb8675dc37dc0adcd4512d1f9" [label="sha256:4b0a5cf7b4ce98d7862bf8cbb594ce3527717bedb8675dc37dc0adcd4512d1f9" shape="plaintext"]; 41 | "sha256:1c5320070ff30eecf3265f227b2646e54428945092b0866a55da4bb20415f066" -> "sha256:1d9bc5098154416cf2d5ba0b0aaba6ab88348e4cc0a728bf0f37a7db32c36426" [label=""]; 42 | "sha256:8f440bbee7e64fd9a1846d02a7e195458de7f91994244c95683866804fed65d6" -> "sha256:1d9bc5098154416cf2d5ba0b0aaba6ab88348e4cc0a728bf0f37a7db32c36426" [label="/dest"]; 43 | "sha256:73f494e3e5baaa46b1a4cc3a4d1c59c049e82cd5cd70b3c166fe09cbe4cb143e" -> "sha256:1d9bc5098154416cf2d5ba0b0aaba6ab88348e4cc0a728bf0f37a7db32c36426" [label="/src-0"]; 44 | "sha256:1d9bc5098154416cf2d5ba0b0aaba6ab88348e4cc0a728bf0f37a7db32c36426" -> "sha256:0f1183cf8ee0399b25b89d75d371ff7dd2cf4b9f10c73a0761ecfd29ef4a9120" [label=""]; 45 | "sha256:1d9bc5098154416cf2d5ba0b0aaba6ab88348e4cc0a728bf0f37a7db32c36426" -> "sha256:348c2dded9e336b03cea79e2cfebbe0f4a7189cf306fa283380ed4b7e5e51d32" [label=""]; 46 | "sha256:1c5320070ff30eecf3265f227b2646e54428945092b0866a55da4bb20415f066" -> "sha256:1b2db4270fb36ed9837304839bbaa1d532bd3893fbd2bfd57863d6dbe85e0e7c" [label=""]; 47 | "sha256:8f440bbee7e64fd9a1846d02a7e195458de7f91994244c95683866804fed65d6" -> "sha256:1b2db4270fb36ed9837304839bbaa1d532bd3893fbd2bfd57863d6dbe85e0e7c" [label="/dest"]; 48 | "sha256:348c2dded9e336b03cea79e2cfebbe0f4a7189cf306fa283380ed4b7e5e51d32" -> "sha256:1b2db4270fb36ed9837304839bbaa1d532bd3893fbd2bfd57863d6dbe85e0e7c" [label="/src-0/stage0_bin"]; 49 | "sha256:1c5320070ff30eecf3265f227b2646e54428945092b0866a55da4bb20415f066" -> "sha256:2026e0c8c202590a664fc4e03de5d236045bea576d68fd7078d76e86cdecf5ba" [label=""]; 50 | "sha256:1b2db4270fb36ed9837304839bbaa1d532bd3893fbd2bfd57863d6dbe85e0e7c" -> "sha256:2026e0c8c202590a664fc4e03de5d236045bea576d68fd7078d76e86cdecf5ba" [label="/dest"]; 51 | "sha256:0f1183cf8ee0399b25b89d75d371ff7dd2cf4b9f10c73a0761ecfd29ef4a9120" -> "sha256:2026e0c8c202590a664fc4e03de5d236045bea576d68fd7078d76e86cdecf5ba" [label="/src-0/stage1_bin"]; 52 | "sha256:2026e0c8c202590a664fc4e03de5d236045bea576d68fd7078d76e86cdecf5ba" -> "sha256:4b0a5cf7b4ce98d7862bf8cbb594ce3527717bedb8675dc37dc0adcd4512d1f9" [label=""]; 53 | }`, 54 | ), 55 | }, 56 | } 57 | 58 | for _, tt := range tests { 59 | got, err := docker2dot.Docker2Dot(tt.input) 60 | if err != nil { 61 | t.Errorf("got unexpected error: %+v", err) 62 | } 63 | if !reflect.DeepEqual(got, tt.want) { 64 | t.Errorf("got unexpected result:\ngot: %+v\nwant: %+v\n", string(got), string(tt.want)) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /dockerfile2llb/convert.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/url" 9 | "path" 10 | "path/filepath" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/containerd/containerd/platforms" 16 | "github.com/docker/distribution/reference" 17 | "github.com/docker/go-connections/nat" 18 | "github.com/moby/buildkit/client/llb" 19 | "github.com/moby/buildkit/client/llb/imagemetaresolver" 20 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 21 | "github.com/moby/buildkit/frontend/dockerfile/parser" 22 | "github.com/moby/buildkit/frontend/dockerfile/shell" 23 | gw "github.com/moby/buildkit/frontend/gateway/client" 24 | "github.com/moby/buildkit/solver/pb" 25 | "github.com/moby/buildkit/util/apicaps" 26 | "github.com/moby/buildkit/util/system" 27 | specs "github.com/opencontainers/image-spec/specs-go/v1" 28 | "github.com/pkg/errors" 29 | "golang.org/x/sync/errgroup" 30 | ) 31 | 32 | const ( 33 | emptyImageName = "scratch" 34 | defaultContextLocalName = "context" 35 | historyComment = "buildkit.dockerfile.v0" 36 | 37 | DefaultCopyImage = "docker/dockerfile-copy:v0.1.9" 38 | ) 39 | 40 | type ConvertOpt struct { 41 | Target string 42 | MetaResolver llb.ImageMetaResolver 43 | BuildArgs map[string]string 44 | Labels map[string]string 45 | SessionID string 46 | BuildContext *llb.State 47 | Excludes []string 48 | // IgnoreCache contains names of the stages that should not use build cache. 49 | // Empty slice means ignore cache for all stages. Nil doesn't disable cache. 50 | IgnoreCache []string 51 | // CacheIDNamespace scopes the IDs for different cache mounts 52 | CacheIDNamespace string 53 | ImageResolveMode llb.ResolveMode 54 | TargetPlatform *specs.Platform 55 | BuildPlatforms []specs.Platform 56 | PrefixPlatform bool 57 | ExtraHosts []llb.HostIP 58 | ForceNetMode pb.NetMode 59 | OverrideCopyImage string 60 | LLBCaps *apicaps.CapSet 61 | ContextLocalName string 62 | } 63 | 64 | func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, *Image, error) { 65 | if len(dt) == 0 { 66 | return nil, nil, errors.Errorf("the Dockerfile cannot be empty") 67 | } 68 | 69 | if opt.ContextLocalName == "" { 70 | opt.ContextLocalName = defaultContextLocalName 71 | } 72 | 73 | platformOpt := buildPlatformOpt(&opt) 74 | 75 | optMetaArgs := getPlatformArgs(platformOpt) 76 | for i, arg := range optMetaArgs { 77 | optMetaArgs[i] = setKVValue(arg, opt.BuildArgs) 78 | } 79 | 80 | dockerfile, err := parser.Parse(bytes.NewReader(dt)) 81 | if err != nil { 82 | return nil, nil, err 83 | } 84 | 85 | proxyEnv := proxyEnvFromBuildArgs(opt.BuildArgs) 86 | 87 | stages, metaArgs, err := instructions.Parse(dockerfile.AST) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | 92 | shlex := shell.NewLex(dockerfile.EscapeToken) 93 | 94 | for _, metaArg := range metaArgs { 95 | if metaArg.Value != nil { 96 | *metaArg.Value, _ = shlex.ProcessWordWithMap(*metaArg.Value, metaArgsToMap(optMetaArgs)) 97 | } 98 | optMetaArgs = append(optMetaArgs, setKVValue(metaArg.KeyValuePairOptional, opt.BuildArgs)) 99 | } 100 | 101 | metaResolver := opt.MetaResolver 102 | if metaResolver == nil { 103 | metaResolver = imagemetaresolver.Default() 104 | } 105 | 106 | allDispatchStates := newDispatchStates() 107 | 108 | // set base state for every image 109 | for i, st := range stages { 110 | name, err := shlex.ProcessWordWithMap(st.BaseName, metaArgsToMap(optMetaArgs)) 111 | if err != nil { 112 | return nil, nil, err 113 | } 114 | if name == "" { 115 | return nil, nil, errors.Errorf("base name (%s) should not be blank", st.BaseName) 116 | } 117 | st.BaseName = name 118 | 119 | ds := &dispatchState{ 120 | stage: st, 121 | deps: make(map[*dispatchState]struct{}), 122 | ctxPaths: make(map[string]struct{}), 123 | stageName: st.Name, 124 | prefixPlatform: opt.PrefixPlatform, 125 | } 126 | 127 | if st.Name == "" { 128 | ds.stageName = fmt.Sprintf("stage-%d", i) 129 | } 130 | 131 | if v := st.Platform; v != "" { 132 | v, err := shlex.ProcessWordWithMap(v, metaArgsToMap(optMetaArgs)) 133 | if err != nil { 134 | return nil, nil, errors.Wrapf(err, "failed to process arguments for platform %s", v) 135 | } 136 | 137 | p, err := platforms.Parse(v) 138 | if err != nil { 139 | return nil, nil, errors.Wrapf(err, "failed to parse platform %s", v) 140 | } 141 | ds.platform = &p 142 | } 143 | allDispatchStates.addState(ds) 144 | 145 | total := 0 146 | if ds.stage.BaseName != emptyImageName && ds.base == nil { 147 | total = 1 148 | } 149 | for _, cmd := range ds.stage.Commands { 150 | switch cmd.(type) { 151 | case *instructions.AddCommand, *instructions.CopyCommand, *instructions.RunCommand: 152 | total++ 153 | case *instructions.WorkdirCommand: 154 | if useFileOp(opt.BuildArgs, opt.LLBCaps) { 155 | total++ 156 | } 157 | } 158 | } 159 | ds.cmdTotal = total 160 | 161 | if opt.IgnoreCache != nil { 162 | if len(opt.IgnoreCache) == 0 { 163 | ds.ignoreCache = true 164 | } else if st.Name != "" { 165 | for _, n := range opt.IgnoreCache { 166 | if strings.EqualFold(n, st.Name) { 167 | ds.ignoreCache = true 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | var target *dispatchState 175 | if opt.Target == "" { 176 | target = allDispatchStates.lastTarget() 177 | } else { 178 | var ok bool 179 | target, ok = allDispatchStates.findStateByName(opt.Target) 180 | if !ok { 181 | return nil, nil, errors.Errorf("target stage %s could not be found", opt.Target) 182 | } 183 | } 184 | 185 | // fill dependencies to stages so unreachable ones can avoid loading image configs 186 | for _, d := range allDispatchStates.states { 187 | d.commands = make([]command, len(d.stage.Commands)) 188 | for i, cmd := range d.stage.Commands { 189 | newCmd, err := toCommand(cmd, allDispatchStates) 190 | if err != nil { 191 | return nil, nil, err 192 | } 193 | d.commands[i] = newCmd 194 | for _, src := range newCmd.sources { 195 | if src != nil { 196 | d.deps[src] = struct{}{} 197 | if src.unregistered { 198 | allDispatchStates.addState(src) 199 | } 200 | } 201 | } 202 | } 203 | } 204 | 205 | if has, state := hasCircularDependency(allDispatchStates.states); has { 206 | return nil, nil, fmt.Errorf("circular dependency detected on stage: %s", state.stageName) 207 | } 208 | 209 | if len(allDispatchStates.states) == 1 { 210 | allDispatchStates.states[0].stageName = "" 211 | } 212 | 213 | eg, ctx := errgroup.WithContext(ctx) 214 | for i, d := range allDispatchStates.states { 215 | reachable := isReachable(target, d) 216 | // resolve image config for every stage 217 | if d.base == nil { 218 | if d.stage.BaseName == emptyImageName { 219 | d.state = llb.Scratch() 220 | d.image = emptyImage(platformOpt.targetPlatform) 221 | continue 222 | } 223 | func(i int, d *dispatchState) { 224 | eg.Go(func() error { 225 | ref, err := reference.ParseNormalizedNamed(d.stage.BaseName) 226 | if err != nil { 227 | return errors.Wrapf(err, "failed to parse stage name %q", d.stage.BaseName) 228 | } 229 | platform := d.platform 230 | if platform == nil { 231 | platform = &platformOpt.targetPlatform 232 | } 233 | d.stage.BaseName = reference.TagNameOnly(ref).String() 234 | var isScratch bool 235 | if metaResolver != nil && reachable && !d.unregistered { 236 | prefix := "[" 237 | if opt.PrefixPlatform && platform != nil { 238 | prefix += platforms.Format(*platform) + " " 239 | } 240 | prefix += "internal]" 241 | dgst, dt, err := metaResolver.ResolveImageConfig(ctx, d.stage.BaseName, gw.ResolveImageConfigOpt{ 242 | Platform: platform, 243 | ResolveMode: opt.ImageResolveMode.String(), 244 | LogName: fmt.Sprintf("%s load metadata for %s", prefix, d.stage.BaseName), 245 | }) 246 | if err == nil { // handle the error while builder is actually running 247 | var img Image 248 | if err := json.Unmarshal(dt, &img); err != nil { 249 | return err 250 | } 251 | img.Created = nil 252 | // if there is no explicit target platform, try to match based on image config 253 | if d.platform == nil && platformOpt.implicitTarget { 254 | p := autoDetectPlatform(img, *platform, platformOpt.buildPlatforms) 255 | platform = &p 256 | } 257 | d.image = img 258 | if dgst != "" { 259 | ref, err = reference.WithDigest(ref, dgst) 260 | if err != nil { 261 | return err 262 | } 263 | } 264 | d.stage.BaseName = ref.String() 265 | if len(img.RootFS.DiffIDs) == 0 { 266 | isScratch = true 267 | // schema1 images can't return diffIDs so double check :( 268 | for _, h := range img.History { 269 | if !h.EmptyLayer { 270 | isScratch = false 271 | break 272 | } 273 | } 274 | } 275 | } 276 | } 277 | if isScratch { 278 | d.state = llb.Scratch() 279 | } else { 280 | d.state = llb.Image(d.stage.BaseName, dfCmd(d.stage.SourceCode), llb.Platform(*platform), opt.ImageResolveMode, llb.WithCustomName(prefixCommand(d, "FROM "+d.stage.BaseName, opt.PrefixPlatform, platform))) 281 | } 282 | d.platform = platform 283 | return nil 284 | }) 285 | }(i, d) 286 | } 287 | } 288 | 289 | if err := eg.Wait(); err != nil { 290 | return nil, nil, err 291 | } 292 | 293 | buildContext := &mutableOutput{} 294 | ctxPaths := map[string]struct{}{} 295 | 296 | for _, d := range allDispatchStates.states { 297 | if !isReachable(target, d) { 298 | continue 299 | } 300 | if d.base != nil { 301 | d.state = d.base.state 302 | d.platform = d.base.platform 303 | d.image = clone(d.base.image) 304 | } 305 | 306 | // make sure that PATH is always set 307 | if _, ok := shell.BuildEnvs(d.image.Config.Env)["PATH"]; !ok { 308 | d.image.Config.Env = append(d.image.Config.Env, "PATH="+system.DefaultPathEnv) 309 | } 310 | 311 | // initialize base metadata from image conf 312 | for _, env := range d.image.Config.Env { 313 | k, v := parseKeyValue(env) 314 | d.state = d.state.AddEnv(k, v) 315 | } 316 | if d.image.Config.WorkingDir != "" { 317 | if err = dispatchWorkdir(d, &instructions.WorkdirCommand{Path: d.image.Config.WorkingDir}, false, nil); err != nil { 318 | return nil, nil, err 319 | } 320 | } 321 | if d.image.Config.User != "" { 322 | if err = dispatchUser(d, &instructions.UserCommand{User: d.image.Config.User}, false); err != nil { 323 | return nil, nil, err 324 | } 325 | } 326 | d.state = d.state.Network(opt.ForceNetMode) 327 | 328 | opt := dispatchOpt{ 329 | allDispatchStates: allDispatchStates, 330 | metaArgs: optMetaArgs, 331 | buildArgValues: opt.BuildArgs, 332 | shlex: shlex, 333 | sessionID: opt.SessionID, 334 | buildContext: llb.NewState(buildContext), 335 | proxyEnv: proxyEnv, 336 | cacheIDNamespace: opt.CacheIDNamespace, 337 | buildPlatforms: platformOpt.buildPlatforms, 338 | targetPlatform: platformOpt.targetPlatform, 339 | extraHosts: opt.ExtraHosts, 340 | copyImage: opt.OverrideCopyImage, 341 | llbCaps: opt.LLBCaps, 342 | } 343 | if opt.copyImage == "" { 344 | opt.copyImage = DefaultCopyImage 345 | } 346 | 347 | if err = dispatchOnBuild(d, d.image.Config.OnBuild, opt); err != nil { 348 | return nil, nil, err 349 | } 350 | 351 | for _, cmd := range d.commands { 352 | if err := dispatch(d, cmd, opt); err != nil { 353 | return nil, nil, err 354 | } 355 | } 356 | 357 | for p := range d.ctxPaths { 358 | ctxPaths[p] = struct{}{} 359 | } 360 | } 361 | 362 | if len(opt.Labels) != 0 && target.image.Config.Labels == nil { 363 | target.image.Config.Labels = make(map[string]string, len(opt.Labels)) 364 | } 365 | for k, v := range opt.Labels { 366 | target.image.Config.Labels[k] = v 367 | } 368 | 369 | opts := []llb.LocalOption{ 370 | llb.SessionID(opt.SessionID), 371 | llb.ExcludePatterns(opt.Excludes), 372 | llb.SharedKeyHint(opt.ContextLocalName), 373 | WithInternalName("load build context"), 374 | } 375 | if includePatterns := normalizeContextPaths(ctxPaths); includePatterns != nil { 376 | opts = append(opts, llb.FollowPaths(includePatterns)) 377 | } 378 | 379 | bc := llb.Local(opt.ContextLocalName, opts...) 380 | if opt.BuildContext != nil { 381 | bc = *opt.BuildContext 382 | } 383 | buildContext.Output = bc.Output() 384 | 385 | defaults := []llb.ConstraintsOpt{ 386 | llb.Platform(platformOpt.targetPlatform), 387 | } 388 | if opt.LLBCaps != nil { 389 | defaults = append(defaults, llb.WithCaps(*opt.LLBCaps)) 390 | } 391 | st := target.state.SetMarshalDefaults(defaults...) 392 | 393 | if !platformOpt.implicitTarget { 394 | target.image.OS = platformOpt.targetPlatform.OS 395 | target.image.Architecture = platformOpt.targetPlatform.Architecture 396 | target.image.Variant = platformOpt.targetPlatform.Variant 397 | } 398 | 399 | return &st, &target.image, nil 400 | } 401 | 402 | func metaArgsToMap(metaArgs []instructions.KeyValuePairOptional) map[string]string { 403 | m := map[string]string{} 404 | 405 | for _, arg := range metaArgs { 406 | m[arg.Key] = arg.ValueString() 407 | } 408 | 409 | return m 410 | } 411 | 412 | func toCommand(ic instructions.Command, allDispatchStates *dispatchStates) (command, error) { 413 | cmd := command{Command: ic} 414 | if c, ok := ic.(*instructions.CopyCommand); ok { 415 | if c.From != "" { 416 | var stn *dispatchState 417 | index, err := strconv.Atoi(c.From) 418 | if err != nil { 419 | stn, ok = allDispatchStates.findStateByName(c.From) 420 | if !ok { 421 | stn = &dispatchState{ 422 | stage: instructions.Stage{BaseName: c.From}, 423 | deps: make(map[*dispatchState]struct{}), 424 | unregistered: true, 425 | } 426 | } 427 | } else { 428 | stn, err = allDispatchStates.findStateByIndex(index) 429 | if err != nil { 430 | return command{}, err 431 | } 432 | } 433 | cmd.sources = []*dispatchState{stn} 434 | } 435 | } 436 | 437 | if ok := detectRunMount(&cmd, allDispatchStates); ok { 438 | return cmd, nil 439 | } 440 | 441 | return cmd, nil 442 | } 443 | 444 | type dispatchOpt struct { 445 | allDispatchStates *dispatchStates 446 | metaArgs []instructions.KeyValuePairOptional 447 | buildArgValues map[string]string 448 | shlex *shell.Lex 449 | sessionID string 450 | buildContext llb.State 451 | proxyEnv *llb.ProxyEnv 452 | cacheIDNamespace string 453 | targetPlatform specs.Platform 454 | buildPlatforms []specs.Platform 455 | extraHosts []llb.HostIP 456 | copyImage string 457 | llbCaps *apicaps.CapSet 458 | } 459 | 460 | func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error { 461 | if ex, ok := cmd.Command.(instructions.SupportsSingleWordExpansion); ok { 462 | err := ex.Expand(func(word string) (string, error) { 463 | return opt.shlex.ProcessWordWithMap(word, toEnvMap(d.buildArgs, d.image.Config.Env)) 464 | }) 465 | if err != nil { 466 | return err 467 | } 468 | } 469 | 470 | var err error 471 | switch c := cmd.Command.(type) { 472 | case *instructions.MaintainerCommand: 473 | err = dispatchMaintainer(d, c) 474 | case *instructions.EnvCommand: 475 | err = dispatchEnv(d, c) 476 | case *instructions.RunCommand: 477 | err = dispatchRun(d, c, opt.proxyEnv, cmd.sources, opt) 478 | case *instructions.WorkdirCommand: 479 | err = dispatchWorkdir(d, c, true, &opt) 480 | case *instructions.AddCommand: 481 | err = dispatchCopy(d, c.SourcesAndDest, opt.buildContext, true, c, c.Chown, opt) 482 | if err == nil { 483 | for _, src := range c.Sources() { 484 | if !strings.HasPrefix(src, "http://") && !strings.HasPrefix(src, "https://") { 485 | d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{} 486 | } 487 | } 488 | } 489 | case *instructions.LabelCommand: 490 | err = dispatchLabel(d, c) 491 | case *instructions.OnbuildCommand: 492 | err = dispatchOnbuild(d, c) 493 | case *instructions.CmdCommand: 494 | err = dispatchCmd(d, c) 495 | case *instructions.EntrypointCommand: 496 | err = dispatchEntrypoint(d, c) 497 | case *instructions.HealthCheckCommand: 498 | err = dispatchHealthcheck(d, c) 499 | case *instructions.ExposeCommand: 500 | err = dispatchExpose(d, c, opt.shlex) 501 | case *instructions.UserCommand: 502 | err = dispatchUser(d, c, true) 503 | case *instructions.VolumeCommand: 504 | err = dispatchVolume(d, c) 505 | // WARN: wabassembly can not use signal package... 506 | // case *instructions.StopSignalCommand: 507 | // err = dispatchStopSignal(d, c) 508 | case *instructions.ShellCommand: 509 | err = dispatchShell(d, c) 510 | case *instructions.ArgCommand: 511 | err = dispatchArg(d, c, opt.metaArgs, opt.buildArgValues) 512 | case *instructions.CopyCommand: 513 | l := opt.buildContext 514 | if len(cmd.sources) != 0 { 515 | l = cmd.sources[0].state 516 | } 517 | err = dispatchCopy(d, c.SourcesAndDest, l, false, c, c.Chown, opt) 518 | if err == nil && len(cmd.sources) == 0 { 519 | for _, src := range c.Sources() { 520 | d.ctxPaths[path.Join("/", filepath.ToSlash(src))] = struct{}{} 521 | } 522 | } 523 | default: 524 | } 525 | return err 526 | } 527 | 528 | type dispatchState struct { 529 | state llb.State 530 | image Image 531 | platform *specs.Platform 532 | stage instructions.Stage 533 | base *dispatchState 534 | deps map[*dispatchState]struct{} 535 | buildArgs []instructions.KeyValuePairOptional 536 | commands []command 537 | ctxPaths map[string]struct{} 538 | ignoreCache bool 539 | cmdSet bool 540 | unregistered bool 541 | stageName string 542 | cmdIndex int 543 | cmdTotal int 544 | prefixPlatform bool 545 | } 546 | 547 | type dispatchStates struct { 548 | states []*dispatchState 549 | statesByName map[string]*dispatchState 550 | } 551 | 552 | func newDispatchStates() *dispatchStates { 553 | return &dispatchStates{statesByName: map[string]*dispatchState{}} 554 | } 555 | 556 | func (dss *dispatchStates) addState(ds *dispatchState) { 557 | dss.states = append(dss.states, ds) 558 | 559 | if d, ok := dss.statesByName[ds.stage.BaseName]; ok { 560 | ds.base = d 561 | } 562 | if ds.stage.Name != "" { 563 | dss.statesByName[strings.ToLower(ds.stage.Name)] = ds 564 | } 565 | } 566 | 567 | func (dss *dispatchStates) findStateByName(name string) (*dispatchState, bool) { 568 | ds, ok := dss.statesByName[strings.ToLower(name)] 569 | return ds, ok 570 | } 571 | 572 | func (dss *dispatchStates) findStateByIndex(index int) (*dispatchState, error) { 573 | if index < 0 || index >= len(dss.states) { 574 | return nil, errors.Errorf("invalid stage index %d", index) 575 | } 576 | 577 | return dss.states[index], nil 578 | } 579 | 580 | func (dss *dispatchStates) lastTarget() *dispatchState { 581 | return dss.states[len(dss.states)-1] 582 | } 583 | 584 | type command struct { 585 | instructions.Command 586 | sources []*dispatchState 587 | } 588 | 589 | func dispatchOnBuild(d *dispatchState, triggers []string, opt dispatchOpt) error { 590 | for _, trigger := range triggers { 591 | ast, err := parser.Parse(strings.NewReader(trigger)) 592 | if err != nil { 593 | return err 594 | } 595 | if len(ast.AST.Children) != 1 { 596 | return errors.New("onbuild trigger should be a single expression") 597 | } 598 | ic, err := instructions.ParseCommand(ast.AST.Children[0]) 599 | if err != nil { 600 | return err 601 | } 602 | cmd, err := toCommand(ic, opt.allDispatchStates) 603 | if err != nil { 604 | return err 605 | } 606 | if err := dispatch(d, cmd, opt); err != nil { 607 | return err 608 | } 609 | } 610 | return nil 611 | } 612 | 613 | func dispatchEnv(d *dispatchState, c *instructions.EnvCommand) error { 614 | commitMessage := bytes.NewBufferString("ENV") 615 | for _, e := range c.Env { 616 | commitMessage.WriteString(" " + e.String()) 617 | d.state = d.state.AddEnv(e.Key, e.Value) 618 | d.image.Config.Env = addEnv(d.image.Config.Env, e.Key, e.Value) 619 | } 620 | return commitToHistory(&d.image, commitMessage.String(), false, nil) 621 | } 622 | 623 | func dispatchRun(d *dispatchState, c *instructions.RunCommand, proxy *llb.ProxyEnv, sources []*dispatchState, dopt dispatchOpt) error { 624 | var args []string = c.CmdLine 625 | if c.PrependShell { 626 | args = withShell(d.image, args) 627 | } 628 | env := d.state.Env() 629 | opt := []llb.RunOption{llb.Args(args)} 630 | for _, arg := range d.buildArgs { 631 | if arg.Value != nil { 632 | env = append(env, fmt.Sprintf("%s=%s", arg.Key, arg.ValueString())) 633 | opt = append(opt, llb.AddEnv(arg.Key, arg.ValueString())) 634 | } 635 | } 636 | opt = append(opt, dfCmd(c)) 637 | if d.ignoreCache { 638 | opt = append(opt, llb.IgnoreCache) 639 | } 640 | if proxy != nil { 641 | opt = append(opt, llb.WithProxy(*proxy)) 642 | } 643 | 644 | runMounts, err := dispatchRunMounts(d, c, sources, dopt) 645 | if err != nil { 646 | return err 647 | } 648 | opt = append(opt, runMounts...) 649 | 650 | shlex := *dopt.shlex 651 | shlex.RawQuotes = true 652 | shlex.SkipUnsetEnv = true 653 | 654 | opt = append(opt, llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(&shlex, c.String(), env)), d.prefixPlatform, d.state.GetPlatform()))) 655 | for _, h := range dopt.extraHosts { 656 | opt = append(opt, llb.AddExtraHost(h.Host, h.IP)) 657 | } 658 | d.state = d.state.Run(opt...).Root() 659 | return commitToHistory(&d.image, "RUN "+runCommandString(args, d.buildArgs), true, &d.state) 660 | } 661 | 662 | func dispatchWorkdir(d *dispatchState, c *instructions.WorkdirCommand, commit bool, opt *dispatchOpt) error { 663 | d.state = d.state.Dir(c.Path) 664 | wd := c.Path 665 | if !path.IsAbs(c.Path) { 666 | wd = path.Join("/", d.image.Config.WorkingDir, wd) 667 | } 668 | d.image.Config.WorkingDir = wd 669 | if commit { 670 | withLayer := false 671 | if wd != "/" && opt != nil && useFileOp(opt.buildArgValues, opt.llbCaps) { 672 | mkdirOpt := []llb.MkdirOption{llb.WithParents(true)} 673 | if user := d.image.Config.User; user != "" { 674 | mkdirOpt = append(mkdirOpt, llb.WithUser(user)) 675 | } 676 | platform := opt.targetPlatform 677 | if d.platform != nil { 678 | platform = *d.platform 679 | } 680 | d.state = d.state.File(llb.Mkdir(wd, 0755, mkdirOpt...), llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, c.String(), d.state.Env())), d.prefixPlatform, &platform))) 681 | withLayer = true 682 | } 683 | return commitToHistory(&d.image, "WORKDIR "+wd, withLayer, nil) 684 | } 685 | return nil 686 | } 687 | 688 | func dispatchCopyFileOp(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, opt dispatchOpt) error { 689 | dest := path.Join("/", pathRelativeToWorkingDir(d.state, c.Dest())) 690 | if c.Dest() == "." || c.Dest() == "" || c.Dest()[len(c.Dest())-1] == filepath.Separator { 691 | dest += string(filepath.Separator) 692 | } 693 | 694 | var copyOpt []llb.CopyOption 695 | 696 | if chown != "" { 697 | copyOpt = append(copyOpt, llb.WithUser(chown)) 698 | } 699 | 700 | commitMessage := bytes.NewBufferString("") 701 | if isAddCommand { 702 | commitMessage.WriteString("ADD") 703 | } else { 704 | commitMessage.WriteString("COPY") 705 | } 706 | 707 | var a *llb.FileAction 708 | 709 | for _, src := range c.Sources() { 710 | commitMessage.WriteString(" " + src) 711 | if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { 712 | if !isAddCommand { 713 | return errors.New("source can't be a URL for COPY") 714 | } 715 | 716 | // Resources from remote URLs are not decompressed. 717 | // https://docs.docker.com/engine/reference/builder/#add 718 | // 719 | // Note: mixing up remote archives and local archives in a single ADD instruction 720 | // would result in undefined behavior: https://github.com/moby/buildkit/pull/387#discussion_r189494717 721 | u, err := url.Parse(src) 722 | f := "__unnamed__" 723 | if err == nil { 724 | if base := path.Base(u.Path); base != "." && base != "/" { 725 | f = base 726 | } 727 | } 728 | 729 | st := llb.HTTP(src, llb.Filename(f), dfCmd(c)) 730 | 731 | opts := append([]llb.CopyOption{&llb.CopyInfo{ 732 | CreateDestPath: true, 733 | }}, copyOpt...) 734 | 735 | if a == nil { 736 | a = llb.Copy(st, f, dest, opts...) 737 | } else { 738 | a = a.Copy(st, f, dest, opts...) 739 | } 740 | } else { 741 | opts := append([]llb.CopyOption{&llb.CopyInfo{ 742 | FollowSymlinks: true, 743 | CopyDirContentsOnly: true, 744 | AttemptUnpack: isAddCommand, 745 | CreateDestPath: true, 746 | AllowWildcard: true, 747 | AllowEmptyWildcard: true, 748 | }}, copyOpt...) 749 | 750 | if a == nil { 751 | a = llb.Copy(sourceState, src, dest, opts...) 752 | } else { 753 | a = a.Copy(sourceState, src, dest, opts...) 754 | } 755 | } 756 | } 757 | 758 | commitMessage.WriteString(" " + c.Dest()) 759 | 760 | platform := opt.targetPlatform 761 | if d.platform != nil { 762 | platform = *d.platform 763 | } 764 | 765 | fileOpt := []llb.ConstraintsOpt{llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, cmdToPrint.String(), d.state.Env())), d.prefixPlatform, &platform))} 766 | if d.ignoreCache { 767 | fileOpt = append(fileOpt, llb.IgnoreCache) 768 | } 769 | 770 | d.state = d.state.File(a, fileOpt...) 771 | return commitToHistory(&d.image, commitMessage.String(), true, &d.state) 772 | } 773 | 774 | func dispatchCopy(d *dispatchState, c instructions.SourcesAndDest, sourceState llb.State, isAddCommand bool, cmdToPrint fmt.Stringer, chown string, opt dispatchOpt) error { 775 | if useFileOp(opt.buildArgValues, opt.llbCaps) { 776 | return dispatchCopyFileOp(d, c, sourceState, isAddCommand, cmdToPrint, chown, opt) 777 | } 778 | 779 | img := llb.Image(opt.copyImage, llb.MarkImageInternal, llb.Platform(opt.buildPlatforms[0]), WithInternalName("helper image for file operations")) 780 | 781 | dest := path.Join(".", pathRelativeToWorkingDir(d.state, c.Dest())) 782 | if c.Dest() == "." || c.Dest() == "" || c.Dest()[len(c.Dest())-1] == filepath.Separator { 783 | dest += string(filepath.Separator) 784 | } 785 | args := []string{"copy"} 786 | unpack := isAddCommand 787 | 788 | mounts := make([]llb.RunOption, 0, len(c.Sources())) 789 | if chown != "" { 790 | args = append(args, fmt.Sprintf("--chown=%s", chown)) 791 | _, _, err := parseUser(chown) 792 | if err != nil { 793 | mounts = append(mounts, llb.AddMount("/etc/passwd", d.state, llb.SourcePath("/etc/passwd"), llb.Readonly)) 794 | mounts = append(mounts, llb.AddMount("/etc/group", d.state, llb.SourcePath("/etc/group"), llb.Readonly)) 795 | } 796 | } 797 | 798 | commitMessage := bytes.NewBufferString("") 799 | if isAddCommand { 800 | commitMessage.WriteString("ADD") 801 | } else { 802 | commitMessage.WriteString("COPY") 803 | } 804 | 805 | for i, src := range c.Sources() { 806 | commitMessage.WriteString(" " + src) 807 | if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { 808 | if !isAddCommand { 809 | return errors.New("source can't be a URL for COPY") 810 | } 811 | 812 | // Resources from remote URLs are not decompressed. 813 | // https://docs.docker.com/engine/reference/builder/#add 814 | // 815 | // Note: mixing up remote archives and local archives in a single ADD instruction 816 | // would result in undefined behavior: https://github.com/moby/buildkit/pull/387#discussion_r189494717 817 | unpack = false 818 | u, err := url.Parse(src) 819 | f := "__unnamed__" 820 | if err == nil { 821 | if base := path.Base(u.Path); base != "." && base != "/" { 822 | f = base 823 | } 824 | } 825 | target := path.Join(fmt.Sprintf("/src-%d", i), f) 826 | args = append(args, target) 827 | mounts = append(mounts, llb.AddMount(path.Dir(target), llb.HTTP(src, llb.Filename(f), dfCmd(c)), llb.Readonly)) 828 | } else { 829 | d, f := splitWildcards(src) 830 | targetCmd := fmt.Sprintf("/src-%d", i) 831 | targetMount := targetCmd 832 | if f == "" { 833 | f = path.Base(src) 834 | targetMount = path.Join(targetMount, f) 835 | } 836 | targetCmd = path.Join(targetCmd, f) 837 | args = append(args, targetCmd) 838 | mounts = append(mounts, llb.AddMount(targetMount, sourceState, llb.SourcePath(d), llb.Readonly)) 839 | } 840 | } 841 | 842 | commitMessage.WriteString(" " + c.Dest()) 843 | 844 | args = append(args, dest) 845 | if unpack { 846 | args = append(args[:1], append([]string{"--unpack"}, args[1:]...)...) 847 | } 848 | 849 | platform := opt.targetPlatform 850 | if d.platform != nil { 851 | platform = *d.platform 852 | } 853 | 854 | runOpt := []llb.RunOption{llb.Args(args), llb.Dir("/dest"), llb.ReadonlyRootFS(), dfCmd(cmdToPrint), llb.WithCustomName(prefixCommand(d, uppercaseCmd(processCmdEnv(opt.shlex, cmdToPrint.String(), d.state.Env())), d.prefixPlatform, &platform))} 855 | if d.ignoreCache { 856 | runOpt = append(runOpt, llb.IgnoreCache) 857 | } 858 | 859 | if opt.llbCaps != nil { 860 | if err := opt.llbCaps.Supports(pb.CapExecMetaNetwork); err == nil { 861 | runOpt = append(runOpt, llb.Network(llb.NetModeNone)) 862 | } 863 | } 864 | 865 | run := img.Run(append(runOpt, mounts...)...) 866 | d.state = run.AddMount("/dest", d.state).Platform(platform) 867 | 868 | return commitToHistory(&d.image, commitMessage.String(), true, &d.state) 869 | } 870 | 871 | func dispatchMaintainer(d *dispatchState, c *instructions.MaintainerCommand) error { 872 | d.image.Author = c.Maintainer 873 | return commitToHistory(&d.image, fmt.Sprintf("MAINTAINER %v", c.Maintainer), false, nil) 874 | } 875 | 876 | func dispatchLabel(d *dispatchState, c *instructions.LabelCommand) error { 877 | commitMessage := bytes.NewBufferString("LABEL") 878 | if d.image.Config.Labels == nil { 879 | d.image.Config.Labels = make(map[string]string, len(c.Labels)) 880 | } 881 | for _, v := range c.Labels { 882 | d.image.Config.Labels[v.Key] = v.Value 883 | commitMessage.WriteString(" " + v.String()) 884 | } 885 | return commitToHistory(&d.image, commitMessage.String(), false, nil) 886 | } 887 | 888 | func dispatchOnbuild(d *dispatchState, c *instructions.OnbuildCommand) error { 889 | d.image.Config.OnBuild = append(d.image.Config.OnBuild, c.Expression) 890 | return nil 891 | } 892 | 893 | func dispatchCmd(d *dispatchState, c *instructions.CmdCommand) error { 894 | var args []string = c.CmdLine 895 | if c.PrependShell { 896 | args = withShell(d.image, args) 897 | } 898 | d.image.Config.Cmd = args 899 | d.image.Config.ArgsEscaped = true 900 | d.cmdSet = true 901 | return commitToHistory(&d.image, fmt.Sprintf("CMD %q", args), false, nil) 902 | } 903 | 904 | func dispatchEntrypoint(d *dispatchState, c *instructions.EntrypointCommand) error { 905 | var args []string = c.CmdLine 906 | if c.PrependShell { 907 | args = withShell(d.image, args) 908 | } 909 | d.image.Config.Entrypoint = args 910 | if !d.cmdSet { 911 | d.image.Config.Cmd = nil 912 | } 913 | return commitToHistory(&d.image, fmt.Sprintf("ENTRYPOINT %q", args), false, nil) 914 | } 915 | 916 | func dispatchHealthcheck(d *dispatchState, c *instructions.HealthCheckCommand) error { 917 | d.image.Config.Healthcheck = &HealthConfig{ 918 | Test: c.Health.Test, 919 | Interval: c.Health.Interval, 920 | Timeout: c.Health.Timeout, 921 | StartPeriod: c.Health.StartPeriod, 922 | Retries: c.Health.Retries, 923 | } 924 | return commitToHistory(&d.image, fmt.Sprintf("HEALTHCHECK %q", d.image.Config.Healthcheck), false, nil) 925 | } 926 | 927 | func dispatchExpose(d *dispatchState, c *instructions.ExposeCommand, shlex *shell.Lex) error { 928 | ports := []string{} 929 | for _, p := range c.Ports { 930 | ps, err := shlex.ProcessWordsWithMap(p, toEnvMap(d.buildArgs, d.image.Config.Env)) 931 | if err != nil { 932 | return err 933 | } 934 | ports = append(ports, ps...) 935 | } 936 | c.Ports = ports 937 | 938 | ps, _, err := nat.ParsePortSpecs(c.Ports) 939 | if err != nil { 940 | return err 941 | } 942 | 943 | if d.image.Config.ExposedPorts == nil { 944 | d.image.Config.ExposedPorts = make(map[string]struct{}) 945 | } 946 | for p := range ps { 947 | d.image.Config.ExposedPorts[string(p)] = struct{}{} 948 | } 949 | 950 | return commitToHistory(&d.image, fmt.Sprintf("EXPOSE %v", ps), false, nil) 951 | } 952 | 953 | func dispatchUser(d *dispatchState, c *instructions.UserCommand, commit bool) error { 954 | d.state = d.state.User(c.User) 955 | d.image.Config.User = c.User 956 | if commit { 957 | return commitToHistory(&d.image, fmt.Sprintf("USER %v", c.User), false, nil) 958 | } 959 | return nil 960 | } 961 | 962 | func dispatchVolume(d *dispatchState, c *instructions.VolumeCommand) error { 963 | if d.image.Config.Volumes == nil { 964 | d.image.Config.Volumes = map[string]struct{}{} 965 | } 966 | for _, v := range c.Volumes { 967 | if v == "" { 968 | return errors.New("VOLUME specified can not be an empty string") 969 | } 970 | d.image.Config.Volumes[v] = struct{}{} 971 | } 972 | return commitToHistory(&d.image, fmt.Sprintf("VOLUME %v", c.Volumes), false, nil) 973 | } 974 | 975 | // WARN: wabassembly can not use signal package... 976 | // func dispatchStopSignal(d *dispatchState, c *instructions.StopSignalCommand) error { 977 | // if _, err := signal.ParseSignal(c.Signal); err != nil { 978 | // return err 979 | // } 980 | // d.image.Config.StopSignal = c.Signal 981 | // return commitToHistory(&d.image, fmt.Sprintf("STOPSIGNAL %v", c.Signal), false, nil) 982 | // } 983 | 984 | func dispatchShell(d *dispatchState, c *instructions.ShellCommand) error { 985 | d.image.Config.Shell = c.Shell 986 | return commitToHistory(&d.image, fmt.Sprintf("SHELL %v", c.Shell), false, nil) 987 | } 988 | 989 | func dispatchArg(d *dispatchState, c *instructions.ArgCommand, metaArgs []instructions.KeyValuePairOptional, buildArgValues map[string]string) error { 990 | commitStr := "ARG " + c.Key 991 | buildArg := setKVValue(c.KeyValuePairOptional, buildArgValues) 992 | 993 | if c.Value != nil { 994 | commitStr += "=" + *c.Value 995 | } 996 | if buildArg.Value == nil { 997 | for _, ma := range metaArgs { 998 | if ma.Key == buildArg.Key { 999 | buildArg.Value = ma.Value 1000 | } 1001 | } 1002 | } 1003 | 1004 | d.buildArgs = append(d.buildArgs, buildArg) 1005 | return commitToHistory(&d.image, commitStr, false, nil) 1006 | } 1007 | 1008 | func pathRelativeToWorkingDir(s llb.State, p string) string { 1009 | if path.IsAbs(p) { 1010 | return p 1011 | } 1012 | return path.Join(s.GetDir(), p) 1013 | } 1014 | 1015 | func splitWildcards(name string) (string, string) { 1016 | i := 0 1017 | for ; i < len(name); i++ { 1018 | ch := name[i] 1019 | if ch == '\\' { 1020 | i++ 1021 | } else if ch == '*' || ch == '?' || ch == '[' { 1022 | break 1023 | } 1024 | } 1025 | if i == len(name) { 1026 | return name, "" 1027 | } 1028 | 1029 | base := path.Base(name[:i]) 1030 | if name[:i] == "" || strings.HasSuffix(name[:i], string(filepath.Separator)) { 1031 | base = "" 1032 | } 1033 | return path.Dir(name[:i]), base + name[i:] 1034 | } 1035 | 1036 | func addEnv(env []string, k, v string) []string { 1037 | gotOne := false 1038 | for i, envVar := range env { 1039 | key, _ := parseKeyValue(envVar) 1040 | if shell.EqualEnvKeys(key, k) { 1041 | env[i] = k + "=" + v 1042 | gotOne = true 1043 | break 1044 | } 1045 | } 1046 | if !gotOne { 1047 | env = append(env, k+"="+v) 1048 | } 1049 | return env 1050 | } 1051 | 1052 | func parseKeyValue(env string) (string, string) { 1053 | parts := strings.SplitN(env, "=", 2) 1054 | v := "" 1055 | if len(parts) > 1 { 1056 | v = parts[1] 1057 | } 1058 | 1059 | return parts[0], v 1060 | } 1061 | 1062 | func setKVValue(kvpo instructions.KeyValuePairOptional, values map[string]string) instructions.KeyValuePairOptional { 1063 | if v, ok := values[kvpo.Key]; ok { 1064 | kvpo.Value = &v 1065 | } 1066 | return kvpo 1067 | } 1068 | 1069 | func toEnvMap(args []instructions.KeyValuePairOptional, env []string) map[string]string { 1070 | m := shell.BuildEnvs(env) 1071 | 1072 | for _, arg := range args { 1073 | // If key already exists, keep previous value. 1074 | if _, ok := m[arg.Key]; ok { 1075 | continue 1076 | } 1077 | if arg.Value != nil { 1078 | m[arg.Key] = arg.ValueString() 1079 | } 1080 | } 1081 | return m 1082 | } 1083 | 1084 | func dfCmd(cmd interface{}) llb.ConstraintsOpt { 1085 | // TODO: add fmt.Stringer to instructions.Command to remove interface{} 1086 | var cmdStr string 1087 | if cmd, ok := cmd.(fmt.Stringer); ok { 1088 | cmdStr = cmd.String() 1089 | } 1090 | if cmd, ok := cmd.(string); ok { 1091 | cmdStr = cmd 1092 | } 1093 | return llb.WithDescription(map[string]string{ 1094 | "com.docker.dockerfile.v1.command": cmdStr, 1095 | }) 1096 | } 1097 | 1098 | func runCommandString(args []string, buildArgs []instructions.KeyValuePairOptional) string { 1099 | var tmpBuildEnv []string 1100 | for _, arg := range buildArgs { 1101 | tmpBuildEnv = append(tmpBuildEnv, arg.Key+"="+arg.ValueString()) 1102 | } 1103 | if len(tmpBuildEnv) > 0 { 1104 | tmpBuildEnv = append([]string{fmt.Sprintf("|%d", len(tmpBuildEnv))}, tmpBuildEnv...) 1105 | } 1106 | 1107 | return strings.Join(append(tmpBuildEnv, args...), " ") 1108 | } 1109 | 1110 | func commitToHistory(img *Image, msg string, withLayer bool, st *llb.State) error { 1111 | if st != nil { 1112 | msg += " # buildkit" 1113 | } 1114 | 1115 | img.History = append(img.History, specs.History{ 1116 | CreatedBy: msg, 1117 | Comment: historyComment, 1118 | EmptyLayer: !withLayer, 1119 | }) 1120 | return nil 1121 | } 1122 | 1123 | func isReachable(from, to *dispatchState) (ret bool) { 1124 | if from == nil { 1125 | return false 1126 | } 1127 | if from == to || isReachable(from.base, to) { 1128 | return true 1129 | } 1130 | for d := range from.deps { 1131 | if isReachable(d, to) { 1132 | return true 1133 | } 1134 | } 1135 | return false 1136 | } 1137 | 1138 | func hasCircularDependency(states []*dispatchState) (bool, *dispatchState) { 1139 | var visit func(state *dispatchState) bool 1140 | if states == nil { 1141 | return false, nil 1142 | } 1143 | visited := make(map[*dispatchState]struct{}) 1144 | path := make(map[*dispatchState]struct{}) 1145 | 1146 | visit = func(state *dispatchState) bool { 1147 | _, ok := visited[state] 1148 | if ok { 1149 | return false 1150 | } 1151 | visited[state] = struct{}{} 1152 | path[state] = struct{}{} 1153 | for dep := range state.deps { 1154 | _, ok = path[dep] 1155 | if ok { 1156 | return true 1157 | } 1158 | if visit(dep) { 1159 | return true 1160 | } 1161 | } 1162 | delete(path, state) 1163 | return false 1164 | } 1165 | for _, state := range states { 1166 | if visit(state) { 1167 | return true, state 1168 | } 1169 | } 1170 | return false, nil 1171 | } 1172 | 1173 | func parseUser(str string) (uid uint32, gid uint32, err error) { 1174 | if str == "" { 1175 | return 0, 0, nil 1176 | } 1177 | parts := strings.SplitN(str, ":", 2) 1178 | for i, v := range parts { 1179 | switch i { 1180 | case 0: 1181 | uid, err = parseUID(v) 1182 | if err != nil { 1183 | return 0, 0, err 1184 | } 1185 | if len(parts) == 1 { 1186 | gid = uid 1187 | } 1188 | case 1: 1189 | gid, err = parseUID(v) 1190 | if err != nil { 1191 | return 0, 0, err 1192 | } 1193 | } 1194 | } 1195 | return 1196 | } 1197 | 1198 | func parseUID(str string) (uint32, error) { 1199 | if str == "root" { 1200 | return 0, nil 1201 | } 1202 | uid, err := strconv.ParseUint(str, 10, 32) 1203 | if err != nil { 1204 | return 0, err 1205 | } 1206 | return uint32(uid), nil 1207 | } 1208 | 1209 | func normalizeContextPaths(paths map[string]struct{}) []string { 1210 | pathSlice := make([]string, 0, len(paths)) 1211 | for p := range paths { 1212 | if p == "/" { 1213 | return nil 1214 | } 1215 | pathSlice = append(pathSlice, p) 1216 | } 1217 | 1218 | toDelete := map[string]struct{}{} 1219 | for i := range pathSlice { 1220 | for j := range pathSlice { 1221 | if i == j { 1222 | continue 1223 | } 1224 | if strings.HasPrefix(pathSlice[j], pathSlice[i]+"/") { 1225 | delete(paths, pathSlice[j]) 1226 | } 1227 | } 1228 | } 1229 | 1230 | toSort := make([]string, 0, len(paths)) 1231 | for p := range paths { 1232 | if _, ok := toDelete[p]; !ok { 1233 | toSort = append(toSort, path.Join(".", p)) 1234 | } 1235 | } 1236 | sort.Slice(toSort, func(i, j int) bool { 1237 | return toSort[i] < toSort[j] 1238 | }) 1239 | return toSort 1240 | } 1241 | 1242 | func proxyEnvFromBuildArgs(args map[string]string) *llb.ProxyEnv { 1243 | pe := &llb.ProxyEnv{} 1244 | isNil := true 1245 | for k, v := range args { 1246 | if strings.EqualFold(k, "http_proxy") { 1247 | pe.HttpProxy = v 1248 | isNil = false 1249 | } 1250 | if strings.EqualFold(k, "https_proxy") { 1251 | pe.HttpsProxy = v 1252 | isNil = false 1253 | } 1254 | if strings.EqualFold(k, "ftp_proxy") { 1255 | pe.FtpProxy = v 1256 | isNil = false 1257 | } 1258 | if strings.EqualFold(k, "no_proxy") { 1259 | pe.NoProxy = v 1260 | isNil = false 1261 | } 1262 | } 1263 | if isNil { 1264 | return nil 1265 | } 1266 | return pe 1267 | } 1268 | 1269 | type mutableOutput struct { 1270 | llb.Output 1271 | } 1272 | 1273 | func withShell(img Image, args []string) []string { 1274 | var shell []string 1275 | if len(img.Config.Shell) > 0 { 1276 | shell = append([]string{}, img.Config.Shell...) 1277 | } else { 1278 | shell = defaultShell() 1279 | } 1280 | return append(shell, strings.Join(args, " ")) 1281 | } 1282 | 1283 | func autoDetectPlatform(img Image, target specs.Platform, supported []specs.Platform) specs.Platform { 1284 | os := img.OS 1285 | arch := img.Architecture 1286 | if target.OS == os && target.Architecture == arch { 1287 | return target 1288 | } 1289 | for _, p := range supported { 1290 | if p.OS == os && p.Architecture == arch { 1291 | return p 1292 | } 1293 | } 1294 | return target 1295 | } 1296 | 1297 | func WithInternalName(name string) llb.ConstraintsOpt { 1298 | return llb.WithCustomName("[internal] " + name) 1299 | } 1300 | 1301 | func uppercaseCmd(str string) string { 1302 | p := strings.SplitN(str, " ", 2) 1303 | p[0] = strings.ToUpper(p[0]) 1304 | return strings.Join(p, " ") 1305 | } 1306 | 1307 | func processCmdEnv(shlex *shell.Lex, cmd string, env []string) string { 1308 | w, err := shlex.ProcessWord(cmd, env) 1309 | if err != nil { 1310 | return cmd 1311 | } 1312 | return w 1313 | } 1314 | 1315 | func prefixCommand(ds *dispatchState, str string, prefixPlatform bool, platform *specs.Platform) string { 1316 | if ds.cmdTotal == 0 { 1317 | return str 1318 | } 1319 | out := "[" 1320 | if prefixPlatform && platform != nil { 1321 | out += platforms.Format(*platform) + " " 1322 | } 1323 | if ds.stageName != "" { 1324 | out += ds.stageName + " " 1325 | } 1326 | ds.cmdIndex++ 1327 | out += fmt.Sprintf("%d/%d] ", ds.cmdIndex, ds.cmdTotal) 1328 | return out + str 1329 | } 1330 | 1331 | func useFileOp(args map[string]string, caps *apicaps.CapSet) bool { 1332 | enabled := true 1333 | if v, ok := args["BUILDKIT_DISABLE_FILEOP"]; ok { 1334 | if b, err := strconv.ParseBool(v); err == nil { 1335 | enabled = !b 1336 | } 1337 | } 1338 | return enabled && caps != nil && caps.Supports(pb.CapFileBase) == nil 1339 | } 1340 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_norunmount.go: -------------------------------------------------------------------------------- 1 | // +build !dfrunmount 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/moby/buildkit/client/llb" 7 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 8 | ) 9 | 10 | func detectRunMount(cmd *command, allDispatchStates *dispatchStates) bool { 11 | return false 12 | } 13 | 14 | func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*dispatchState, opt dispatchOpt) ([]llb.RunOption, error) { 15 | return nil, nil 16 | } 17 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_nosecrets.go: -------------------------------------------------------------------------------- 1 | // +build dfrunmount,!dfsecrets 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/moby/buildkit/client/llb" 7 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func dispatchSecret(m *instructions.Mount) (llb.RunOption, error) { 12 | return nil, errors.Errorf("secret mounts not allowed") 13 | } 14 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_nossh.go: -------------------------------------------------------------------------------- 1 | // +build dfrunmount,!dfssh 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/moby/buildkit/client/llb" 7 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func dispatchSSH(m *instructions.Mount) (llb.RunOption, error) { 12 | return nil, errors.Errorf("ssh mounts not allowed") 13 | } 14 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_runmount.go: -------------------------------------------------------------------------------- 1 | // +build dfrunmount 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "path" 7 | "path/filepath" 8 | 9 | "github.com/moby/buildkit/client/llb" 10 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func detectRunMount(cmd *command, allDispatchStates *dispatchStates) bool { 15 | if c, ok := cmd.Command.(*instructions.RunCommand); ok { 16 | mounts := instructions.GetMounts(c) 17 | sources := make([]*dispatchState, len(mounts)) 18 | for i, mount := range mounts { 19 | if mount.From == "" && mount.Type == instructions.MountTypeCache { 20 | mount.From = emptyImageName 21 | } 22 | from := mount.From 23 | if from == "" || mount.Type == instructions.MountTypeTmpfs { 24 | continue 25 | } 26 | stn, ok := allDispatchStates.findStateByName(from) 27 | if !ok { 28 | stn = &dispatchState{ 29 | stage: instructions.Stage{BaseName: from}, 30 | deps: make(map[*dispatchState]struct{}), 31 | unregistered: true, 32 | } 33 | } 34 | sources[i] = stn 35 | } 36 | cmd.sources = sources 37 | return true 38 | } 39 | 40 | return false 41 | } 42 | 43 | func dispatchRunMounts(d *dispatchState, c *instructions.RunCommand, sources []*dispatchState, opt dispatchOpt) ([]llb.RunOption, error) { 44 | var out []llb.RunOption 45 | mounts := instructions.GetMounts(c) 46 | 47 | for i, mount := range mounts { 48 | if mount.From == "" && mount.Type == instructions.MountTypeCache { 49 | mount.From = emptyImageName 50 | } 51 | st := opt.buildContext 52 | if mount.From != "" { 53 | st = sources[i].state 54 | } 55 | var mountOpts []llb.MountOption 56 | if mount.Type == instructions.MountTypeTmpfs { 57 | st = llb.Scratch() 58 | mountOpts = append(mountOpts, llb.Tmpfs()) 59 | } 60 | if mount.Type == instructions.MountTypeSecret { 61 | secret, err := dispatchSecret(mount) 62 | if err != nil { 63 | return nil, err 64 | } 65 | out = append(out, secret) 66 | continue 67 | } 68 | if mount.Type == instructions.MountTypeSSH { 69 | ssh, err := dispatchSSH(mount) 70 | if err != nil { 71 | return nil, err 72 | } 73 | out = append(out, ssh) 74 | continue 75 | } 76 | if mount.ReadOnly { 77 | mountOpts = append(mountOpts, llb.Readonly) 78 | } else if mount.Type == instructions.MountTypeBind { 79 | mountOpts = append(mountOpts, llb.ForceNoOutput) 80 | } 81 | if mount.Type == instructions.MountTypeCache { 82 | sharing := llb.CacheMountShared 83 | if mount.CacheSharing == instructions.MountSharingPrivate { 84 | sharing = llb.CacheMountPrivate 85 | } 86 | if mount.CacheSharing == instructions.MountSharingLocked { 87 | sharing = llb.CacheMountLocked 88 | } 89 | mountOpts = append(mountOpts, llb.AsPersistentCacheDir(opt.cacheIDNamespace+"/"+mount.CacheID, sharing)) 90 | } 91 | target := mount.Target 92 | if !filepath.IsAbs(filepath.Clean(mount.Target)) { 93 | target = filepath.Join("/", d.state.GetDir(), mount.Target) 94 | } 95 | if target == "/" { 96 | return nil, errors.Errorf("invalid mount target %q", target) 97 | } 98 | if src := path.Join("/", mount.Source); src != "/" { 99 | mountOpts = append(mountOpts, llb.SourcePath(src)) 100 | } 101 | out = append(out, llb.AddMount(target, st, mountOpts...)) 102 | 103 | d.ctxPaths[path.Join("/", filepath.ToSlash(mount.Source))] = struct{}{} 104 | } 105 | return out, nil 106 | } 107 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_secrets.go: -------------------------------------------------------------------------------- 1 | // +build dfsecrets 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "path" 7 | 8 | "github.com/moby/buildkit/client/llb" 9 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func dispatchSecret(m *instructions.Mount) (llb.RunOption, error) { 14 | id := m.CacheID 15 | if m.Source != "" { 16 | id = m.Source 17 | } 18 | 19 | if id == "" { 20 | if m.Target == "" { 21 | return nil, errors.Errorf("one of source, target required") 22 | } 23 | id = path.Base(m.Target) 24 | } 25 | 26 | target := m.Target 27 | if target == "" { 28 | target = "/run/secrets/" + path.Base(id) 29 | } 30 | 31 | opts := []llb.SecretOption{llb.SecretID(id)} 32 | 33 | if !m.Required { 34 | opts = append(opts, llb.SecretOptional) 35 | } 36 | 37 | if m.UID != nil || m.GID != nil || m.Mode != nil { 38 | var uid, gid, mode int 39 | if m.UID != nil { 40 | uid = int(*m.UID) 41 | } 42 | if m.GID != nil { 43 | gid = int(*m.GID) 44 | } 45 | if m.Mode != nil { 46 | mode = int(*m.Mode) 47 | } else { 48 | mode = 0400 49 | } 50 | opts = append(opts, llb.SecretFileOpt(uid, gid, mode)) 51 | } 52 | 53 | return llb.AddSecret(target, opts...), nil 54 | } 55 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_ssh.go: -------------------------------------------------------------------------------- 1 | // +build dfssh 2 | 3 | package dockerfile2llb 4 | 5 | import ( 6 | "github.com/moby/buildkit/client/llb" 7 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func dispatchSSH(m *instructions.Mount) (llb.RunOption, error) { 12 | if m.Source != "" { 13 | return nil, errors.Errorf("ssh does not support source") 14 | } 15 | opts := []llb.SSHOption{llb.SSHID(m.CacheID)} 16 | 17 | if m.Target != "" { 18 | opts = append(opts, llb.SSHSocketTarget(m.Target)) 19 | } 20 | 21 | if !m.Required { 22 | opts = append(opts, llb.SSHOptional) 23 | } 24 | 25 | if m.UID != nil || m.GID != nil || m.Mode != nil { 26 | var uid, gid, mode int 27 | if m.UID != nil { 28 | uid = int(*m.UID) 29 | } 30 | if m.GID != nil { 31 | gid = int(*m.GID) 32 | } 33 | if m.Mode != nil { 34 | mode = int(*m.Mode) 35 | } else { 36 | mode = 0600 37 | } 38 | opts = append(opts, llb.SSHSocketOpt(m.Target, uid, gid, mode)) 39 | } 40 | 41 | return llb.AddSSHSocket(opts...), nil 42 | } 43 | -------------------------------------------------------------------------------- /dockerfile2llb/convert_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 7 | "github.com/moby/buildkit/util/appcontext" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDockerfileParsing(t *testing.T) { 12 | t.Parallel() 13 | df := `FROM busybox 14 | ENV FOO bar 15 | COPY f1 f2 /sub/ 16 | RUN ls -l 17 | ` 18 | _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 19 | assert.NoError(t, err) 20 | 21 | df = `FROM busybox AS foo 22 | ENV FOO bar 23 | FROM foo 24 | COPY --from=foo f1 / 25 | COPY --from=0 f2 / 26 | ` 27 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 28 | assert.NoError(t, err) 29 | 30 | df = `FROM busybox AS foo 31 | ENV FOO bar 32 | FROM foo 33 | COPY --from=foo f1 / 34 | COPY --from=0 f2 / 35 | ` 36 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ 37 | Target: "Foo", 38 | }) 39 | assert.NoError(t, err) 40 | 41 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{ 42 | Target: "nosuch", 43 | }) 44 | assert.Error(t, err) 45 | 46 | df = `FROM busybox 47 | ADD http://github.com/moby/buildkit/blob/master/README.md / 48 | ` 49 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 50 | assert.NoError(t, err) 51 | 52 | df = `FROM busybox 53 | COPY http://github.com/moby/buildkit/blob/master/README.md / 54 | ` 55 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 56 | assert.EqualError(t, err, "source can't be a URL for COPY") 57 | 58 | df = `FROM "" AS foo` 59 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 60 | assert.Error(t, err) 61 | 62 | df = `FROM ${BLANK} AS foo` 63 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 64 | assert.Error(t, err) 65 | } 66 | 67 | func TestAddEnv(t *testing.T) { 68 | // k exists in env as key 69 | // override = true 70 | env := []string{"key1=val1", "key2=val2"} 71 | result := addEnv(env, "key1", "value1") 72 | assert.Equal(t, []string{"key1=value1", "key2=val2"}, result) 73 | 74 | // k does not exist in env as key 75 | // override = true 76 | env = []string{"key1=val1", "key2=val2"} 77 | result = addEnv(env, "key3", "val3") 78 | assert.Equal(t, []string{"key1=val1", "key2=val2", "key3=val3"}, result) 79 | 80 | // env has same keys 81 | // override = true 82 | env = []string{"key1=val1", "key1=val2"} 83 | result = addEnv(env, "key1", "value1") 84 | assert.Equal(t, []string{"key1=value1", "key1=val2"}, result) 85 | 86 | // k matches with key only string in env 87 | // override = true 88 | env = []string{"key1=val1", "key2=val2", "key3"} 89 | result = addEnv(env, "key3", "val3") 90 | assert.Equal(t, []string{"key1=val1", "key2=val2", "key3=val3"}, result) 91 | } 92 | 93 | func TestParseKeyValue(t *testing.T) { 94 | k, v := parseKeyValue("key=val") 95 | assert.Equal(t, "key", k) 96 | assert.Equal(t, "val", v) 97 | 98 | k, v = parseKeyValue("key=") 99 | assert.Equal(t, "key", k) 100 | assert.Equal(t, "", v) 101 | 102 | k, v = parseKeyValue("key") 103 | assert.Equal(t, "key", k) 104 | assert.Equal(t, "", v) 105 | } 106 | 107 | func TestToEnvList(t *testing.T) { 108 | // args has no duplicated key with env 109 | v := "val2" 110 | args := []instructions.KeyValuePairOptional{{Key: "key2", Value: &v}} 111 | env := []string{"key1=val1"} 112 | resutl := toEnvMap(args, env) 113 | assert.Equal(t, map[string]string{"key1": "val1", "key2": "val2"}, resutl) 114 | 115 | // value of args is nil 116 | args = []instructions.KeyValuePairOptional{{Key: "key2", Value: nil}} 117 | env = []string{"key1=val1"} 118 | resutl = toEnvMap(args, env) 119 | assert.Equal(t, map[string]string{"key1": "val1"}, resutl) 120 | 121 | // args has duplicated key with env 122 | v = "val2" 123 | args = []instructions.KeyValuePairOptional{{Key: "key1", Value: &v}} 124 | env = []string{"key1=val1"} 125 | resutl = toEnvMap(args, env) 126 | assert.Equal(t, map[string]string{"key1": "val1"}, resutl) 127 | 128 | v = "val2" 129 | args = []instructions.KeyValuePairOptional{{Key: "key1", Value: &v}} 130 | env = []string{"key1="} 131 | resutl = toEnvMap(args, env) 132 | assert.Equal(t, map[string]string{"key1": ""}, resutl) 133 | 134 | v = "val2" 135 | args = []instructions.KeyValuePairOptional{{Key: "key1", Value: &v}} 136 | env = []string{"key1"} 137 | resutl = toEnvMap(args, env) 138 | assert.Equal(t, map[string]string{"key1": ""}, resutl) 139 | 140 | // env has duplicated keys 141 | v = "val2" 142 | args = []instructions.KeyValuePairOptional{{Key: "key2", Value: &v}} 143 | env = []string{"key1=val1", "key1=val1_2"} 144 | resutl = toEnvMap(args, env) 145 | assert.Equal(t, map[string]string{"key1": "val1", "key2": "val2"}, resutl) 146 | 147 | // args has duplicated keys 148 | v1 := "v1" 149 | v2 := "v2" 150 | args = []instructions.KeyValuePairOptional{{Key: "key2", Value: &v1}, {Key: "key2", Value: &v2}} 151 | env = []string{"key1=val1"} 152 | resutl = toEnvMap(args, env) 153 | assert.Equal(t, map[string]string{"key1": "val1", "key2": "v1"}, resutl) 154 | } 155 | 156 | func TestDockerfileCircularDependencies(t *testing.T) { 157 | // single stage depends on itself 158 | df := `FROM busybox AS stage0 159 | COPY --from=stage0 f1 /sub/ 160 | ` 161 | _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 162 | assert.EqualError(t, err, "circular dependency detected on stage: stage0") 163 | 164 | // multiple stages with circular dependency 165 | df = `FROM busybox AS stage0 166 | COPY --from=stage2 f1 /sub/ 167 | FROM busybox AS stage1 168 | COPY --from=stage0 f2 /sub/ 169 | FROM busybox AS stage2 170 | COPY --from=stage1 f2 /sub/ 171 | ` 172 | _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{}) 173 | assert.EqualError(t, err, "circular dependency detected on stage: stage0") 174 | } 175 | -------------------------------------------------------------------------------- /dockerfile2llb/defaultshell_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package dockerfile2llb 4 | 5 | func defaultShell() []string { 6 | return []string{"/bin/sh", "-c"} 7 | } 8 | -------------------------------------------------------------------------------- /dockerfile2llb/defaultshell_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package dockerfile2llb 4 | 5 | func defaultShell() []string { 6 | return []string{"cmd", "/S", "/C"} 7 | } 8 | -------------------------------------------------------------------------------- /dockerfile2llb/directives.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | const keySyntax = "syntax" 11 | 12 | var reDirective = regexp.MustCompile(`^#\s*([a-zA-Z][a-zA-Z0-9]*)\s*=\s*(.+?)\s*$`) 13 | 14 | func DetectSyntax(r io.Reader) (string, string, bool) { 15 | directives := ParseDirectives(r) 16 | if len(directives) == 0 { 17 | return "", "", false 18 | } 19 | v, ok := directives[keySyntax] 20 | if !ok { 21 | return "", "", false 22 | } 23 | p := strings.SplitN(v, " ", 2) 24 | return p[0], v, true 25 | } 26 | 27 | func ParseDirectives(r io.Reader) map[string]string { 28 | m := map[string]string{} 29 | s := bufio.NewScanner(r) 30 | for s.Scan() { 31 | match := reDirective.FindStringSubmatch(s.Text()) 32 | if len(match) == 0 { 33 | return m 34 | } 35 | m[strings.ToLower(match[1])] = match[2] 36 | } 37 | return m 38 | } 39 | -------------------------------------------------------------------------------- /dockerfile2llb/directives_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDirectives(t *testing.T) { 12 | t.Parallel() 13 | 14 | dt := `#escape=\ 15 | # key = FOO bar 16 | 17 | # smth 18 | ` 19 | 20 | d := ParseDirectives(bytes.NewBuffer([]byte(dt))) 21 | require.Equal(t, len(d), 2, fmt.Sprintf("%+v", d)) 22 | 23 | v, ok := d["escape"] 24 | require.True(t, ok) 25 | require.Equal(t, v, "\\") 26 | 27 | v, ok = d["key"] 28 | require.True(t, ok) 29 | require.Equal(t, v, "FOO bar") 30 | 31 | // for some reason Moby implementation in case insensitive for escape 32 | dt = `# EScape=\ 33 | # KEY = FOO bar 34 | 35 | # smth 36 | ` 37 | 38 | d = ParseDirectives(bytes.NewBuffer([]byte(dt))) 39 | require.Equal(t, len(d), 2, fmt.Sprintf("%+v", d)) 40 | 41 | v, ok = d["escape"] 42 | require.True(t, ok) 43 | require.Equal(t, v, "\\") 44 | 45 | v, ok = d["key"] 46 | require.True(t, ok) 47 | require.Equal(t, v, "FOO bar") 48 | } 49 | 50 | func TestSyntaxDirective(t *testing.T) { 51 | t.Parallel() 52 | 53 | dt := `# syntax = dockerfile:experimental // opts 54 | FROM busybox 55 | ` 56 | 57 | ref, cmdline, ok := DetectSyntax(bytes.NewBuffer([]byte(dt))) 58 | require.True(t, ok) 59 | require.Equal(t, ref, "dockerfile:experimental") 60 | require.Equal(t, cmdline, "dockerfile:experimental // opts") 61 | 62 | dt = `FROM busybox 63 | RUN ls 64 | ` 65 | ref, cmdline, ok = DetectSyntax(bytes.NewBuffer([]byte(dt))) 66 | require.False(t, ok) 67 | require.Equal(t, ref, "") 68 | require.Equal(t, cmdline, "") 69 | 70 | } 71 | -------------------------------------------------------------------------------- /dockerfile2llb/image.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/docker/docker/api/types/strslice" 7 | "github.com/moby/buildkit/util/system" 8 | specs "github.com/opencontainers/image-spec/specs-go/v1" 9 | ) 10 | 11 | // HealthConfig holds configuration settings for the HEALTHCHECK feature. 12 | type HealthConfig struct { 13 | // Test is the test to perform to check that the container is healthy. 14 | // An empty slice means to inherit the default. 15 | // The options are: 16 | // {} : inherit healthcheck 17 | // {"NONE"} : disable healthcheck 18 | // {"CMD", args...} : exec arguments directly 19 | // {"CMD-SHELL", command} : run command with system's default shell 20 | Test []string `json:",omitempty"` 21 | 22 | // Zero means to inherit. Durations are expressed as integer nanoseconds. 23 | Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. 24 | Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. 25 | StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down. 26 | 27 | // Retries is the number of consecutive failures needed to consider a container as unhealthy. 28 | // Zero means inherit. 29 | Retries int `json:",omitempty"` 30 | } 31 | 32 | // ImageConfig is a docker compatible config for an image 33 | type ImageConfig struct { 34 | specs.ImageConfig 35 | 36 | Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy 37 | ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific) 38 | 39 | // NetworkDisabled bool `json:",omitempty"` // Is network disabled 40 | // MacAddress string `json:",omitempty"` // Mac Address of the container 41 | OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile 42 | StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container 43 | Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT 44 | } 45 | 46 | // Image is the JSON structure which describes some basic information about the image. 47 | // This provides the `application/vnd.oci.image.config.v1+json` mediatype when marshalled to JSON. 48 | type Image struct { 49 | specs.Image 50 | 51 | // Config defines the execution parameters which should be used as a base when running a container using the image. 52 | Config ImageConfig `json:"config,omitempty"` 53 | 54 | // Variant defines platform variant. To be added to OCI. 55 | Variant string `json:"variant,omitempty"` 56 | } 57 | 58 | func clone(src Image) Image { 59 | img := src 60 | img.Config = src.Config 61 | img.Config.Env = append([]string{}, src.Config.Env...) 62 | img.Config.Cmd = append([]string{}, src.Config.Cmd...) 63 | img.Config.Entrypoint = append([]string{}, src.Config.Entrypoint...) 64 | return img 65 | } 66 | 67 | func emptyImage(platform specs.Platform) Image { 68 | img := Image{ 69 | Image: specs.Image{ 70 | Architecture: platform.Architecture, 71 | OS: platform.OS, 72 | }, 73 | Variant: platform.Variant, 74 | } 75 | img.RootFS.Type = "layers" 76 | img.Config.WorkingDir = "/" 77 | img.Config.Env = []string{"PATH=" + system.DefaultPathEnv} 78 | return img 79 | } 80 | -------------------------------------------------------------------------------- /dockerfile2llb/platform.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "github.com/containerd/containerd/platforms" 5 | "github.com/moby/buildkit/frontend/dockerfile/instructions" 6 | specs "github.com/opencontainers/image-spec/specs-go/v1" 7 | ) 8 | 9 | type platformOpt struct { 10 | targetPlatform specs.Platform 11 | buildPlatforms []specs.Platform 12 | implicitTarget bool 13 | } 14 | 15 | func buildPlatformOpt(opt *ConvertOpt) *platformOpt { 16 | buildPlatforms := opt.BuildPlatforms 17 | targetPlatform := opt.TargetPlatform 18 | implicitTargetPlatform := false 19 | 20 | if opt.TargetPlatform != nil && opt.BuildPlatforms == nil { 21 | buildPlatforms = []specs.Platform{*opt.TargetPlatform} 22 | } 23 | if len(buildPlatforms) == 0 { 24 | buildPlatforms = []specs.Platform{platforms.DefaultSpec()} 25 | } 26 | 27 | if opt.TargetPlatform == nil { 28 | implicitTargetPlatform = true 29 | targetPlatform = &buildPlatforms[0] 30 | } 31 | 32 | return &platformOpt{ 33 | targetPlatform: *targetPlatform, 34 | buildPlatforms: buildPlatforms, 35 | implicitTarget: implicitTargetPlatform, 36 | } 37 | } 38 | 39 | func getPlatformArgs(po *platformOpt) []instructions.KeyValuePairOptional { 40 | bp := po.buildPlatforms[0] 41 | tp := po.targetPlatform 42 | m := map[string]string{ 43 | "BUILDPLATFORM": platforms.Format(bp), 44 | "BUILDOS": bp.OS, 45 | "BUILDARCH": bp.Architecture, 46 | "BUILDVARIANT": bp.Variant, 47 | "TARGETPLATFORM": platforms.Format(tp), 48 | "TARGETOS": tp.OS, 49 | "TARGETARCH": tp.Architecture, 50 | "TARGETVARIANT": tp.Variant, 51 | } 52 | opts := make([]instructions.KeyValuePairOptional, 0, len(m)) 53 | for k, v := range m { 54 | s := v 55 | opts = append(opts, instructions.KeyValuePairOptional{Key: k, Value: &s}) 56 | } 57 | return opts 58 | } 59 | -------------------------------------------------------------------------------- /dockerfile2llb/platform_test.go: -------------------------------------------------------------------------------- 1 | package dockerfile2llb 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/containerd/containerd/platforms" 7 | specs "github.com/opencontainers/image-spec/specs-go/v1" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestResolveBuildPlatforms(t *testing.T) { 12 | dummyPlatform1 := specs.Platform{Architecture: "DummyArchitecture1", OS: "DummyOS1"} 13 | dummyPlatform2 := specs.Platform{Architecture: "DummyArchitecture2", OS: "DummyOS2"} 14 | 15 | // BuildPlatforms is set and TargetPlatform is set 16 | opt := ConvertOpt{TargetPlatform: &dummyPlatform1, BuildPlatforms: []specs.Platform{dummyPlatform2}} 17 | result := buildPlatformOpt(&opt).buildPlatforms 18 | assert.Equal(t, []specs.Platform{dummyPlatform2}, result) 19 | 20 | // BuildPlatforms is not set and TargetPlatform is set 21 | opt = ConvertOpt{TargetPlatform: &dummyPlatform1, BuildPlatforms: nil} 22 | result = buildPlatformOpt(&opt).buildPlatforms 23 | assert.Equal(t, []specs.Platform{dummyPlatform1}, result) 24 | 25 | // BuildPlatforms is set and TargetPlatform is not set 26 | opt = ConvertOpt{TargetPlatform: nil, BuildPlatforms: []specs.Platform{dummyPlatform2}} 27 | result = buildPlatformOpt(&opt).buildPlatforms 28 | assert.Equal(t, []specs.Platform{dummyPlatform2}, result) 29 | 30 | // BuildPlatforms is not set and TargetPlatform is not set 31 | opt = ConvertOpt{TargetPlatform: nil, BuildPlatforms: nil} 32 | result = buildPlatformOpt(&opt).buildPlatforms 33 | assert.Equal(t, []specs.Platform{platforms.DefaultSpec()}, result) 34 | } 35 | 36 | func TestResolveTargetPlatform(t *testing.T) { 37 | dummyPlatform := specs.Platform{Architecture: "DummyArchitecture", OS: "DummyOS"} 38 | 39 | // TargetPlatform is set 40 | opt := ConvertOpt{TargetPlatform: &dummyPlatform} 41 | result := buildPlatformOpt(&opt) 42 | assert.Equal(t, dummyPlatform, result.targetPlatform) 43 | 44 | // TargetPlatform is not set 45 | opt = ConvertOpt{TargetPlatform: nil} 46 | result = buildPlatformOpt(&opt) 47 | assert.Equal(t, result.buildPlatforms[0], result.targetPlatform) 48 | } 49 | 50 | func TestImplicitTargetPlatform(t *testing.T) { 51 | dummyPlatform := specs.Platform{Architecture: "DummyArchitecture", OS: "DummyOS"} 52 | 53 | // TargetPlatform is set 54 | opt := ConvertOpt{TargetPlatform: &dummyPlatform} 55 | result := buildPlatformOpt(&opt).implicitTarget 56 | assert.Equal(t, false, result) 57 | 58 | // TargetPlatform is not set 59 | opt = ConvertOpt{TargetPlatform: nil} 60 | result = buildPlatformOpt(&opt).implicitTarget 61 | assert.Equal(t, true, result) 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/po3rin/dockerdot 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/containerd/containerd v1.3.0-0.20190426060238-3a3f0aac8819 7 | github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible 8 | github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c 9 | github.com/docker/go-connections v0.3.0 10 | github.com/moby/buildkit v0.5.1 11 | github.com/opencontainers/go-digest v1.0.0-rc1 12 | github.com/opencontainers/image-spec v1.0.1 13 | github.com/pkg/errors v0.8.1 14 | github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect 15 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 // indirect 16 | github.com/stretchr/testify v1.3.0 17 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f 18 | golang.org/x/sys v0.0.0-20190614160838-b47fdc937951 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 3 | github.com/Microsoft/go-winio v0.4.13-0.20190408173621-84b4ab48a507/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 4 | github.com/Microsoft/hcsshim v0.8.5/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= 5 | github.com/apache/thrift v0.0.0-20161221203622-b2a4d4ae21c7/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 6 | github.com/codahale/hdrhistogram v0.0.0-20160425231609-f8ad88b59a58/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 7 | github.com/containerd/cgroups v0.0.0-20190226200435-dbea6f2bd416/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= 8 | github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= 9 | github.com/containerd/containerd v1.2.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 10 | github.com/containerd/containerd v1.3.0-0.20190426060238-3a3f0aac8819 h1:otmq8xNIzAo+2SjPURbYZXVW+B6hZBAWJ+JApzCYWDk= 11 | github.com/containerd/containerd v1.3.0-0.20190426060238-3a3f0aac8819/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= 12 | github.com/containerd/continuity v0.0.0-20181001140422-bd77b46c8352/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 13 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= 14 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 15 | github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= 16 | github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= 17 | github.com/containerd/ttrpc v0.0.0-20190411181408-699c4e40d1e7 h1:SKDlsIhYxNE1LO0xwuOR+3QWj3zRibVQu5jWIMQmOfU= 18 | github.com/containerd/ttrpc v0.0.0-20190411181408-699c4e40d1e7/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= 19 | github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd h1:JNn81o/xG+8NEo3bC/vx9pbi/g2WI8mtP2/nXzu297Y= 20 | github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= 21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 24 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/docker/cli v0.0.0-20190321234815-f40f9c240ab0/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 26 | github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible h1:dvc1KSkIYTVjZgHf/CTC2diTYC8PzhaA5sFISRfNVrE= 27 | github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 28 | github.com/docker/docker v0.0.0-20180531152204-71cd53e4a197/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 29 | github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c h1:rZ+3jNsgjvYgdZ0Nrd4Udrv8rneDbWBohAPuXsTsvGU= 30 | github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 31 | github.com/docker/docker-credential-helpers v0.6.0/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= 32 | github.com/docker/go-connections v0.3.0 h1:3lOnM9cSzgGwx8VfK/NGOW5fLQ0GjIlCkaktF+n1M6o= 33 | github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 34 | github.com/docker/go-events v0.0.0-20170721190031-9461782956ad h1:VXIse57M5C6ezDuCPyq6QmMvEJ2xclYKZ35SfkXdm3E= 35 | github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 36 | github.com/docker/go-units v0.3.1 h1:QAFdsA6jLCnglbqE6mUsHuPcJlntY94DkxHf4deHKIU= 37 | github.com/docker/go-units v0.3.1/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 38 | github.com/docker/libnetwork v0.0.0-20180913200009-36d3bed0e9f4/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= 39 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 40 | github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= 41 | github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 42 | github.com/gogo/googleapis v0.0.0-20180501115203-b23578765ee5/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 43 | github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 44 | github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= 45 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 46 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 47 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 48 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 49 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 50 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 51 | github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 h1:JM174NTeGNJ2m/oLH3UOWOvWQQKd+BoL3hcSCUWFLt0= 52 | github.com/google/shlex v0.0.0-20150127133951-6f45313302b9/go.mod h1:RpwtwJQFrIEPstU94h88MWPXP2ektJZ8cZ0YntAmXiE= 53 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY= 54 | github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= 55 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 56 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 57 | github.com/hashicorp/golang-lru v0.0.0-20160207214719-a0d98a5f2880/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 58 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 59 | github.com/hashicorp/uuid v0.0.0-20160311170451-ebb0a03e909c/go.mod h1:fHzc09UnyJyqyW+bFuq864eh+wC7dj65aXmXLRe5to0= 60 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 61 | github.com/ishidawataru/sctp v0.0.0-20180213033435-07191f837fed/go.mod h1:DM4VvS+hD/kDi1U1QsX2fnZowwBhqD0Dk3bRPKF/Oc8= 62 | github.com/jaguilar/vt100 v0.0.0-20150826170717-2703a27b14ea/go.mod h1:QMdK4dGB3YhEW2BmA1wgGpPYI3HZy/5gD705PXKUVSg= 63 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 64 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 65 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 68 | github.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= 69 | github.com/moby/buildkit v0.5.1 h1:a2JHgsMuN/KGafhNK+AxmAIIov9itJTT4z3TyFTSE4c= 70 | github.com/moby/buildkit v0.5.1/go.mod h1:MlzfF7dLLq+tMiE5Dt8qD2iwXvZa1OnwWxMZX/wjBWs= 71 | github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 72 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 73 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 74 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 75 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 76 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 77 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 78 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 79 | github.com/opencontainers/runc v1.0.0-rc6/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 80 | github.com/opencontainers/runc v1.0.1-0.20190307181833-2b18fe1d885e/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 81 | github.com/opencontainers/runtime-spec v0.0.0-20180909173843-eba862dc2470/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 82 | github.com/opentracing-contrib/go-stdlib v0.0.0-20171029140428-b1a47cfbdd75/go.mod h1:PLldrQSroqzH70Xl+1DQcGnefIbqsKR7UDaiux3zV+w= 83 | github.com/opentracing/opentracing-go v0.0.0-20171003133519-1361b9cd60be/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 84 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 85 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 86 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 87 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 88 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 89 | github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= 90 | github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 h1:SpaoQDTgpo2YZkvmr2mtgloFFfPTjtLMlZkQtNAPQik= 91 | github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 92 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= 93 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 94 | github.com/sirupsen/logrus v1.0.3/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 95 | github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= 96 | github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 97 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 98 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 99 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 100 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 101 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 102 | github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 103 | github.com/tonistiigi/fsutil v0.0.0-20190327153851-3bbb99cdbd76 h1:eGfgYrNUSD448sa4mxH6nQpyZfN39QH0mLB7QaKIjus= 104 | github.com/tonistiigi/fsutil v0.0.0-20190327153851-3bbb99cdbd76/go.mod h1:pzh7kdwkDRh+Bx8J30uqaKJ1M4QrSH/um8fcIXeM8rc= 105 | github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= 106 | github.com/uber/jaeger-client-go v0.0.0-20180103221425-e02c85f9069e/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= 107 | github.com/uber/jaeger-lib v1.2.1/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 108 | github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5 h1:MCfT24H3f//U5+UCrZp1/riVO3B50BovxtDiNn0XKkk= 109 | github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 110 | github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= 111 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= 112 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 113 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 114 | golang.org/x/crypto v0.0.0-20190129210102-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 115 | golang.org/x/crypto v0.0.0-20190129210102-ccddf3741a0c h1:MWY7h75sb9ioBR+s5Zgq1JYXxhbZvrSP2okwLi3ItmI= 116 | golang.org/x/crypto v0.0.0-20190129210102-ccddf3741a0c/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 117 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 118 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 119 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= 120 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 123 | golang.org/x/sys v0.0.0-20190303122642-d455e41777fc h1:8EoQ+alqRKjWXD8k4lJE91+f24UIqbKmbOG3yZg82hk= 124 | golang.org/x/sys v0.0.0-20190303122642-d455e41777fc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190614160838-b47fdc937951 h1:ZUgGZ7PSkne6oY+VgAvayrB16owfm9/DKAtgWubzgzU= 126 | golang.org/x/sys v0.0.0-20190614160838-b47fdc937951/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 128 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 129 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 130 | google.golang.org/genproto v0.0.0-20170523043604-d80a6e20e776 h1:wVJP1pATLVPNxCz4R2mTO6HUJgfGE0PmIu2E10RuhCw= 131 | google.golang.org/genproto v0.0.0-20170523043604-d80a6e20e776/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 132 | google.golang.org/grpc v1.12.0 h1:Mm8atZtkT+P6R43n/dqNDWkPPu5BwRVu/1rJnJCeZH8= 133 | google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 134 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 137 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 138 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 139 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 140 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 141 | gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 142 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 143 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 144 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 |
18 |

Dockerfile Dependency graph

19 | 20 |
21 |
22 | 38 |
39 | 40 |
41 | 42 | 54 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin: auto; 3 | font-family: 'Avenir', Helvetica, Arial, sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | text-align: center; 7 | display: flex; 8 | align-items: center; 9 | flex-direction: column; 10 | color: #2c3e50; 11 | } 12 | svg{ 13 | width: 50vw; 14 | } 15 | #graph{ 16 | width: 50vw; 17 | } 18 | #repo{ 19 | width: 46px; 20 | height: 46px; 21 | } 22 | header{ 23 | padding: 12px; 24 | display: flex; 25 | justify-content: space-between; 26 | width: 50vw; 27 | } 28 | h1{ 29 | font-size: 20px; 30 | } 31 | #textarea { 32 | width: 50vw; 33 | height: 30vh; 34 | border: 1px gray bold; 35 | } 36 | #button{ 37 | margin: 8px; 38 | cursor: pointer; 39 | } 40 | @media screen and (max-width:700px) { 41 | svg{ 42 | width: 90vw; 43 | } 44 | header{ 45 | width: 95vw; 46 | padding: 8px; 47 | } 48 | #graph{ 49 | width: 90vw; 50 | } 51 | #textarea { 52 | width: 90vw; 53 | height: 30vh; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "syscall/js" 6 | 7 | "github.com/po3rin/dockerdot/docker2dot" 8 | ) 9 | 10 | func registerCallbacks() { 11 | var cb js.Func 12 | document := js.Global().Get("document") 13 | element := document.Call("getElementById", "textarea") 14 | 15 | cb = js.FuncOf(func(this js.Value, args []js.Value) interface{} { 16 | text := element.Get("value").String() 17 | dockerfile := []byte(text) 18 | 19 | // https://github.com/golang/go/issues/26382 20 | // should wrap func with gorutine. 21 | go func() { 22 | dot, err := docker2dot.Docker2Dot(dockerfile) 23 | if err != nil { 24 | fmt.Println(err) 25 | } 26 | showGraph := js.Global().Get("showGraph") 27 | showGraph.Invoke(string(dot)) 28 | }() 29 | return nil 30 | }) 31 | js.Global().Get("document").Call("getElementById", "button").Call("addEventListener", "click", cb) 32 | } 33 | 34 | func main() { 35 | c := make(chan struct{}, 0) 36 | registerCallbacks() 37 | <-c 38 | } 39 | -------------------------------------------------------------------------------- /main.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/dockerdot/07ce17e04517ca51b938e670e2f35bb3551af077/main.wasm -------------------------------------------------------------------------------- /static/all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/dockerdot/07ce17e04517ca51b938e670e2f35bb3551af077/static/all.png -------------------------------------------------------------------------------- /static/dockerdot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/dockerdot/07ce17e04517ca51b938e670e2f35bb3551af077/static/dockerdot.png -------------------------------------------------------------------------------- /static/err.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/dockerdot/07ce17e04517ca51b938e670e2f35bb3551af077/static/err.png -------------------------------------------------------------------------------- /static/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/dockerdot/07ce17e04517ca51b938e670e2f35bb3551af077/static/github.png -------------------------------------------------------------------------------- /static/sp.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/po3rin/dockerdot/07ce17e04517ca51b938e670e2f35bb3551af077/static/sp.gif -------------------------------------------------------------------------------- /viz.js: -------------------------------------------------------------------------------- 1 | /* 2 | Viz.js 2.1.2 (Graphviz 2.40.1, Expat 2.2.5, Emscripten 1.37.36) 3 | Copyright (c) 2014-2018 Michael Daines 4 | Licensed under MIT license 5 | 6 | This distribution contains other software in object code form: 7 | 8 | Graphviz 9 | Licensed under Eclipse Public License - v 1.0 10 | http://www.graphviz.org 11 | 12 | Expat 13 | Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd and Clark Cooper 14 | Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers. 15 | Licensed under MIT license 16 | http://www.libexpat.org 17 | 18 | zlib 19 | Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler 20 | http://www.zlib.net/zlib_license.html 21 | */ 22 | (function (global, factory) { 23 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 24 | typeof define === 'function' && define.amd ? define(factory) : 25 | (global.Viz = factory()); 26 | }(this, (function () { 'use strict'; 27 | 28 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 29 | return typeof obj; 30 | } : function (obj) { 31 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 32 | }; 33 | 34 | var classCallCheck = function (instance, Constructor) { 35 | if (!(instance instanceof Constructor)) { 36 | throw new TypeError("Cannot call a class as a function"); 37 | } 38 | }; 39 | 40 | var createClass = function () { 41 | function defineProperties(target, props) { 42 | for (var i = 0; i < props.length; i++) { 43 | var descriptor = props[i]; 44 | descriptor.enumerable = descriptor.enumerable || false; 45 | descriptor.configurable = true; 46 | if ("value" in descriptor) descriptor.writable = true; 47 | Object.defineProperty(target, descriptor.key, descriptor); 48 | } 49 | } 50 | 51 | return function (Constructor, protoProps, staticProps) { 52 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 53 | if (staticProps) defineProperties(Constructor, staticProps); 54 | return Constructor; 55 | }; 56 | }(); 57 | 58 | var _extends = Object.assign || function (target) { 59 | for (var i = 1; i < arguments.length; i++) { 60 | var source = arguments[i]; 61 | 62 | for (var key in source) { 63 | if (Object.prototype.hasOwnProperty.call(source, key)) { 64 | target[key] = source[key]; 65 | } 66 | } 67 | } 68 | 69 | return target; 70 | }; 71 | 72 | var WorkerWrapper = function () { 73 | function WorkerWrapper(worker) { 74 | var _this = this; 75 | 76 | classCallCheck(this, WorkerWrapper); 77 | 78 | this.worker = worker; 79 | this.listeners = []; 80 | this.nextId = 0; 81 | 82 | this.worker.addEventListener('message', function (event) { 83 | var id = event.data.id; 84 | var error = event.data.error; 85 | var result = event.data.result; 86 | 87 | _this.listeners[id](error, result); 88 | delete _this.listeners[id]; 89 | }); 90 | } 91 | 92 | createClass(WorkerWrapper, [{ 93 | key: 'render', 94 | value: function render(src, options) { 95 | var _this2 = this; 96 | 97 | return new Promise(function (resolve, reject) { 98 | var id = _this2.nextId++; 99 | 100 | _this2.listeners[id] = function (error, result) { 101 | if (error) { 102 | reject(new Error(error.message, error.fileName, error.lineNumber)); 103 | return; 104 | } 105 | resolve(result); 106 | }; 107 | 108 | _this2.worker.postMessage({ id: id, src: src, options: options }); 109 | }); 110 | } 111 | }]); 112 | return WorkerWrapper; 113 | }(); 114 | 115 | var ModuleWrapper = function ModuleWrapper(module, render) { 116 | classCallCheck(this, ModuleWrapper); 117 | 118 | var instance = module(); 119 | this.render = function (src, options) { 120 | return new Promise(function (resolve, reject) { 121 | try { 122 | resolve(render(instance, src, options)); 123 | } catch (error) { 124 | reject(error); 125 | } 126 | }); 127 | }; 128 | }; 129 | 130 | // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 131 | 132 | 133 | function b64EncodeUnicode(str) { 134 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { 135 | return String.fromCharCode('0x' + p1); 136 | })); 137 | } 138 | 139 | function defaultScale() { 140 | if ('devicePixelRatio' in window && window.devicePixelRatio > 1) { 141 | return window.devicePixelRatio; 142 | } else { 143 | return 1; 144 | } 145 | } 146 | 147 | function svgXmlToImageElement(svgXml) { 148 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 149 | _ref$scale = _ref.scale, 150 | scale = _ref$scale === undefined ? defaultScale() : _ref$scale, 151 | _ref$mimeType = _ref.mimeType, 152 | mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType, 153 | _ref$quality = _ref.quality, 154 | quality = _ref$quality === undefined ? 1 : _ref$quality; 155 | 156 | return new Promise(function (resolve, reject) { 157 | var svgImage = new Image(); 158 | 159 | svgImage.onload = function () { 160 | var canvas = document.createElement('canvas'); 161 | canvas.width = svgImage.width * scale; 162 | canvas.height = svgImage.height * scale; 163 | 164 | var context = canvas.getContext("2d"); 165 | context.drawImage(svgImage, 0, 0, canvas.width, canvas.height); 166 | 167 | canvas.toBlob(function (blob) { 168 | var image = new Image(); 169 | image.src = URL.createObjectURL(blob); 170 | image.width = svgImage.width; 171 | image.height = svgImage.height; 172 | 173 | resolve(image); 174 | }, mimeType, quality); 175 | }; 176 | 177 | svgImage.onerror = function (e) { 178 | var error; 179 | 180 | if ('error' in e) { 181 | error = e.error; 182 | } else { 183 | error = new Error('Error loading SVG'); 184 | } 185 | 186 | reject(error); 187 | }; 188 | 189 | svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml); 190 | }); 191 | } 192 | 193 | function svgXmlToImageElementFabric(svgXml) { 194 | var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 195 | _ref2$scale = _ref2.scale, 196 | scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale, 197 | _ref2$mimeType = _ref2.mimeType, 198 | mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType, 199 | _ref2$quality = _ref2.quality, 200 | quality = _ref2$quality === undefined ? 1 : _ref2$quality; 201 | 202 | var multiplier = scale; 203 | 204 | var format = void 0; 205 | if (mimeType == 'image/jpeg') { 206 | format = 'jpeg'; 207 | } else if (mimeType == 'image/png') { 208 | format = 'png'; 209 | } 210 | 211 | return new Promise(function (resolve, reject) { 212 | fabric.loadSVGFromString(svgXml, function (objects, options) { 213 | // If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one element back even given an empty graph, so we will assume an error in this case. 214 | if (objects.length == 0) { 215 | reject(new Error('Error loading SVG with Fabric')); 216 | } 217 | 218 | var element = document.createElement("canvas"); 219 | element.width = options.width; 220 | element.height = options.height; 221 | 222 | var canvas = new fabric.Canvas(element, { enableRetinaScaling: false }); 223 | var obj = fabric.util.groupSVGElements(objects, options); 224 | canvas.add(obj).renderAll(); 225 | 226 | var image = new Image(); 227 | image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality }); 228 | image.width = options.width; 229 | image.height = options.height; 230 | 231 | resolve(image); 232 | }); 233 | }); 234 | } 235 | 236 | var Viz = function () { 237 | function Viz() { 238 | var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 239 | workerURL = _ref3.workerURL, 240 | worker = _ref3.worker, 241 | Module = _ref3.Module, 242 | render = _ref3.render; 243 | 244 | classCallCheck(this, Viz); 245 | 246 | if (typeof workerURL !== 'undefined') { 247 | this.wrapper = new WorkerWrapper(new Worker(workerURL)); 248 | } else if (typeof worker !== 'undefined') { 249 | this.wrapper = new WorkerWrapper(worker); 250 | } else if (typeof Module !== 'undefined' && typeof render !== 'undefined') { 251 | this.wrapper = new ModuleWrapper(Module, render); 252 | } else if (typeof Viz.Module !== 'undefined' && typeof Viz.render !== 'undefined') { 253 | this.wrapper = new ModuleWrapper(Viz.Module, Viz.render); 254 | } else { 255 | throw new Error('Must specify workerURL or worker option, Module and render options, or include one of full.render.js or lite.render.js after viz.js.'); 256 | } 257 | } 258 | 259 | createClass(Viz, [{ 260 | key: 'renderString', 261 | value: function renderString(src) { 262 | var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 263 | _ref4$format = _ref4.format, 264 | format = _ref4$format === undefined ? 'svg' : _ref4$format, 265 | _ref4$engine = _ref4.engine, 266 | engine = _ref4$engine === undefined ? 'dot' : _ref4$engine, 267 | _ref4$files = _ref4.files, 268 | files = _ref4$files === undefined ? [] : _ref4$files, 269 | _ref4$images = _ref4.images, 270 | images = _ref4$images === undefined ? [] : _ref4$images, 271 | _ref4$yInvert = _ref4.yInvert, 272 | yInvert = _ref4$yInvert === undefined ? false : _ref4$yInvert, 273 | _ref4$nop = _ref4.nop, 274 | nop = _ref4$nop === undefined ? 0 : _ref4$nop; 275 | 276 | for (var i = 0; i < images.length; i++) { 277 | files.push({ 278 | path: images[i].path, 279 | data: '\n\n' 280 | }); 281 | } 282 | 283 | return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop }); 284 | } 285 | }, { 286 | key: 'renderSVGElement', 287 | value: function renderSVGElement(src) { 288 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 289 | 290 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 291 | var parser = new DOMParser(); 292 | return parser.parseFromString(str, 'image/svg+xml').documentElement; 293 | }); 294 | } 295 | }, { 296 | key: 'renderImageElement', 297 | value: function renderImageElement(src) { 298 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 299 | var scale = options.scale, 300 | mimeType = options.mimeType, 301 | quality = options.quality; 302 | 303 | 304 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 305 | if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) { 306 | return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality }); 307 | } else { 308 | return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality }); 309 | } 310 | }); 311 | } 312 | }, { 313 | key: 'renderJSONObject', 314 | value: function renderJSONObject(src) { 315 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 316 | var format = options.format; 317 | 318 | 319 | if (format !== 'json' || format !== 'json0') { 320 | format = 'json'; 321 | } 322 | 323 | return this.renderString(src, _extends({}, options, { format: format })).then(function (str) { 324 | return JSON.parse(str); 325 | }); 326 | } 327 | }]); 328 | return Viz; 329 | }(); 330 | 331 | return Viz; 332 | 333 | }))); 334 | -------------------------------------------------------------------------------- /wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | if (!global.fs) { 34 | let outputBuf = ""; 35 | global.fs = { 36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 37 | writeSync(fd, buf) { 38 | outputBuf += decoder.decode(buf); 39 | const nl = outputBuf.lastIndexOf("\n"); 40 | if (nl != -1) { 41 | console.log(outputBuf.substr(0, nl)); 42 | outputBuf = outputBuf.substr(nl + 1); 43 | } 44 | return buf.length; 45 | }, 46 | write(fd, buf, offset, length, position, callback) { 47 | if (offset !== 0 || length !== buf.length || position !== null) { 48 | throw new Error("not implemented"); 49 | } 50 | const n = this.writeSync(fd, buf); 51 | callback(null, n); 52 | }, 53 | open(path, flags, mode, callback) { 54 | const err = new Error("not implemented"); 55 | err.code = "ENOSYS"; 56 | callback(err); 57 | }, 58 | read(fd, buffer, offset, length, position, callback) { 59 | const err = new Error("not implemented"); 60 | err.code = "ENOSYS"; 61 | callback(err); 62 | }, 63 | fsync(fd, callback) { 64 | callback(null); 65 | }, 66 | }; 67 | } 68 | 69 | if (!global.crypto) { 70 | const nodeCrypto = require("crypto"); 71 | global.crypto = { 72 | getRandomValues(b) { 73 | nodeCrypto.randomFillSync(b); 74 | }, 75 | }; 76 | } 77 | 78 | if (!global.performance) { 79 | global.performance = { 80 | now() { 81 | const [sec, nsec] = process.hrtime(); 82 | return sec * 1000 + nsec / 1000000; 83 | }, 84 | }; 85 | } 86 | 87 | if (!global.TextEncoder) { 88 | global.TextEncoder = require("util").TextEncoder; 89 | } 90 | 91 | if (!global.TextDecoder) { 92 | global.TextDecoder = require("util").TextDecoder; 93 | } 94 | 95 | // End of polyfills for common API. 96 | 97 | const encoder = new TextEncoder("utf-8"); 98 | const decoder = new TextDecoder("utf-8"); 99 | 100 | global.Go = class { 101 | constructor() { 102 | this.argv = ["js"]; 103 | this.env = {}; 104 | this.exit = (code) => { 105 | if (code !== 0) { 106 | console.warn("exit code:", code); 107 | } 108 | }; 109 | this._exitPromise = new Promise((resolve) => { 110 | this._resolveExitPromise = resolve; 111 | }); 112 | this._pendingEvent = null; 113 | this._scheduledTimeouts = new Map(); 114 | this._nextCallbackTimeoutID = 1; 115 | 116 | const mem = () => { 117 | // The buffer may change when requesting more memory. 118 | return new DataView(this._inst.exports.mem.buffer); 119 | } 120 | 121 | const setInt64 = (addr, v) => { 122 | mem().setUint32(addr + 0, v, true); 123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 124 | } 125 | 126 | const getInt64 = (addr) => { 127 | const low = mem().getUint32(addr + 0, true); 128 | const high = mem().getInt32(addr + 4, true); 129 | return low + high * 4294967296; 130 | } 131 | 132 | const loadValue = (addr) => { 133 | const f = mem().getFloat64(addr, true); 134 | if (f === 0) { 135 | return undefined; 136 | } 137 | if (!isNaN(f)) { 138 | return f; 139 | } 140 | 141 | const id = mem().getUint32(addr, true); 142 | return this._values[id]; 143 | } 144 | 145 | const storeValue = (addr, v) => { 146 | const nanHead = 0x7FF80000; 147 | 148 | if (typeof v === "number") { 149 | if (isNaN(v)) { 150 | mem().setUint32(addr + 4, nanHead, true); 151 | mem().setUint32(addr, 0, true); 152 | return; 153 | } 154 | if (v === 0) { 155 | mem().setUint32(addr + 4, nanHead, true); 156 | mem().setUint32(addr, 1, true); 157 | return; 158 | } 159 | mem().setFloat64(addr, v, true); 160 | return; 161 | } 162 | 163 | switch (v) { 164 | case undefined: 165 | mem().setFloat64(addr, 0, true); 166 | return; 167 | case null: 168 | mem().setUint32(addr + 4, nanHead, true); 169 | mem().setUint32(addr, 2, true); 170 | return; 171 | case true: 172 | mem().setUint32(addr + 4, nanHead, true); 173 | mem().setUint32(addr, 3, true); 174 | return; 175 | case false: 176 | mem().setUint32(addr + 4, nanHead, true); 177 | mem().setUint32(addr, 4, true); 178 | return; 179 | } 180 | 181 | let ref = this._refs.get(v); 182 | if (ref === undefined) { 183 | ref = this._values.length; 184 | this._values.push(v); 185 | this._refs.set(v, ref); 186 | } 187 | let typeFlag = 0; 188 | switch (typeof v) { 189 | case "string": 190 | typeFlag = 1; 191 | break; 192 | case "symbol": 193 | typeFlag = 2; 194 | break; 195 | case "function": 196 | typeFlag = 3; 197 | break; 198 | } 199 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 200 | mem().setUint32(addr, ref, true); 201 | } 202 | 203 | const loadSlice = (addr) => { 204 | const array = getInt64(addr + 0); 205 | const len = getInt64(addr + 8); 206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 207 | } 208 | 209 | const loadSliceOfValues = (addr) => { 210 | const array = getInt64(addr + 0); 211 | const len = getInt64(addr + 8); 212 | const a = new Array(len); 213 | for (let i = 0; i < len; i++) { 214 | a[i] = loadValue(array + i * 8); 215 | } 216 | return a; 217 | } 218 | 219 | const loadString = (addr) => { 220 | const saddr = getInt64(addr + 0); 221 | const len = getInt64(addr + 8); 222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 223 | } 224 | 225 | const timeOrigin = Date.now() - performance.now(); 226 | this.importObject = { 227 | go: { 228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 231 | // This changes the SP, thus we have to update the SP used by the imported function. 232 | 233 | // func wasmExit(code int32) 234 | "runtime.wasmExit": (sp) => { 235 | const code = mem().getInt32(sp + 8, true); 236 | this.exited = true; 237 | delete this._inst; 238 | delete this._values; 239 | delete this._refs; 240 | this.exit(code); 241 | }, 242 | 243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 244 | "runtime.wasmWrite": (sp) => { 245 | const fd = getInt64(sp + 8); 246 | const p = getInt64(sp + 16); 247 | const n = mem().getInt32(sp + 24, true); 248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 249 | }, 250 | 251 | // func nanotime() int64 252 | "runtime.nanotime": (sp) => { 253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 254 | }, 255 | 256 | // func walltime() (sec int64, nsec int32) 257 | "runtime.walltime": (sp) => { 258 | const msec = (new Date).getTime(); 259 | setInt64(sp + 8, msec / 1000); 260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); 261 | }, 262 | 263 | // func scheduleTimeoutEvent(delay int64) int32 264 | "runtime.scheduleTimeoutEvent": (sp) => { 265 | const id = this._nextCallbackTimeoutID; 266 | this._nextCallbackTimeoutID++; 267 | this._scheduledTimeouts.set(id, setTimeout( 268 | () => { 269 | this._resume(); 270 | while (this._scheduledTimeouts.has(id)) { 271 | // for some reason Go failed to register the timeout event, log and try again 272 | // (temporary workaround for https://github.com/golang/go/issues/28975) 273 | console.warn("scheduleTimeoutEvent: missed timeout event"); 274 | this._resume(); 275 | } 276 | }, 277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 278 | )); 279 | mem().setInt32(sp + 16, id, true); 280 | }, 281 | 282 | // func clearTimeoutEvent(id int32) 283 | "runtime.clearTimeoutEvent": (sp) => { 284 | const id = mem().getInt32(sp + 8, true); 285 | clearTimeout(this._scheduledTimeouts.get(id)); 286 | this._scheduledTimeouts.delete(id); 287 | }, 288 | 289 | // func getRandomData(r []byte) 290 | "runtime.getRandomData": (sp) => { 291 | crypto.getRandomValues(loadSlice(sp + 8)); 292 | }, 293 | 294 | // func stringVal(value string) ref 295 | "syscall/js.stringVal": (sp) => { 296 | storeValue(sp + 24, loadString(sp + 8)); 297 | }, 298 | 299 | // func valueGet(v ref, p string) ref 300 | "syscall/js.valueGet": (sp) => { 301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 302 | sp = this._inst.exports.getsp(); // see comment above 303 | storeValue(sp + 32, result); 304 | }, 305 | 306 | // func valueSet(v ref, p string, x ref) 307 | "syscall/js.valueSet": (sp) => { 308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 309 | }, 310 | 311 | // func valueIndex(v ref, i int) ref 312 | "syscall/js.valueIndex": (sp) => { 313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 314 | }, 315 | 316 | // valueSetIndex(v ref, i int, x ref) 317 | "syscall/js.valueSetIndex": (sp) => { 318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 319 | }, 320 | 321 | // func valueCall(v ref, m string, args []ref) (ref, bool) 322 | "syscall/js.valueCall": (sp) => { 323 | try { 324 | const v = loadValue(sp + 8); 325 | const m = Reflect.get(v, loadString(sp + 16)); 326 | const args = loadSliceOfValues(sp + 32); 327 | const result = Reflect.apply(m, v, args); 328 | sp = this._inst.exports.getsp(); // see comment above 329 | storeValue(sp + 56, result); 330 | mem().setUint8(sp + 64, 1); 331 | } catch (err) { 332 | storeValue(sp + 56, err); 333 | mem().setUint8(sp + 64, 0); 334 | } 335 | }, 336 | 337 | // func valueInvoke(v ref, args []ref) (ref, bool) 338 | "syscall/js.valueInvoke": (sp) => { 339 | try { 340 | const v = loadValue(sp + 8); 341 | const args = loadSliceOfValues(sp + 16); 342 | const result = Reflect.apply(v, undefined, args); 343 | sp = this._inst.exports.getsp(); // see comment above 344 | storeValue(sp + 40, result); 345 | mem().setUint8(sp + 48, 1); 346 | } catch (err) { 347 | storeValue(sp + 40, err); 348 | mem().setUint8(sp + 48, 0); 349 | } 350 | }, 351 | 352 | // func valueNew(v ref, args []ref) (ref, bool) 353 | "syscall/js.valueNew": (sp) => { 354 | try { 355 | const v = loadValue(sp + 8); 356 | const args = loadSliceOfValues(sp + 16); 357 | const result = Reflect.construct(v, args); 358 | sp = this._inst.exports.getsp(); // see comment above 359 | storeValue(sp + 40, result); 360 | mem().setUint8(sp + 48, 1); 361 | } catch (err) { 362 | storeValue(sp + 40, err); 363 | mem().setUint8(sp + 48, 0); 364 | } 365 | }, 366 | 367 | // func valueLength(v ref) int 368 | "syscall/js.valueLength": (sp) => { 369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 370 | }, 371 | 372 | // valuePrepareString(v ref) (ref, int) 373 | "syscall/js.valuePrepareString": (sp) => { 374 | const str = encoder.encode(String(loadValue(sp + 8))); 375 | storeValue(sp + 16, str); 376 | setInt64(sp + 24, str.length); 377 | }, 378 | 379 | // valueLoadString(v ref, b []byte) 380 | "syscall/js.valueLoadString": (sp) => { 381 | const str = loadValue(sp + 8); 382 | loadSlice(sp + 16).set(str); 383 | }, 384 | 385 | // func valueInstanceOf(v ref, t ref) bool 386 | "syscall/js.valueInstanceOf": (sp) => { 387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 388 | }, 389 | 390 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 391 | "syscall/js.copyBytesToGo": (sp) => { 392 | const dst = loadSlice(sp + 8); 393 | const src = loadValue(sp + 32); 394 | if (!(src instanceof Uint8Array)) { 395 | mem().setUint8(sp + 48, 0); 396 | return; 397 | } 398 | const toCopy = src.subarray(0, dst.length); 399 | dst.set(toCopy); 400 | setInt64(sp + 40, toCopy.length); 401 | mem().setUint8(sp + 48, 1); 402 | }, 403 | 404 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 405 | "syscall/js.copyBytesToJS": (sp) => { 406 | const dst = loadValue(sp + 8); 407 | const src = loadSlice(sp + 16); 408 | if (!(dst instanceof Uint8Array)) { 409 | mem().setUint8(sp + 48, 0); 410 | return; 411 | } 412 | const toCopy = src.subarray(0, dst.length); 413 | dst.set(toCopy); 414 | setInt64(sp + 40, toCopy.length); 415 | mem().setUint8(sp + 48, 1); 416 | }, 417 | 418 | "debug": (value) => { 419 | console.log(value); 420 | }, 421 | } 422 | }; 423 | } 424 | 425 | async run(instance) { 426 | this._inst = instance; 427 | this._values = [ // TODO: garbage collection 428 | NaN, 429 | 0, 430 | null, 431 | true, 432 | false, 433 | global, 434 | this, 435 | ]; 436 | this._refs = new Map(); 437 | this.exited = false; 438 | 439 | const mem = new DataView(this._inst.exports.mem.buffer) 440 | 441 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 442 | let offset = 4096; 443 | 444 | const strPtr = (str) => { 445 | const ptr = offset; 446 | const bytes = encoder.encode(str + "\0"); 447 | new Uint8Array(mem.buffer, offset, bytes.length).set(bytes); 448 | offset += bytes.length; 449 | if (offset % 8 !== 0) { 450 | offset += 8 - (offset % 8); 451 | } 452 | return ptr; 453 | }; 454 | 455 | const argc = this.argv.length; 456 | 457 | const argvPtrs = []; 458 | this.argv.forEach((arg) => { 459 | argvPtrs.push(strPtr(arg)); 460 | }); 461 | 462 | const keys = Object.keys(this.env).sort(); 463 | argvPtrs.push(keys.length); 464 | keys.forEach((key) => { 465 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 466 | }); 467 | 468 | const argv = offset; 469 | argvPtrs.forEach((ptr) => { 470 | mem.setUint32(offset, ptr, true); 471 | mem.setUint32(offset + 4, 0, true); 472 | offset += 8; 473 | }); 474 | 475 | this._inst.exports.run(argc, argv); 476 | if (this.exited) { 477 | this._resolveExitPromise(); 478 | } 479 | await this._exitPromise; 480 | } 481 | 482 | _resume() { 483 | if (this.exited) { 484 | throw new Error("Go program has already exited"); 485 | } 486 | this._inst.exports.resume(); 487 | if (this.exited) { 488 | this._resolveExitPromise(); 489 | } 490 | } 491 | 492 | _makeFuncWrapper(id) { 493 | const go = this; 494 | return function () { 495 | const event = { id: id, this: this, args: arguments }; 496 | go._pendingEvent = event; 497 | go._resume(); 498 | return event.result; 499 | }; 500 | } 501 | } 502 | 503 | if ( 504 | global.require && 505 | global.require.main === module && 506 | global.process && 507 | global.process.versions && 508 | !global.process.versions.electron 509 | ) { 510 | if (process.argv.length < 3) { 511 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 512 | process.exit(1); 513 | } 514 | 515 | const go = new Go(); 516 | go.argv = process.argv.slice(2); 517 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 518 | go.exit = process.exit; 519 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 520 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 521 | if (code === 0 && !go.exited) { 522 | // deadlock, make Go print error and stack traces 523 | go._pendingEvent = { id: 0 }; 524 | go._resume(); 525 | } 526 | }); 527 | return go.run(result.instance); 528 | }).catch((err) => { 529 | console.error(err); 530 | process.exit(1); 531 | }); 532 | } 533 | })(); 534 | --------------------------------------------------------------------------------