package build
import (
"context"
"io"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/containerd/containerd/platforms"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
specs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/tonistiigi/buildx/driver"
"github.com/tonistiigi/buildx/util/progress"
"golang.org/x/sync/errgroup"
)
type Options struct {
Inputs Inputs
Tags [ ] string
Labels map [ string ] string
BuildArgs map [ string ] string
Pull bool
NoCache bool
Target string
Platforms [ ] specs . Platform
Exports [ ] client . ExportEntry
Session [ ] session . Attachable
// DockerTarget
}
type Inputs struct {
ContextPath string
DockerfilePath string
InStream io . Reader
}
type DriverInfo struct {
Driver driver . Driver
Name string
Platform [ ] string // TODO: specs.Platform
Err error
}
func getFirstDriver ( drivers [ ] DriverInfo ) ( driver . Driver , error ) {
err := errors . Errorf ( "no drivers found" )
for _ , di := range drivers {
if di . Driver != nil {
return di . Driver , nil
}
if di . Err != nil {
err = di . Err
}
}
return nil , err
}
func Build ( ctx context . Context , drivers [ ] DriverInfo , opt map [ string ] Options , pw progress . Writer ) ( map [ string ] * client . SolveResponse , error ) {
if len ( drivers ) == 0 {
return nil , errors . Errorf ( "driver required for build" )
}
if len ( drivers ) > 1 {
return nil , errors . Errorf ( "multiple drivers currently not supported" )
}
pwOld := pw
d , err := getFirstDriver ( drivers )
if err != nil {
return nil , err
}
_ , isDefaultMobyDriver := d . ( interface {
IsDefaultMobyDriver ( )
} )
c , pw , err := driver . Boot ( ctx , d , pw )
if err != nil {
close ( pwOld . Status ( ) )
<- pwOld . Done ( )
return nil , err
}
withPrefix := len ( opt ) > 1
mw := progress . NewMultiWriter ( pw )
eg , ctx := errgroup . WithContext ( ctx )
resp := map [ string ] * client . SolveResponse { }
var mu sync . Mutex
for k , opt := range opt {
pw := mw . WithPrefix ( k , withPrefix )
so := client . SolveOpt {
Frontend : "dockerfile.v0" ,
FrontendAttrs : map [ string ] string { } ,
}
switch len ( opt . Exports ) {
case 1 :
// valid
case 0 :
if isDefaultMobyDriver {
// backwards compat for docker driver only:
// this ensures the build results in a docker image.
opt . Exports = [ ] client . ExportEntry { { Type : "image" , Attrs : map [ string ] string { } } }
}
default :
return nil , errors . Errorf ( "multiple outputs currently unsupported" )
}
if len ( opt . Tags ) > 0 {
for i , e := range opt . Exports {
switch e . Type {
case "image" , "oci" , "docker" :
opt . Exports [ i ] . Attrs [ "name" ] = strings . Join ( opt . Tags , "," )
}
}
} else {
for _ , e := range opt . Exports {
if e . Type == "image" && e . Attrs [ "name" ] == "" && e . Attrs [ "push" ] != "" {
if ok , _ := strconv . ParseBool ( e . Attrs [ "push" ] ) ; ok {
return nil , errors . Errorf ( "tag is needed when pushing to registry" )
}
}
}
}
for i , e := range opt . Exports {
if e . Type == "oci" && ! d . Features ( ) [ driver . OCIExporter ] {
return nil , notSupported ( d , driver . OCIExporter )
}
if e . Type == "docker" {
if e . Output == nil {
if ! isDefaultMobyDriver {
return nil , errors . Errorf ( "loading to docker currently not implemented, specify dest file or -" )
}
e . Type = "image"
} else if ! d . Features ( ) [ driver . DockerExporter ] {
return nil , notSupported ( d , driver . DockerExporter )
}
}
if e . Type == "image" && isDefaultMobyDriver {
opt . Exports [ i ] . Type = "moby"
if e . Attrs [ "push" ] != "" {
if ok , _ := strconv . ParseBool ( e . Attrs [ "push" ] ) ; ok {
return nil , errors . Errorf ( "auto-push is currently not implemented for docker driver" )
}
}
}
}
// TODO: handle loading to docker daemon
so . Exports = opt . Exports
so . Session = opt . Session
if err := LoadInputs ( opt . Inputs , & so ) ; err != nil {
return nil , err
}
if opt . Pull {
so . FrontendAttrs [ "image-resolve-mode" ] = "pull"
}
if opt . Target != "" {
so . FrontendAttrs [ "target" ] = opt . Target
}
if opt . NoCache {
so . FrontendAttrs [ "no-cache" ] = ""
}
for k , v := range opt . BuildArgs {
so . FrontendAttrs [ "build-arg:" + k ] = v
}
for k , v := range opt . Labels {
so . FrontendAttrs [ "label:" + k ] = v
}
if len ( opt . Platforms ) != 0 {
pp := make ( [ ] string , len ( opt . Platforms ) )
for i , p := range opt . Platforms {
pp [ i ] = platforms . Format ( p )
}
if len ( pp ) > 1 && ! d . Features ( ) [ driver . MultiPlatform ] {
return nil , notSupported ( d , driver . MultiPlatform )
}
so . FrontendAttrs [ "platform" ] = strings . Join ( pp , "," )
}
var statusCh chan * client . SolveStatus
if pw != nil {
statusCh = pw . Status ( )
eg . Go ( func ( ) error {
<- pw . Done ( )
return pw . Err ( )
} )
}
eg . Go ( func ( ) error {
rr , err := c . Solve ( ctx , nil , so , statusCh )
if err != nil {
return err
}
mu . Lock ( )
resp [ k ] = rr
mu . Unlock ( )
return nil
} )
}
if err := eg . Wait ( ) ; err != nil {
return nil , err
}
return resp , nil
}
func LoadInputs ( inp Inputs , target * client . SolveOpt ) error {
if inp . ContextPath == "" {
return errors . New ( "please specify build context (e.g. \".\" for the current directory)" )
}
// TODO: handle stdin, symlinks, remote contexts, check files exist
if inp . DockerfilePath == "" {
inp . DockerfilePath = filepath . Join ( inp . ContextPath , "Dockerfile" )
}
if target . LocalDirs == nil {
target . LocalDirs = map [ string ] string { }
}
target . LocalDirs [ "context" ] = inp . ContextPath
target . LocalDirs [ "dockerfile" ] = filepath . Dir ( inp . DockerfilePath )
if target . FrontendAttrs == nil {
target . FrontendAttrs = map [ string ] string { }
}
target . FrontendAttrs [ "filename" ] = filepath . Base ( inp . DockerfilePath )
return nil
}
func notSupported ( d driver . Driver , f driver . Feature ) error {
return errors . Errorf ( "%s feature is currently not supported for %s driver. Please switch to a different driver (eg. \"docker buildx new\")" , f , d . Factory ( ) . Name ( ) )
}