├── .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 |
--------------------------------------------------------------------------------