From 8e1272dfc892cd37169126d7d578227a690ed0bf Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Mon, 10 Oct 2022 11:58:39 +0200 Subject: [PATCH] export(local): wrap opt Signed-off-by: CrazyMax --- README.md | 48 ++++++++++++++++++++ exporter/local/export.go | 43 +++++++++++++----- frontend/dockerfile/dockerfile_test.go | 62 ++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4d24ef5cd8e9d..8472eb7d9f917 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,54 @@ buildctl build ... --output type=tar,dest=out.tar buildctl build ... --output type=tar > out.tar ``` +With a [multi-platform build](docs/multi-platform.md), a subfolder matching +each target platform will be created in the destination directory: + +```dockerfile +FROM busybox AS build +ARG TARGETOS +ARG TARGETARCH +RUN mkdir /out && echo foo > /out/hello-$TARGETOS-$TARGETARCH + +FROM scratch +COPY --from=build /out / +``` + +```bash +$ buildctl build \ + --frontend dockerfile.v0 \ + --opt platform=linux/amd64,linux/arm64 \ + --output type=local,dest=./bin/release + +$ tree ./bin +./bin/ +└── release + ├── linux_amd64 + │ └── hello-linux-amd64 + └── linux_arm64 + └── hello-linux-arm64 +``` + +You can use the `wrap` option to wrap content in destination directory: + +```bash +$ buildctl build \ + --frontend dockerfile.v0 \ + --opt platform=linux/amd64,linux/arm64 \ + --output type=local,dest=./bin/release,wrap=true + +$ tree ./bin +./bin/ +└── release + ├── hello-linux-amd64 + └── hello-linux-arm64 +``` + +> **Warning** +> +> Wrapping content may overwrite existing files from build result of other +> platforms. + #### Docker tarball ```bash diff --git a/exporter/local/export.go b/exporter/local/export.go index 3f8889a237599..b206f5aabbddc 100644 --- a/exporter/local/export.go +++ b/exporter/local/export.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "os" + "strconv" "strings" "time" @@ -23,6 +24,12 @@ import ( "golang.org/x/time/rate" ) +const ( + // wrap is an exporter option which can be used to merge result in the + // output directory when multiple references are exported. + attrWrap = "wrap" +) + type Opt struct { SessionManager *session.Manager } @@ -38,16 +45,28 @@ func New(opt Opt) (exporter.Exporter, error) { } func (e *localExporter) Resolve(ctx context.Context, opt map[string]string) (exporter.ExporterInstance, error) { + ei := &localExporterInstance{localExporter: e} + + if v, ok := opt[attrWrap]; ok { + b, err := strconv.ParseBool(v) + if err != nil { + return nil, errors.Wrapf(err, "non-bool value for %s: %s", attrWrap, v) + } + ei.wrap = b + } + tm, _, err := epoch.ParseAttr(opt) if err != nil { return nil, err } + ei.epoch = tm - return &localExporterInstance{localExporter: e, epoch: tm}, nil + return ei, nil } type localExporterInstance struct { *localExporter + wrap bool epoch *time.Time } @@ -152,16 +171,18 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source lbl := "copying files" if isMap { lbl += " " + k - st := fstypes.Stat{ - Mode: uint32(os.ModeDir | 0755), - Path: strings.Replace(k, "/", "_", -1), - } - if e.epoch != nil { - st.ModTime = e.epoch.UnixNano() - } - fs, err = fsutil.SubDirFS([]fsutil.Dir{{FS: fs, Stat: st}}) - if err != nil { - return err + if !e.wrap { + st := fstypes.Stat{ + Mode: uint32(os.ModeDir | 0755), + Path: strings.Replace(k, "/", "_", -1), + } + if e.epoch != nil { + st.ModTime = e.epoch.UnixNano() + } + fs, err = fsutil.SubDirFS([]fsutil.Dir{{FS: fs, Stat: st}}) + if err != nil { + return err + } } } diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index 0e11aabb11645..c77da70fad252 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -86,6 +86,7 @@ var allTests = integration.TestFuncs( testPlatformArgsImplicit, testPlatformArgsExplicit, testExportMultiPlatform, + testExportLocalWrapMultiPlatform, testQuotedMetaArgs, testIgnoreEntrypoint, testSymlinkedDockerfile, @@ -1475,6 +1476,67 @@ COPY arch-$TARGETARCH whoami } } +func testExportLocalWrapMultiPlatform(t *testing.T, sb integration.Sandbox) { + integration.SkipIfDockerd(t, sb, "oci exporter", "multi-platform") + f := getFrontend(t, sb) + + dockerfile := []byte(` +FROM scratch +ARG TARGETOS +ARG TARGETARCH +COPY hello-$TARGETOS-$TARGETARCH . +`) + + dir, err := integration.Tmpdir( + t, + fstest.CreateFile("Dockerfile", dockerfile, 0600), + fstest.CreateFile("hello-linux-arm", []byte(`hello linux/arm`), 0600), + fstest.CreateFile("hello-linux-amd64", []byte(`hello linux/amd64`), 0600), + fstest.CreateFile("hello-linux-s390x", []byte(`hello linux/s390x`), 0600), + fstest.CreateFile("hello-linux-ppc64le", []byte(`hello linux/ppc64le`), 0600), + fstest.CreateFile("hello-windows-amd64", []byte(`hello windows/amd64`), 0600), + ) + require.NoError(t, err) + + c, err := client.New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + destDir := t.TempDir() + + _, err = f.Solve(sb.Context(), c, client.SolveOpt{ + LocalDirs: map[string]string{ + builder.DefaultLocalNameDockerfile: dir, + builder.DefaultLocalNameContext: dir, + }, + FrontendAttrs: map[string]string{ + "platform": "windows/amd64,linux/arm,linux/s390x", + }, + Exports: []client.ExportEntry{ + { + Type: client.ExporterLocal, + OutputDir: destDir, + Attrs: map[string]string{ + "wrap": "true", + }, + }, + }, + }, nil) + require.NoError(t, err) + + dt, err := os.ReadFile(filepath.Join(destDir, "hello-windows-amd64")) + require.NoError(t, err) + require.Equal(t, "hello windows/amd64", string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "hello-linux-arm")) + require.NoError(t, err) + require.Equal(t, "hello linux/arm", string(dt)) + + dt, err = os.ReadFile(filepath.Join(destDir, "hello-linux-s390x")) + require.NoError(t, err) + require.Equal(t, "hello linux/s390x", string(dt)) +} + // tonistiigi/fsutil#46 func testContextChangeDirToFile(t *testing.T, sb integration.Sandbox) { f := getFrontend(t, sb)