Files
pack/core/providers/golang/golang.go
T
zeekay 7844a60aff refactor(pack): modernize to github.com/hanzoai/pack and remove upstream branding
- Migrate module path to github.com/hanzoai/pack across all Go packages
- Remove legacy upstream docs and competitor brand references
- Modernize LLM.md and mise.toml for pristine Hanzo Pack builder
2026-09-04 00:40:42 -07:00

272 lines
7.9 KiB
Go

package golang
import (
"fmt"
"path/filepath"
"strings"
"github.com/hanzoai/pack/core/generate"
"github.com/hanzoai/pack/core/plan"
)
const (
DEFAULT_GO_VERSION = "1.25"
GO_BUILD_CACHE_KEY = "go-build"
GO_BINARY_NAME = "out"
GO_PATH = "/go"
)
type GoProvider struct{}
func (p *GoProvider) Name() string {
return "golang"
}
func (p *GoProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
return p.isGoMod(ctx) || p.isGoWorkspace(ctx) || ctx.App.HasFile("main.go"), nil
}
func (p *GoProvider) Initialize(ctx *generate.GenerateContext) error {
return nil
}
func (p *GoProvider) Plan(ctx *generate.GenerateContext) error {
builder := p.GetBuilder(ctx)
p.InstallGoPackages(ctx, builder)
install := ctx.NewCommandStep("install")
install.AddInput(plan.NewStepLayer(builder.Name()))
p.InstallGoDeps(ctx, install)
build := ctx.NewCommandStep("build")
build.AddInput(plan.NewStepLayer(install.Name()))
p.Build(ctx, build)
ctx.Deploy.StartCmd = fmt.Sprintf("./%s", GO_BINARY_NAME)
if p.hasCGOEnabled(ctx) {
ctx.Logger.LogInfo("CGO is enabled")
ctx.Deploy.AddAptPackages([]string{"libc6"})
}
ctx.Deploy.AddInputs([]plan.Layer{
plan.NewStepLayer(build.Name(), plan.Filter{
Include: []string{"."},
}),
})
p.addMetadata(ctx)
return nil
}
func (p *GoProvider) Build(ctx *generate.GenerateContext, build *generate.CommandStepBuilder) {
var buildCmd string
flags := "-w -s"
baseBuildCmd := fmt.Sprintf("go build -ldflags=\"%s\" -o %s", flags, GO_BINARY_NAME)
if modulePath, _ := ctx.Env.GetConfigVariable("GO_WORKSPACE_MODULE"); modulePath != "" {
// Use the provided env var path to build the specified module
ctx.Logger.LogInfo("Building workspace module: %s", modulePath)
buildCmd = fmt.Sprintf("%s ./%s", baseBuildCmd, modulePath)
} else if binName, _ := ctx.Env.GetConfigVariable("GO_BIN"); binName != "" {
// Use the provided env var path to build the specified command
ctx.Logger.LogInfo("Building bin: %s", binName)
buildCmd = fmt.Sprintf("%s ./cmd/%s", baseBuildCmd, binName)
} else if p.isGoMod(ctx) && p.hasRootGoFiles(ctx) {
// Use the default build command if there are root go files
buildCmd = baseBuildCmd
} else if dirs, err := ctx.App.FindDirectories("cmd/*"); err == nil && len(dirs) > 0 {
// Try to find a command in the cmd directory if no other build command is specified
cmdName := filepath.Base(dirs[0])
ctx.Logger.LogInfo("Building command: %s", cmdName)
buildCmd = fmt.Sprintf("%s ./cmd/%s", baseBuildCmd, cmdName)
} else if p.isGoMod(ctx) {
// Use the default build command if there are no root go files
buildCmd = baseBuildCmd
} else if p.isGoWorkspace(ctx) {
// For workspaces without explicit module selection, try to find a module with main package
packages := p.GoWorkspacePackages(ctx)
for _, pkg := range packages {
if ctx.App.HasFile(filepath.Join(pkg, "main.go")) {
ctx.Logger.LogInfo("Building workspace module: %s", pkg)
buildCmd = fmt.Sprintf("%s ./%s", baseBuildCmd, pkg)
break
}
}
} else if ctx.App.HasFile("main.go") {
// Fallback to building the main package if no other build command is specified
buildCmd = fmt.Sprintf("%s main.go", baseBuildCmd)
}
build.AddInput(plan.NewLocalLayer())
if buildCmd == "" {
return
}
build.AddCache(p.goBuildCache(ctx))
build.AddCommands([]plan.Command{
plan.NewExecCommand(buildCmd),
})
}
func (p *GoProvider) InstallGoDeps(ctx *generate.GenerateContext, install *generate.CommandStepBuilder) {
install.AddEnvVars(map[string]string{
"GOPATH": GO_PATH,
"GOBIN": fmt.Sprintf("%s/bin", GO_PATH),
})
install.AddCommands([]plan.Command{
plan.NewPathCommand(fmt.Sprintf("%s/bin", GO_PATH)),
})
if !p.isGoMod(ctx) && !p.isGoWorkspace(ctx) {
return
}
install.AddCache(p.goBuildCache(ctx))
if p.isGoMod(ctx) {
install.AddCommand(plan.NewCopyCommand("go.mod"))
if ctx.App.HasFile("go.sum") {
install.AddCommand(plan.NewCopyCommand("go.sum"))
}
}
if p.isGoWorkspace(ctx) {
install.AddCommand(plan.NewCopyCommand("go.work"))
if ctx.App.HasFile("go.work.sum") {
install.AddCommand(plan.NewCopyCommand("go.work.sum"))
}
}
workspacePackages := p.GoWorkspacePackages(ctx)
for _, pkgPath := range workspacePackages {
install.AddCommand(plan.NewCopyCommand(filepath.Join(pkgPath, "go.mod")))
if ctx.App.HasFile(filepath.Join(pkgPath, "go.sum")) {
install.AddCommand(plan.NewCopyCommand(filepath.Join(pkgPath, "go.sum")))
}
}
install.AddCommand(plan.NewExecCommand("go mod download"))
ctx.Logger.LogInfo("Using go mod")
if !p.hasCGOEnabled(ctx) {
install.AddEnvVars(map[string]string{"CGO_ENABLED": "0"})
}
}
func (p *GoProvider) extractGoVersionFromMod(ctx *generate.GenerateContext) string {
if goModContents, err := ctx.App.ReadFile("go.mod"); err == nil {
// Split content into lines and look for "go X.XX" line
lines := strings.SplitSeq(string(goModContents), "\n")
for line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "go ") {
// Extract version number
if goVersion := strings.TrimSpace(strings.TrimPrefix(line, "go")); goVersion != "" {
return goVersion
}
}
}
}
return ""
}
func (p *GoProvider) InstallGoPackages(ctx *generate.GenerateContext, miseStep *generate.MiseStepBuilder) {
goPkg := miseStep.Default("go", DEFAULT_GO_VERSION)
if goVersion := p.extractGoVersionFromMod(ctx); goVersion != "" {
miseStep.Version(goPkg, goVersion, "go.mod")
}
if envVersion, varName := ctx.Env.GetConfigVariable("GO_VERSION"); envVersion != "" {
miseStep.Version(goPkg, envVersion, varName)
}
miseStep.UseMiseVersions(ctx, []string{"go"})
}
func (p *GoProvider) GetBuilder(ctx *generate.GenerateContext) *generate.MiseStepBuilder {
miseStep := ctx.GetMiseStepBuilder()
if p.hasCGOEnabled(ctx) {
miseStep.SupportingAptPackages = append(miseStep.SupportingAptPackages, "gcc", "g++", "libc6-dev")
}
return miseStep
}
func (p *GoProvider) addMetadata(ctx *generate.GenerateContext) {
ctx.Metadata.SetBool("goMod", p.isGoMod(ctx))
ctx.Metadata.SetBool("goWorkspace", p.isGoWorkspace(ctx))
ctx.Metadata.SetBool("goRootFile", p.hasRootGoFiles(ctx))
ctx.Metadata.SetBool("goGin", p.isGin(ctx))
ctx.Metadata.SetBool("goCGO", p.hasCGOEnabled(ctx))
}
func (p *GoProvider) goBuildCache(ctx *generate.GenerateContext) string {
return ctx.Caches.AddCache(GO_BUILD_CACHE_KEY, "/root/.cache/go-build")
}
func (p *GoProvider) hasRootGoFiles(ctx *generate.GenerateContext) bool {
if files, err := ctx.App.FindFiles("*.go"); err == nil {
for _, file := range files {
if filepath.Dir(file) == "." {
return true
}
}
}
return false
}
func (p *GoProvider) isGin(ctx *generate.GenerateContext) bool {
if goModContents, err := ctx.App.ReadFile("go.mod"); err == nil {
return strings.Contains(string(goModContents), "github.com/gin-gonic/gin")
}
return false
}
func (p *GoProvider) hasCGOEnabled(ctx *generate.GenerateContext) bool {
return ctx.Env.GetVariable("CGO_ENABLED") == "1"
}
func (p *GoProvider) isGoMod(ctx *generate.GenerateContext) bool {
return ctx.App.HasFile("go.mod")
}
func (p *GoProvider) GoWorkspacePackages(ctx *generate.GenerateContext) []string {
var packages []string
goModFiles, err := ctx.App.FindFiles("**/go.mod")
if err != nil {
return packages
}
for _, modFile := range goModFiles {
if modFile == "go.mod" {
continue
}
dir := filepath.Dir(modFile)
packages = append(packages, dir)
}
return packages
}
func (p *GoProvider) isGoWorkspace(ctx *generate.GenerateContext) bool {
return ctx.App.HasFile("go.work")
}
func (p *GoProvider) CleansePlan(buildPlan *plan.BuildPlan) {}
func (p *GoProvider) StartCommandHelp() string {
return "To configure your start command, Railpack will check:\n\n" +
"1. Create a main.go file in your project root\n\n" +
"2. Create a command in the cmd directory (e.g., cmd/server/main.go)\n\n" +
"3. Set the GO_BIN environment variable to specify which command to build\n\n" +
"4. For workspaces: Set GO_WORKSPACE_MODULE to build a specific module (e.g., GO_WORKSPACE_MODULE=api)"
}