buildkit/cmd/buildctl/build.go

203 lines
4.9 KiB
Go

package main
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"os"
"strings"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"golang.org/x/sync/errgroup"
)
var buildCommand = cli.Command{
Name: "build",
Usage: "build",
Action: build,
Flags: []cli.Flag{
cli.StringFlag{
Name: "exporter",
Usage: "Define exporter for build result",
},
cli.StringSliceFlag{
Name: "exporter-opt",
Usage: "Define custom options for exporter",
},
cli.BoolFlag{
Name: "no-progress",
Usage: "Don't show interactive progress",
},
cli.StringFlag{
Name: "trace",
Usage: "Path to trace file. e.g. /dev/null. Defaults to /tmp/buildctlXXXXXXXXX.",
},
cli.StringSliceFlag{
Name: "local",
Usage: "Allow build access to the local directory",
},
cli.StringFlag{
Name: "frontend",
Usage: "Define frontend used for build",
},
cli.StringSliceFlag{
Name: "frontend-opt",
Usage: "Define custom options for frontend",
},
cli.BoolFlag{
Name: "no-cache",
Usage: "Disable cache for all the vertices. Frontend is not supported.",
},
cli.StringFlag{
Name: "export-cache",
Usage: "Reference to export build cache to",
},
cli.StringFlag{
Name: "import-cache",
Usage: "Reference to import build cache from",
},
},
}
func read(r io.Reader, clicontext *cli.Context) (*llb.Definition, error) {
def, err := llb.ReadFrom(r)
if err != nil {
return nil, errors.Wrap(err, "failed to parse input")
}
if clicontext.Bool("no-cache") {
for _, dt := range def.Def {
var op pb.Op
if err := (&op).Unmarshal(dt); err != nil {
return nil, errors.Wrap(err, "failed to parse llb proto op")
}
dgst := digest.FromBytes(dt)
opMetadata, ok := def.Metadata[dgst]
if !ok {
opMetadata = llb.OpMetadata{}
}
opMetadata.IgnoreCache = true
def.Metadata[dgst] = opMetadata
}
}
return def, nil
}
func openTraceFile(clicontext *cli.Context) (*os.File, error) {
if traceFileName := clicontext.String("trace"); traceFileName != "" {
return os.OpenFile(traceFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
}
return ioutil.TempFile("", "buildctl")
}
func build(clicontext *cli.Context) error {
c, err := resolveClient(clicontext)
if err != nil {
return err
}
traceFile, err := openTraceFile(clicontext)
if err != nil {
return err
}
defer traceFile.Close()
traceEnc := json.NewEncoder(traceFile)
logrus.Infof("tracing logs to %s", traceFile.Name())
ch := make(chan *client.SolveStatus)
displayCh := make(chan *client.SolveStatus)
eg, ctx := errgroup.WithContext(appcontext.Context())
exporterAttrs, err := attrMap(clicontext.StringSlice("exporter-opt"))
if err != nil {
return errors.Wrap(err, "invalid exporter-opt")
}
frontendAttrs, err := attrMap(clicontext.StringSlice("frontend-opt"))
if err != nil {
return errors.Wrap(err, "invalid frontend-opt")
}
localDirs, err := attrMap(clicontext.StringSlice("local"))
if err != nil {
return errors.Wrap(err, "invalid local")
}
var def *llb.Definition
if clicontext.String("frontend") == "" {
def, err = read(os.Stdin, clicontext)
if err != nil {
return err
}
} else {
if clicontext.Bool("no-cache") {
return errors.New("no-cache is not supported for frontends")
}
}
eg.Go(func() error {
return c.Solve(ctx, def, client.SolveOpt{
Exporter: clicontext.String("exporter"),
ExporterAttrs: exporterAttrs,
LocalDirs: localDirs,
Frontend: clicontext.String("frontend"),
FrontendAttrs: frontendAttrs,
ExportCache: clicontext.String("export-cache"),
ImportCache: clicontext.String("import-cache"),
}, ch)
})
eg.Go(func() error {
defer close(displayCh)
for s := range ch {
if err := traceEnc.Encode(s); err != nil {
logrus.Error(err)
}
displayCh <- s
}
return nil
})
eg.Go(func() error {
if clicontext.Bool("no-progress") {
for s := range displayCh {
for _, v := range s.Vertexes {
logrus.Debugf("vertex: %s %s %v %v", v.Digest, v.Name, v.Started, v.Completed)
}
for _, s := range s.Statuses {
logrus.Debugf("status: %s %s %d", s.Vertex, s.ID, s.Current)
}
for _, l := range s.Logs {
logrus.Debugf("log: %s\n%s", l.Vertex, l.Data)
}
}
return nil
}
// not using shared context to not disrupt display but let is finish reporting errors
return progressui.DisplaySolveStatus(context.TODO(), displayCh)
})
return eg.Wait()
}
func attrMap(sl []string) (map[string]string, error) {
m := map[string]string{}
for _, v := range sl {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 {
return nil, errors.Errorf("invalid value %s", v)
}
m[parts[0]] = parts[1]
}
return m, nil
}