Skip to content

Commit

Permalink
Merge pull request #399 from AkihiroSuda/fix-build-output
Browse files Browse the repository at this point in the history
build: support --output=(oci|tar|docker|image), --quiet, --cache-from, --cache-to
  • Loading branch information
AkihiroSuda authored Sep 30, 2021
2 parents d8d4894 + 6f68f89 commit aab5de3
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 37 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,11 +593,20 @@ Flags:
- :whale: `--target`: Set the target build stage to build
- :whale: `--build-arg`: Set build-time variables
- :whale: `--no-cache`: Do not use cache when building the image
- :whale: `--output=OUTPUT`: Output destination (format: type=local,dest=path)
- :whale: `type=local,dest=path/to/output-dir`: Local directory
- :whale: `type=oci[,dest=path/to/output.tar]`: Docker/OCI dual-format tar ball (compatible with `docker buildx build`)
- :whale: `type=docker[,dest=path/to/output.tar]`: Docker format tar ball (compatible with `docker buildx build`)
- :whale: `type=tar[,dest=path/to/output.tar]`: Raw tar ball
- :whale: `type=image,name=example.com/image,push=true`: Push to a registry (see [`buildctl build`](https://github.com/moby/buildkit/tree/v0.9.0#imageregistry) documentation)
- :whale: `--progress=(auto|plain|tty)`: Set type of progress output (auto, plain, tty). Use plain to show container output
- :whale: `--secret`: Secret file to expose to the build: id=mysecret,src=/local/secret
- :whale: `--ssh`: SSH agent socket or keys to expose to the build (format: `default|<id>[=<socket>|<key>[,<key>]]`)
- :whale: `-q, --quiet`: Suppress the build output and print image ID on success
- :whale: `--cache-from=CACHE`: External cache sources (eg. user/app:cache, type=local,src=path/to/dir) (compatible with `docker buildx build`)
- :whale: `--cache-to=CACHE`: Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir) (compatible with `docker buildx build`)

Unimplemented `docker build` flags: `--add-host`, `--cache-from`, `--iidfile`, `--label`, `--network`, `--platform`, `--quiet`, `--squash`
Unimplemented `docker build` flags: `--add-host`, `--iidfile`, `--label`, `--network`, `--platform`, `--squash`

### :whale: nerdctl commit
Create a new image from a container's changes
Expand Down
102 changes: 71 additions & 31 deletions cmd/nerdctl/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
package main

import (
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"

"path/filepath"
Expand Down Expand Up @@ -69,7 +70,7 @@ var buildCommand = &cli.Command{
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Value: "type=docker",
Usage: "Output destination (format: type=local,dest=path)",
},
&cli.StringFlag{
Name: "progress",
Expand All @@ -84,6 +85,19 @@ var buildCommand = &cli.Command{
Name: "ssh",
Usage: "SSH agent socket or keys to expose to the build (format: default|<id>[=<socket>|<key>[,<key>]])",
},
&cli.BoolFlag{
Name: "quiet",
Aliases: []string{"q"},
Usage: "Suppress the build output and print image ID on success",
},
&cli.StringSliceFlag{
Name: "cache-from",
Usage: "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)",
},
&cli.StringSliceFlag{
Name: "cache-to",
Usage: "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)",
},
},
}

Expand All @@ -93,31 +107,37 @@ func buildAction(clicontext *cli.Context) error {
return err
}

buildctlBinary, buildctlArgs, err := generateBuildctlArgs(clicontext)
buildctlBinary, buildctlArgs, needsLoading, err := generateBuildctlArgs(clicontext)
if err != nil {
return err
}

quiet := clicontext.Bool("quiet")

logrus.Debugf("running %s %v", buildctlBinary, buildctlArgs)
buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...)
buildctlCmd.Env = os.Environ()

