From c4340ba59a5f38cac3826c43a659c73a150a44c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Ma=C5=82ota-W=C3=B3jcik?= Date: Mon, 19 Aug 2024 11:07:08 +0200 Subject: [PATCH] Build go binaries in docker (#17) --- cmd/builder/build.go | 11 +++++- commands.go | 3 +- pkg/tools/docker/tools.go | 26 +++++++++++++ pkg/tools/golang/golang.go | 76 ++++++++++++++++++++++++++++++++------ pkg/tools/tools.go | 13 ++++--- 5 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 pkg/tools/docker/tools.go diff --git a/cmd/builder/build.go b/cmd/builder/build.go index 466dcdf..1469cab 100644 --- a/cmd/builder/build.go +++ b/cmd/builder/build.go @@ -2,6 +2,7 @@ package main import ( "context" + "path/filepath" "github.com/outofforest/build" @@ -14,10 +15,18 @@ import ( func buildGo(ctx context.Context, deps build.DepsFunc) error { deps(generateGo) - return golang.Build(ctx, deps, golang.BuildConfig{ + if err := golang.Build(ctx, deps, golang.BuildConfig{ Platform: tools.PlatformLocal, PackagePath: "testapps/golang", BinOutputPath: "bin/test-go", + }); err != nil { + return err + } + + return golang.Build(ctx, deps, golang.BuildConfig{ + Platform: tools.PlatformDocker, + PackagePath: "testapps/golang", + BinOutputPath: filepath.Join("bin", tools.PlatformDocker.String(), "test-go"), }) } diff --git a/commands.go b/commands.go index 7c350bd..884b78c 100644 --- a/commands.go +++ b/commands.go @@ -41,7 +41,8 @@ func enter(ctx context.Context, deps build.DepsFunc) error { bash := exec.Command("bash") bash.Env = append(os.Environ(), fmt.Sprintf("PS1=%s", "("+build.GetName(ctx)+`) [\u@\h \W]\$ `), - fmt.Sprintf("PATH=%s:%s", + fmt.Sprintf("PATH=%s:%s:%s", + filepath.Join(lo.Must(filepath.EvalSymlinks(lo.Must(filepath.Abs(".")))), "bin"), filepath.Join(tools.VersionDir(ctx, tools.PlatformLocal), "bin"), os.Getenv("PATH")), ) diff --git a/pkg/tools/docker/tools.go b/pkg/tools/docker/tools.go new file mode 100644 index 0000000..67ae6f0 --- /dev/null +++ b/pkg/tools/docker/tools.go @@ -0,0 +1,26 @@ +package docker + +import ( + "context" + "os/exec" + + "github.com/outofforest/build" + "github.com/pkg/errors" +) + +// AlpineVersion is the version of the alpine docker image. +const AlpineVersion = "3.20" + +// Label used to tag docker resources created by crust. +const ( + LabelKey = "io.seinetwork.build" + LabelValue = "build" +) + +// EnsureDocker verifies that docker is installed. +func EnsureDocker(_ context.Context, _ build.DepsFunc) error { + if _, err := exec.LookPath("docker"); err != nil { + return errors.Wrap(err, "docker command is not available in PATH") + } + return nil +} diff --git a/pkg/tools/golang/golang.go b/pkg/tools/golang/golang.go index 9de78c9..812def4 100644 --- a/pkg/tools/golang/golang.go +++ b/pkg/tools/golang/golang.go @@ -3,6 +3,7 @@ package golang import ( "context" _ "embed" + "fmt" "io/fs" "os" "os/exec" @@ -18,6 +19,7 @@ import ( "github.com/sei-protocol/build/pkg/helpers" "github.com/sei-protocol/build/pkg/tools" + "github.com/sei-protocol/build/pkg/tools/docker" ) const coverageReportDir = "coverage" @@ -43,8 +45,7 @@ type BuildConfig struct { // Build builds go binary. func Build(ctx context.Context, deps build.DepsFunc, config BuildConfig) error { if config.Platform.OS == tools.OSDocker { - return errors.New("building in docker hasn't been implemented yet") - // return buildInDocker(ctx, config) + return buildInDocker(ctx, deps, config) } return buildLocally(ctx, deps, config) } @@ -154,8 +155,7 @@ func buildLocally(ctx context.Context, deps build.DepsFunc, config BuildConfig) config.Platform, tools.PlatformLocal) } - args, envs := buildArgsAndEnvs(ctx, config, filepath.Join(tools.VersionDir(ctx, config.Platform), "lib")) - args = append(args, "-o", lo.Must(filepath.Abs(config.BinOutputPath)), ".") + args, envs := buildArgsAndEnvs(ctx, config) cmd := exec.Command(tools.Bin(ctx, "bin/go", config.Platform), args...) cmd.Dir = config.PackagePath @@ -173,29 +173,83 @@ func buildLocally(ctx context.Context, deps build.DepsFunc, config BuildConfig) return nil } -func buildArgsAndEnvs(ctx context.Context, config BuildConfig, libDir string) (args, envs []string) { +func buildInDocker(ctx context.Context, deps build.DepsFunc, config BuildConfig) error { + deps(docker.EnsureDocker) + + goTool, err := tools.Get(Go) + if err != nil { + return err + } + + image := fmt.Sprintf("golang:%s-alpine%s", goTool.GetVersion(), docker.AlpineVersion) + + srcDir := lo.Must(filepath.EvalSymlinks(lo.Must(filepath.Abs(".")))) + envDir := tools.EnvDir(ctx) + + if err := os.MkdirAll(envDir, 0o755); err != nil { + return errors.WithStack(err) + } + + args, envs := buildArgsAndEnvs(ctx, config) + if err != nil { + return err + } + runArgs := []string{ + "run", "--rm", + "--label", docker.LabelKey + "=" + docker.LabelValue, + "-v", srcDir + ":" + srcDir, + "-v", envDir + ":" + envDir, + "--workdir", filepath.Join(srcDir, config.PackagePath), + "--user", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), + "--name", "sei-build-golang", + } + + for _, env := range envs { + runArgs = append(runArgs, "--env", env) + } + + runArgs = append(runArgs, image, "/usr/local/go/bin/go") + runArgs = append(runArgs, args...) + + cmd := exec.Command("docker", runArgs...) + logger.Get(ctx).Info( + "Building go package in docker", + zap.String("package", config.PackagePath), + zap.String("command", cmd.String()), + ) + if err := libexec.Exec(ctx, cmd); err != nil { + return errors.Wrapf(err, "building package '%s' failed", config.PackagePath) + } + return nil +} + +func buildArgsAndEnvs(ctx context.Context, config BuildConfig) (args, envs []string) { ldFlags := []string{"-w", "-s"} args = []string{ "build", "-trimpath", "-buildvcs=false", - } - if len(ldFlags) != 0 { - args = append(args, "-ldflags="+strings.Join(ldFlags, " ")) + "-ldflags=" + strings.Join(ldFlags, " "), + "-o", lo.Must(filepath.Abs(config.BinOutputPath)), + ".", } if len(config.Tags) != 0 { args = append(args, "-tags="+strings.Join(config.Tags, ",")) } + goOS := config.Platform.OS + if goOS == tools.OSDocker { + goOS = tools.OSLinux + } + cgoEnabled := "0" if config.CGOEnabled { cgoEnabled = "1" } envs = append(env(ctx), - "LIBRARY_PATH="+libDir, "CGO_ENABLED="+cgoEnabled, - "GOOS="+config.Platform.OS, + "GOOS="+goOS, "GOARCH="+config.Platform.Arch, ) @@ -232,7 +286,7 @@ func lintConfigPath(ctx context.Context) string { func env(ctx context.Context) []string { return []string{ - "PATH=" + os.Getenv("PATH"), + "PATH=" + filepath.Join(tools.VersionDir(ctx, tools.PlatformLocal), "bin") + ":" + os.Getenv("PATH"), "GOPATH=" + filepath.Join(tools.DevDir(ctx), "go"), "GOCACHE=" + filepath.Join(tools.DevDir(ctx), "go", "cache", "gobuild"), "GOLANGCI_LINT_CACHE=" + filepath.Join(tools.DevDir(ctx), "go", "cache", "golangci"), diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 3016038..7255b5d 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -265,6 +265,11 @@ func Get(toolName Name) (Tool, error) { return t, nil } +// EnvDir returns the directory where local environment is stored. +func EnvDir(ctx context.Context) string { + return filepath.Join(lo.Must(os.UserCacheDir()), build.GetName(ctx)) +} + // ToolDownloadDir returns directory where tool is downloaded. func ToolDownloadDir(ctx context.Context, platform Platform, tool Tool) string { return filepath.Join(downloadsDir(ctx, platform), string(tool.GetName())+"-"+tool.GetVersion()) @@ -277,7 +282,7 @@ func ToolLinksDir(ctx context.Context, platform Platform, tool Tool) string { // DevDir returns directory where development files are stored. func DevDir(ctx context.Context) string { - return filepath.Join(envDir(ctx), "dev") + return filepath.Join(EnvDir(ctx), "dev") } // ShouldReinstall check if tool should be reinstalled due to missing files or links. @@ -384,12 +389,8 @@ func Checksum(file string) (string, error) { return "sha256:" + hex.EncodeToString(hasher.Sum(nil)), nil } -func envDir(ctx context.Context) string { - return filepath.Join(lo.Must(os.UserCacheDir()), build.GetName(ctx)) -} - func platformDir(ctx context.Context, platform Platform) string { - return filepath.Join(envDir(ctx), platform.String()) + return filepath.Join(EnvDir(ctx), platform.String()) } func downloadsDir(ctx context.Context, platform Platform) string {