mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
273 lines
6.6 KiB
Go
273 lines
6.6 KiB
Go
package buildkit
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/moby/buildkit/client"
|
|
_ "github.com/moby/buildkit/client/connhelper/dockercontainer"
|
|
_ "github.com/moby/buildkit/client/connhelper/nerdctlcontainer"
|
|
"github.com/moby/buildkit/client/llb"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/session/secrets/secretsprovider"
|
|
"github.com/moby/buildkit/util/appcontext"
|
|
_ "github.com/moby/buildkit/util/grpcutil/encoding/proto"
|
|
"github.com/moby/buildkit/util/progress/progressui"
|
|
"github.com/railwayapp/railpack/core/plan"
|
|
"github.com/tonistiigi/fsutil"
|
|
)
|
|
|
|
type BuildWithBuildkitClientOptions struct {
|
|
ImageName string
|
|
DumpLLB bool
|
|
OutputDir string
|
|
ProgressMode string
|
|
SecretsHash string
|
|
Secrets map[string]string
|
|
Platform BuildPlatform
|
|
ImportCache string
|
|
ExportCache string
|
|
CacheKey string
|
|
}
|
|
|
|
func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWithBuildkitClientOptions) error {
|
|
ctx := appcontext.Context()
|
|
|
|
imageName := opts.ImageName
|
|
if imageName == "" {
|
|
imageName = getImageName(appDir)
|
|
}
|
|
|
|
buildkitHost := os.Getenv("BUILDKIT_HOST")
|
|
if buildkitHost == "" {
|
|
log.Error("BUILDKIT_HOST environment variable is not set")
|
|
return fmt.Errorf("BUILDKIT_HOST environment variable is not set")
|
|
}
|
|
|
|
log.Debugf("Connecting to buildkit host: %s", buildkitHost)
|
|
|
|
c, err := client.New(ctx, buildkitHost)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to buildkit: %w", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
// Get the buildkit info early so we can ensure we can connect to the buildkit host
|
|
info, err := c.Info(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get buildkit info: %w", err)
|
|
}
|
|
|
|
buildPlatform := opts.Platform
|
|
if (buildPlatform == BuildPlatform{}) {
|
|
buildPlatform = DetermineBuildPlatformFromHost()
|
|
}
|
|
|
|
llbState, image, err := ConvertPlanToLLB(plan, ConvertPlanOptions{
|
|
BuildPlatform: buildPlatform,
|
|
SecretsHash: opts.SecretsHash,
|
|
CacheKey: opts.CacheKey,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error converting plan to LLB: %w", err)
|
|
}
|
|
|
|
imageBytes, err := json.Marshal(image)
|
|
if err != nil {
|
|
return fmt.Errorf("error marshalling image: %w", err)
|
|
}
|
|
|
|
def, err := llbState.Marshal(ctx, llb.LinuxAmd64)
|
|
if err != nil {
|
|
return fmt.Errorf("error marshaling LLB state: %w", err)
|
|
}
|
|
|
|
if opts.DumpLLB {
|
|
log.Info("Dumping LLB to stdout")
|
|
err = llb.WriteTo(def, os.Stdout)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing LLB definition: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
ch := make(chan *client.SolveStatus)
|
|
|
|
var pipeR *io.PipeReader
|
|
var pipeW *io.PipeWriter
|
|
errCh := make(chan error, 1)
|
|
|
|
// Only set up pipe and docker load if we're not saving to a directory
|
|
if opts.OutputDir == "" {
|
|
// Create a pipe to connect buildkit output to docker load
|
|
pipeR, pipeW = io.Pipe()
|
|
defer pipeR.Close()
|
|
|
|
// Pipe the image into `docker load`
|
|
go func() {
|
|
cmd := exec.Command("docker", "load")
|
|
cmd.Stdin = pipeR
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
errCh <- cmd.Run()
|
|
}()
|
|
}
|
|
|
|
progressDone := make(chan bool)
|
|
go func() {
|
|
displayCh := make(chan *client.SolveStatus)
|
|
go func() {
|
|
for s := range ch {
|
|
displayCh <- s
|
|
}
|
|
close(displayCh)
|
|
}()
|
|
|
|
progressMode := progressui.AutoMode
|
|
if opts.ProgressMode == "plain" {
|
|
progressMode = progressui.PlainMode
|
|
} else if opts.ProgressMode == "tty" {
|
|
progressMode = progressui.TtyMode
|
|
}
|
|
|
|
display, err := progressui.NewDisplay(os.Stdout, progressMode)
|
|
if err != nil {
|
|
log.Error("failed to create progress display", "error", err)
|
|
}
|
|
|
|
_, err = display.UpdateFrom(ctx, displayCh)
|
|
if err != nil {
|
|
log.Error("failed to update progress display", "error", err)
|
|
}
|
|
progressDone <- true
|
|
}()
|
|
|
|
appFS, err := fsutil.NewFS(appDir)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating FS: %w", err)
|
|
}
|
|
|
|
log.Debugf("Building image for %s with BuildKit %s", buildPlatform.String(), info.BuildkitVersion.Version)
|
|
|
|
secretsMap := make(map[string][]byte)
|
|
for k, v := range opts.Secrets {
|
|
secretsMap[k] = []byte(v)
|
|
}
|
|
secrets := secretsprovider.FromMap(secretsMap)
|
|
|
|
solveOpts := client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
"context": appFS,
|
|
},
|
|
Session: []session.Attachable{secrets},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterDocker,
|
|
Attrs: map[string]string{
|
|
"name": imageName,
|
|
"containerimage.config": string(imageBytes),
|
|
},
|
|
Output: func(_ map[string]string) (io.WriteCloser, error) {
|
|
return pipeW, nil
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Add cache import if specified
|
|
if opts.ImportCache != "" {
|
|
solveOpts.CacheImports = append(solveOpts.CacheImports, client.CacheOptionsEntry{
|
|
Type: "gha",
|
|
Attrs: parseKeyValue(opts.ImportCache),
|
|
})
|
|
}
|
|
|
|
// Add cache export if specified
|
|
if opts.ExportCache != "" {
|
|
solveOpts.CacheExports = append(solveOpts.CacheExports, client.CacheOptionsEntry{
|
|
Type: "gha",
|
|
Attrs: parseKeyValue(opts.ExportCache),
|
|
})
|
|
}
|
|
|
|
// Save the resulting filesystem to a directory
|
|
if opts.OutputDir != "" {
|
|
err = os.MkdirAll(opts.OutputDir, 0755)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating output directory: %w", err)
|
|
}
|
|
|
|
solveOpts = client.SolveOpt{
|
|
LocalMounts: map[string]fsutil.FS{
|
|
"context": appFS,
|
|
},
|
|
Exports: []client.ExportEntry{
|
|
{
|
|
Type: client.ExporterLocal,
|
|
OutputDir: opts.OutputDir,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
startTime := time.Now()
|
|
_, err = c.Solve(ctx, def, solveOpts, ch)
|
|
|
|
// Wait for progress monitoring to complete
|
|
<-progressDone
|
|
|
|
if pipeW != nil {
|
|
pipeW.Close()
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to solve: %w", err)
|
|
}
|
|
|
|
// Only wait for docker load if we used it
|
|
if opts.OutputDir == "" {
|
|
if err := <-errCh; err != nil {
|
|
return fmt.Errorf("docker load failed: %w", err)
|
|
}
|
|
}
|
|
|
|
buildDuration := time.Since(startTime)
|
|
log.Infof("Successfully built image in %.2fs", buildDuration.Seconds())
|
|
|
|
if opts.OutputDir != "" {
|
|
log.Infof("Saved image filesystem to directory `%s`", opts.OutputDir)
|
|
} else {
|
|
log.Infof("Run with `docker run -it %s`", imageName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getImageName(appDir string) string {
|
|
parts := strings.Split(appDir, string(os.PathSeparator))
|
|
name := parts[len(parts)-1]
|
|
if name == "" {
|
|
name = "railpack-app" // Fallback if path ends in separator
|
|
}
|
|
return name
|
|
}
|
|
|
|
// Helper function to parse key=value strings into a map
|
|
func parseKeyValue(s string) map[string]string {
|
|
attrs := make(map[string]string)
|
|
parts := strings.Split(s, ",")
|
|
for _, part := range parts {
|
|
kv := strings.SplitN(part, "=", 2)
|
|
if len(kv) == 2 {
|
|
attrs[kv[0]] = kv[1]
|
|
}
|
|
}
|
|
return attrs
|
|
}
|