diff --git a/devcontainer/features/features.go b/devcontainer/features/features.go index 70e4435c..5e4e726f 100644 --- a/devcontainer/features/features.go +++ b/devcontainer/features/features.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "regexp" + "sort" "strings" "github.com/go-git/go-billy/v5" @@ -161,13 +162,12 @@ type Spec struct { // Extract unpacks the feature from the image and returns a set of lines // that should be appended to a Dockerfile to install the feature. func (s *Spec) Compile(options map[string]any) (string, error) { - // See https://containers.dev/implementors/features/#invoking-installsh - runDirective := []string{"RUN"} + var runDirective []string for key, value := range s.Options { - strValue := fmt.Sprintf("%v", value.Default) + strValue := fmt.Sprint(value.Default) provided, ok := options[key] if ok { - strValue = fmt.Sprintf("%v", provided) + strValue = fmt.Sprint(provided) // delete so we can check if there are any unknown options delete(options, key) } @@ -176,12 +176,24 @@ func (s *Spec) Compile(options map[string]any) (string, error) { if len(options) > 0 { return "", fmt.Errorf("unknown option: %v", options) } + // It's critical that the Dockerfile produced is deterministic, + // regardless of map iteration order. + sort.Strings(runDirective) + // See https://containers.dev/implementors/features/#invoking-installsh + runDirective = append([]string{"RUN"}, runDirective...) runDirective = append(runDirective, s.InstallScriptPath) // Prefix and suffix with a newline to ensure the RUN command is on its own line. lines := []string{"\n"} - for key, value := range s.ContainerEnv { - lines = append(lines, fmt.Sprintf("ENV %s=%s", key, value)) + envKeys := make([]string, 0, len(s.ContainerEnv)) + for key := range s.ContainerEnv { + envKeys = append(envKeys, key) + } + // It's critical that the Dockerfile produced is deterministic, + // regardless of map iteration order. + sort.Strings(envKeys) + for _, key := range envKeys { + lines = append(lines, fmt.Sprintf("ENV %s=%s", key, s.ContainerEnv[key])) } lines = append(lines, strings.Join(runDirective, " "), "\n") diff --git a/envbuilder.go b/envbuilder.go index c21aac8b..c282dd26 100644 --- a/envbuilder.go +++ b/envbuilder.go @@ -495,9 +495,6 @@ func Run(ctx context.Context, options Options) error { CacheTTL: time.Hour * 24 * 7, CacheDir: options.BaseImageCacheDir, }, - // Ensures that the final layer has timestamps stripped. - // This can help with sourcing from a cache! - Reproducible: true, ForceUnpack: true, BuildArgs: buildParams.BuildArgs, CacheRepo: options.CacheRepo,