buildctlStdout, err := buildctlCmd.StdoutPipe()
if err != nil {
return err
var buildctlStdout io.Reader
if needsLoading {
buildctlStdout, err = buildctlCmd.StdoutPipe()
if err != nil {
return err
}
} else {
buildctlCmd.Stdout = clicontext.App.Writer
}
buildctlCmd.Stderr = clicontext.App.ErrWriter

if err := buildctlCmd.Start(); err != nil {
return err
if !quiet {
buildctlCmd.Stderr = clicontext.App.ErrWriter
}

localBuild, err := isLocalBuild(clicontext)
if err != nil {
if err := buildctlCmd.Start(); err != nil {
return err
}
if !localBuild {
if err = loadImage(buildctlStdout, clicontext); err != nil {

if needsLoading {
if err = loadImage(buildctlStdout, clicontext, quiet); err != nil {
return err
}
}
Expand All @@ -129,24 +149,29 @@ func buildAction(clicontext *cli.Context) error {
return nil
}

func generateBuildctlArgs(clicontext *cli.Context) (string, []string, error) {
func generateBuildctlArgs(clicontext *cli.Context) (string, []string, bool, error) {
var needsLoading bool
if clicontext.NArg() < 1 {
return "", nil, errors.New("context needs to be specified")
return "", nil, false, errors.New("context needs to be specified")
}
buildContext := clicontext.Args().First()
if buildContext == "-" || strings.Contains(buildContext, "://") {
return "", nil, errors.Errorf("unsupported build context: %q", buildContext)
return "", nil, false, errors.Errorf("unsupported build context: %q", buildContext)
}

buildctlBinary, err := buildkitutil.BuildctlBinary()
if err != nil {
return "", nil, err
return "", nil, false, err
}

output := fmt.Sprintf("--output=%s", clicontext.String("output"))
output := clicontext.String("output")
if output == "" {
output = "type=docker"
needsLoading = true
}
if tagSlice := strutil.DedupeStrSlice(clicontext.StringSlice("tag")); len(tagSlice) > 0 {
if len(tagSlice) > 1 {
return "", nil, errors.Errorf("specifying multiple -t is not supported yet")
return "", nil, false, errors.Errorf("specifying multiple -t is not supported yet")
}
output += ",name=" + tagSlice[0]
}
Expand All @@ -159,7 +184,7 @@ func generateBuildctlArgs(clicontext *cli.Context) (string, []string, error) {
"--frontend=dockerfile.v0",
"--local=context=" + buildContext,
"--local=dockerfile=" + buildContext,
output,
"--output=" + output,
}...)

if filename := clicontext.String("file"); filename != "" {
Expand All @@ -176,6 +201,20 @@ func generateBuildctlArgs(clicontext *cli.Context) (string, []string, error) {

for _, ba := range strutil.DedupeStrSlice(clicontext.StringSlice("build-arg")) {
buildctlArgs = append(buildctlArgs, "--opt=build-arg:"+ba)

// Support `--build-arg BUILDKIT_INLINE_CACHE=1` for compatibility with `docker buildx build`
// https://github.com/docker/buildx/blob/v0.6.3/docs/reference/buildx_build.md#-export-build-cache-to-an-external-cache-destination---cache-to
if strings.HasPrefix(ba, "BUILDKIT_INLINE_CACHE=") {
bic := strings.TrimPrefix(ba, "BUILDKIT_INLINE_CACHE=")
bicParsed, err := strconv.ParseBool(bic)
if err == nil {
if bicParsed {
buildctlArgs = append(buildctlArgs, "--export-cache=type=inline")
}
} else {
logrus.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic)
}
}
}

if clicontext.Bool("no-cache") {
Expand All @@ -190,18 +229,19 @@ func generateBuildctlArgs(clicontext *cli.Context) (string, []string, error) {
buildctlArgs = append(buildctlArgs, "--ssh="+s)
}

return buildctlBinary, buildctlArgs, nil
}

func isLocalBuild(clicontext *cli.Context) (bool, error) {
opts, err := strutil.ParseCSVMap(clicontext.String("output"))
if err != nil {
return false, err
for _, s := range strutil.DedupeStrSlice(clicontext.StringSlice("cache-from")) {
if !strings.Contains(s, "type=") {
s = "type=registry,ref=" + s
}
buildctlArgs = append(buildctlArgs, "--import-cache="+s)
}
if v, ok := opts["type"]; ok {
if strings.TrimSpace(strings.ToLower(v)) == "local" {
return true, nil

for _, s := range strutil.DedupeStrSlice(clicontext.StringSlice("cache-to")) {
if !strings.Contains(s, "type=") {
s = "type=registry,ref=" + s
}
buildctlArgs = append(buildctlArgs, "--export-cache="+s)
}
return false, nil

return buildctlBinary, buildctlArgs, needsLoading, nil
}
14 changes: 10 additions & 4 deletions cmd/nerdctl/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,10 @@ func loadAction(clicontext *cli.Context) error {
defer f.Close()
in = f
}
return loadImage(in, clicontext)
return loadImage(in, clicontext, false)
}

func loadImage(in io.Reader, clicontext *cli.Context) error {
func loadImage(in io.Reader, clicontext *cli.Context, quiet bool) error {
client, ctx, cancel, err := newClient(clicontext, containerd.WithDefaultPlatform(platforms.DefaultStrict()))
if err != nil {
return err
Expand All @@ -74,12 +74,18 @@ func loadImage(in io.Reader, clicontext *cli.Context) error {
image := containerd.NewImage(client, img)

// TODO: Show unpack status
fmt.Fprintf(clicontext.App.Writer, "unpacking %s (%s)...", img.Name, img.Target.Digest)
if !quiet {
fmt.Fprintf(clicontext.App.Writer, "unpacking %s (%s)...", img.Name, img.Target.Digest)
}
err = image.Unpack(ctx, sn)
if err != nil {
return err
}
fmt.Fprintf(clicontext.App.Writer, "done\n")
if quiet {
fmt.Fprintln(clicontext.App.Writer, img.Target.Digest)
} else {
fmt.Fprintf(clicontext.App.Writer, "done\n")
}
}

return nil
Expand Down
6 changes: 5 additions & 1 deletion pkg/composer/serviceparser/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (

func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName string) (*Build, error) {
if unknown := reflectutil.UnknownNonEmptyFields(c,
"Context", "Dockerfile", "Args", "Target",
"Context", "Dockerfile", "Args", "CacheFrom", "Target",
); len(unknown) > 0 {
logrus.Warnf("Ignoring: build: %+v", unknown)
}
Expand Down Expand Up @@ -66,6 +66,10 @@ func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName st
}
}

for _, s := range c.CacheFrom {
b.BuildArgs = append(b.BuildArgs, "--cache-from="+s)
}

if c.Target != "" {
b.BuildArgs = append(b.BuildArgs, "--target="+c.Target)
}
Expand Down

0 comments on commit aab5de3

Please sign in to comment.