Repository: coder/envbuilder
Pull request #247 (2024-06-24 16:29:40)

Use a deterministic target path for feature dir mounts with useBuildContexts
This avoids caching issues when using the Dockerfile produced in `useBuildContexts` mode. See
https://github.com/coder/envbuilder/pull/205#issuecomment-2181962174 for context.

aaronlehmann: Works for me! I've changed the prefix to `/.envbuilder/features`. I went ahead and hardcoded this because this package won't be able to import the root `envbuilder` package, but let me know if you prefer to pass in `MagicDir` instead.
kylecarbs: I think duplicating it is fine!
kylecarbs: @aaronlehmann feel free to merge whenever!
aaronlehmann: Thanks!

Patch:
diff --git a/devcontainer/features/features.go b/devcontainer/features/features.go
index bbea072..4775aad 100644
--- a/devcontainer/features/features.go
+++ b/devcontainer/features/features.go
@@ -218,6 +218,8 @@ func (s *Spec) Compile(featureRef, featureName, featureDir, containerUser, remot
 	sort.Strings(runDirective)
 	// See https://containers.dev/implementors/features/#invoking-installsh
 	if useBuildContexts {
+		// Use a deterministic target directory to make the resulting Dockerfile cacheable
+		featureDir = "/.envbuilder/features/" + featureName
 		fromDirective = "FROM scratch AS envbuilder_feature_" + featureName + "\nCOPY --from=" + featureRef + " / /\n"
 		runDirective = append([]string{"RUN", "--mount=type=bind,from=envbuilder_feature_" + featureName + ",target=" + featureDir + ",rw"}, runDirective...)
 	} else {


Test patch:
diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go
index 052c1ba..c18c6b7 100644
--- a/devcontainer/devcontainer_test.go
+++ b/devcontainer/devcontainer_test.go
@@ -131,13 +131,13 @@ FROM localhost:5000/envbuilder-test-codercom-code-server:latest
 
 USER root
 # Rust tomato - Example description!
-WORKDIR `+featureOneDir+`
+WORKDIR /.envbuilder/features/one
 ENV TOMATO=example
-RUN --mount=type=bind,from=envbuilder_feature_one,target=`+featureOneDir+`,rw _CONTAINER_USER="1000" _REMOTE_USER="1000" ./install.sh
+RUN --mount=type=bind,from=envbuilder_feature_one,target=/.envbuilder/features/one,rw _CONTAINER_USER="1000" _REMOTE_USER="1000" ./install.sh
 # Go potato - Example description!
-WORKDIR `+featureTwoDir+`
+WORKDIR /.envbuilder/features/two
 ENV POTATO=example
-RUN --mount=type=bind,from=envbuilder_feature_two,target=`+featureTwoDir+`,rw VERSION="potato" _CONTAINER_USER="1000" _REMOTE_USER="1000" ./install.sh
+RUN --mount=type=bind,from=envbuilder_feature_two,target=/.envbuilder/features/two,rw VERSION="potato" _CONTAINER_USER="1000" _REMOTE_USER="1000" ./install.sh
 USER 1000`, params.DockerfileContent)
 
 		require.Equal(t, map[string]string{
diff --git a/devcontainer/features/features_test.go b/devcontainer/features/features_test.go
index 0bef70b..389193c 100644
--- a/devcontainer/features/features_test.go
+++ b/devcontainer/features/features_test.go
@@ -115,6 +115,6 @@ func TestCompile(t *testing.T) {
 		fromDirective, runDirective, err := spec.Compile("coder/test:latest", "test", "/.envbuilder/feature/test-d8e8fc", "containerUser", "remoteUser", true, nil)
 		require.NoError(t, err)
 		require.Equal(t, "FROM scratch AS envbuilder_feature_test\nCOPY --from=coder/test:latest / /", strings.TrimSpace(fromDirective))
-		require.Equal(t, "WORKDIR /.envbuilder/feature/test-d8e8fc\nRUN --mount=type=bind,from=envbuilder_feature_test,target=/.envbuilder/feature/test-d8e8fc,rw _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(runDirective))
+		require.Equal(t, "WORKDIR /.envbuilder/features/test\nRUN --mount=type=bind,from=envbuilder_feature_test,target=/.envbuilder/features/test,rw _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(runDirective))
 	})
 }