Files
pack/core/providers/shell/shell.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

150 lines
4.0 KiB
Go

package shell
import (
"errors"
"github.com/hanzoai/pack/core/generate"
"github.com/hanzoai/pack/core/plan"
"mvdan.cc/sh/v3/fileutil"
)
const (
StartScriptName = "start.sh"
)
type ShellProvider struct {
scriptName string
}
func (p *ShellProvider) Name() string {
return "shell"
}
func (p *ShellProvider) Detect(ctx *generate.GenerateContext) (bool, error) {
return getScript(ctx) != "", nil
}
func (p *ShellProvider) Initialize(ctx *generate.GenerateContext) error {
p.scriptName = getScript(ctx)
if p.scriptName == "" {
return errors.New("start shell script could not be found")
}
return nil
}
func (p *ShellProvider) Plan(ctx *generate.GenerateContext) error {
interpreter, err := detectShellInterpreter(ctx, p.scriptName)
if err != nil {
return err
}
ctx.Deploy.StartCmd = interpreter + " " + p.scriptName
ctx.Metadata.Set("detectedShellInterpreter", interpreter)
// zsh is not included in the base image by default, but it's common enough that we should support it
if interpreter == "zsh" {
ctx.Logger.LogInfo("Installing zsh for shell script execution")
ctx.Deploy.AddAptPackages([]string{"zsh"})
}
ctx.Logger.LogInfo("Using shell script: %s with interpreter: %s", p.scriptName, interpreter)
miseStepName := ctx.GetMiseStepBuilder().Name()
var buildBaseLayer plan.Layer
// If install step is configured (e.g. via RAILPACK_INSTALL_CMD) we add it so the user-supplied install config is run properly
if _, ok := ctx.Config.Steps["install"]; ok {
install := ctx.NewCommandStep("install")
// Install step needs mise base to access any tools installed in the mise step (e.g. via RAILPACK_PACKAGES)
install.AddInput(plan.NewStepLayer(miseStepName))
install.AddInput(plan.NewLocalLayer())
// If we have an install step, the build step should be based on the result of the install step
// so that artifacts from the install step are available during the build.
buildBaseLayer = plan.NewStepLayer(install.Name())
} else {
// If no install step is configured, the build step is based directly on the mise step.
buildBaseLayer = plan.NewStepLayer(miseStepName)
}
build := ctx.NewCommandStep("build")
build.AddInput(buildBaseLayer)
build.AddInput(plan.NewLocalLayer())
build.AddCommands(
[]plan.Command{
plan.NewExecCommand("chmod +x " + p.scriptName),
},
)
ctx.Deploy.AddInputs([]plan.Layer{
ctx.GetMiseStepBuilder().GetLayer(),
plan.NewStepLayer(build.Name(), plan.Filter{
Include: []string{"."},
}),
})
return nil
}
func (p *ShellProvider) CleansePlan(buildPlan *plan.BuildPlan) {}
func (p *ShellProvider) StartCommandHelp() string {
return ""
}
// determine shell script to use for container start
func getScript(ctx *generate.GenerateContext) string {
scriptName, envVarName := ctx.Env.GetConfigVariable("SHELL_SCRIPT")
if scriptName == "" {
scriptName = StartScriptName
}
if ctx.App.HasFile(scriptName) {
return scriptName
}
if envVarName != "" {
ctx.Logger.LogWarn("%s %s script not found", envVarName, scriptName)
} else {
ctx.Logger.LogWarn("script %s not found", scriptName)
}
return ""
}
func detectShellInterpreter(ctx *generate.GenerateContext, scriptName string) (string, error) {
content, err := ctx.App.ReadFile(scriptName)
if err != nil {
return "", err
}
// fileutil.Shebang only recognizes POSIX-compliant shells (bash, sh, zsh, etc).
// Non-POSIX shells like fish are not detected and will return empty string.
// TODO in the future, we should add config for forcing a specific shell interpreter.
interpreter := fileutil.Shebang([]byte(content))
if interpreter == "" {
return "sh", nil
}
return mapToAvailableShell(ctx, interpreter), nil
}
func mapToAvailableShell(ctx *generate.GenerateContext, shell string) string {
switch shell {
case "bash":
return "bash"
case "zsh":
return "zsh"
case "sh", "dash":
return "sh"
case "mksh", "ksh", "fish":
ctx.Logger.LogWarn("Shell '%s' not available in runtime, using 'bash'", shell)
return "bash"
default:
ctx.Logger.LogWarn("Unknown shell '%s', using 'sh'", shell)
return "sh"
}
}