From f07b083d71f2e72eb58037644a879b6792c7f84a Mon Sep 17 00:00:00 2001 From: Ryan Moran Date: Tue, 17 Mar 2026 07:20:47 -0700 Subject: [PATCH] Resolve relative host paths in volume mount specs Resolves relative host paths in --volume specs to absolute paths based on the invoking working directory. --- .gitignore | 1 + internal/config.go | 26 ++++++++++++++++++++ internal/config_test.go | 54 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af0c92a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.agents/ diff --git a/internal/config.go b/internal/config.go index 3268976..11f841d 100644 --- a/internal/config.go +++ b/internal/config.go @@ -3,6 +3,7 @@ package internal import ( "fmt" "os/exec" + "path/filepath" goruntime "runtime" "strings" "time" @@ -68,6 +69,9 @@ func ParseConfig(args []string, environment []string, startDir string) (Config, // Build volumes with defaults (runtime-aware) volumes := buildVolumes(cfg.Volumes, rt) + // Resolve relative host paths in volumes to absolute paths + volumes = resolveVolumePaths(volumes, startDir) + return Config{ Runtime: rt, ImageName: ImageName(cfg.Image), @@ -119,6 +123,28 @@ func buildVolumes(configVolumes []string, rt string) []string { } } +// resolveVolumePaths resolves relative host paths in volume mount specs to absolute paths. +// Volume specs have the format [host-path:]container-path[:options]. +// Host paths starting with "." are treated as relative and resolved against baseDir. +// Named volumes (no leading "/" or ".") and container-only specs are left unchanged. +func resolveVolumePaths(volumes []string, baseDir string) []string { + resolved := make([]string, len(volumes)) + for i, volume := range volumes { + hostPath, rest, hasColon := strings.Cut(volume, ":") + if !hasColon || !strings.HasPrefix(hostPath, ".") { + resolved[i] = volume + continue + } + absPath, err := filepath.Abs(filepath.Join(baseDir, hostPath)) + if err != nil { + resolved[i] = volume + continue + } + resolved[i] = absPath + ":" + rest + } + return resolved +} + // buildEnvironment constructs the environment variable list with runtime-aware defaults func buildEnvironment(environment []string, configEnv map[string]string, rt string) []string { lookup := make(map[string]string) diff --git a/internal/config_test.go b/internal/config_test.go index 71e2466..9fdd312 100644 --- a/internal/config_test.go +++ b/internal/config_test.go @@ -1,6 +1,7 @@ package internal_test import ( + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -82,6 +83,59 @@ func TestConfig(t *testing.T) { }, config.Volumes) }) + t.Run("with relative --volume host path", func(t *testing.T) { + dir := t.TempDir() + args := []string{ + "--runtime", "apple", + "--volume", "./relative/path:/container/path", + "some-program", + } + env := []string{ + "TERM=some-term", + } + + config, err := internal.ParseConfig(args, env, dir) + require.NoError(t, err) + require.Equal(t, []string{ + filepath.Join(dir, "relative/path") + ":/container/path", + }, config.Volumes) + }) + + t.Run("with relative --volume host path and options", func(t *testing.T) { + dir := t.TempDir() + args := []string{ + "--runtime", "apple", + "--volume", "./data:/container/data:ro", + "some-program", + } + env := []string{ + "TERM=some-term", + } + + config, err := internal.ParseConfig(args, env, dir) + require.NoError(t, err) + require.Equal(t, []string{ + filepath.Join(dir, "data") + ":/container/data:ro", + }, config.Volumes) + }) + + t.Run("with named volume is not resolved", func(t *testing.T) { + args := []string{ + "--runtime", "apple", + "--volume", "myvolume:/container/path", + "some-program", + } + env := []string{ + "TERM=some-term", + } + + config, err := internal.ParseConfig(args, env, ".") + require.NoError(t, err) + require.Equal(t, []string{ + "myvolume:/container/path", + }, config.Volumes) + }) + t.Run("with multiple --volume flags", func(t *testing.T) { args := []string{ "--runtime", "apple",