mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
feat: detect shell type using shebang (#327)
* build: adding shell parsing package * feat: change shell provider start command based on shebang * test: test shell type selection in shell provider * test: update snapshot
This commit is contained in:
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"deploy": {
|
||||
"base": {
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:latest"
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "bash start.sh"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"commands": [
|
||||
{
|
||||
"cmd": "chmod +x start.sh"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"image": "ghcr.io/railwayapp/railpack-runtime:latest"
|
||||
},
|
||||
{
|
||||
"include": [
|
||||
"."
|
||||
],
|
||||
"local": true
|
||||
}
|
||||
],
|
||||
"name": "build",
|
||||
"secrets": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
"step": "build"
|
||||
}
|
||||
],
|
||||
"startCommand": "sh start.sh"
|
||||
"startCommand": "bash start.sh"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/railwayapp/railpack/core/generate"
|
||||
"github.com/railwayapp/railpack/core/plan"
|
||||
"mvdan.cc/sh/v3/fileutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,9 +35,15 @@ func (p *ShellProvider) Initialize(ctx *generate.GenerateContext) error {
|
||||
}
|
||||
|
||||
func (p *ShellProvider) Plan(ctx *generate.GenerateContext) error {
|
||||
ctx.Deploy.StartCmd = "sh " + p.scriptName
|
||||
interpreter, err := detectShellInterpreter(ctx, p.scriptName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Logger.LogInfo("Using shell script: %s", p.scriptName)
|
||||
ctx.Deploy.StartCmd = interpreter + " " + p.scriptName
|
||||
ctx.Metadata.Set("detectedShellInterpreter", interpreter)
|
||||
|
||||
ctx.Logger.LogInfo("Using shell script: %s with interpreter: %s", p.scriptName, interpreter)
|
||||
|
||||
build := ctx.NewCommandStep("build")
|
||||
build.AddInput(plan.NewImageLayer(plan.RailpackRuntimeImage))
|
||||
@@ -79,3 +86,32 @@ func getScript(ctx *generate.GenerateContext) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func detectShellInterpreter(ctx *generate.GenerateContext, scriptName string) (string, error) {
|
||||
content, err := ctx.App.ReadFile(scriptName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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 "sh", "dash":
|
||||
return "sh"
|
||||
case "mksh", "zsh", "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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/railwayapp/railpack/core/app"
|
||||
@@ -122,3 +124,61 @@ func TestGetScript(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectShellInterpreter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scriptContent string
|
||||
wantInterpreter string
|
||||
}{
|
||||
{
|
||||
name: "bash shebang",
|
||||
scriptContent: "#!/bin/bash\necho 'hello'",
|
||||
wantInterpreter: "bash",
|
||||
},
|
||||
{
|
||||
name: "bash with env",
|
||||
scriptContent: "#!/usr/bin/env bash\necho 'hello'",
|
||||
wantInterpreter: "bash",
|
||||
},
|
||||
{
|
||||
name: "sh shebang",
|
||||
scriptContent: "#!/bin/sh\necho 'hello'",
|
||||
wantInterpreter: "sh",
|
||||
},
|
||||
{
|
||||
name: "dash shebang",
|
||||
scriptContent: "#!/bin/dash\necho 'hello'",
|
||||
wantInterpreter: "sh",
|
||||
},
|
||||
{
|
||||
name: "zsh shebang (unsupported)",
|
||||
scriptContent: "#!/bin/zsh\necho 'hello'",
|
||||
wantInterpreter: "bash",
|
||||
},
|
||||
{
|
||||
name: "no shebang",
|
||||
scriptContent: "echo 'hello'",
|
||||
wantInterpreter: "sh",
|
||||
},
|
||||
{
|
||||
name: "empty file",
|
||||
scriptContent: "",
|
||||
wantInterpreter: "sh",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "test.sh")
|
||||
err := os.WriteFile(scriptPath, []byte(tt.scriptContent), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := testingUtils.CreateGenerateContext(t, tmpDir)
|
||||
got, err := detectShellInterpreter(ctx, "test.sh")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantInterpreter, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Intentionally using a bash specific array syntax to ensure the correct shell is picked.
|
||||
|
||||
fruits=("apple" "banana" "cherry")
|
||||
|
||||
echo "Bash array test:"
|
||||
for fruit in "${fruits[@]}"; do
|
||||
echo "- $fruit"
|
||||
done
|
||||
|
||||
echo "Array length: ${#fruits[@]}"
|
||||
echo "Test completed successfully"
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"expectedOutput": [
|
||||
"Bash array test:",
|
||||
"- apple",
|
||||
"- banana",
|
||||
"- cherry",
|
||||
"Array length: 3",
|
||||
"Test completed successfully"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/zsh
|
||||
|
||||
# Output the container architecture
|
||||
uname -m
|
||||
@@ -11,10 +11,11 @@ require (
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/containerd/platforms v1.0.0-rc.1
|
||||
github.com/gkampitakis/go-snaps v0.5.9
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/invopop/jsonschema v0.13.0
|
||||
github.com/moby/buildkit v0.19.0
|
||||
github.com/moby/patternmatcher v0.6.0
|
||||
github.com/muesli/termenv v0.15.2
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -24,6 +25,7 @@ require (
|
||||
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
mvdan.cc/sh/v3 v3.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -70,14 +72,13 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.1 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
@@ -102,7 +103,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect
|
||||
|
||||
@@ -102,8 +102,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -185,8 +185,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs=
|
||||
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
|
||||
@@ -282,10 +282,10 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
@@ -319,6 +319,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI=
|
||||
mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc=
|
||||
|
||||
Reference in New Issue
Block a user