Repository: buildpacks/lifecycle
Pull request #1276 (2024-01-24 07:19:55)

Provide context directory for extensions
<!-- 🎉🎉🎉 Thank you for the PR!!! 🎉🎉🎉 -->

### Summary
<!-- Please describe your changes at a high level. -->

This PR implements the [extension layer](https://github.com/buildpacks/rfcs/blob/main/text/0000-extension-layer.md) RFC by providing one of the following context directory to the kaniko build during the extension phase:

- `$CNB_OUTPUT_DIR/context`
- `$CNB_OUTPUT_DIR/context.build` and/or `$CNB_OUTPUT_DIR/context.run`

More details and examples can be found in the RFC.

#### Release notes
<!-- Please provide 1-2 sentences for release notes. -->
<!-- Example: When using platform API `0.7` or greater, the `creator` logs the expected phase header for the analyze phase -->
Provide and allow extensions to write into a shared or specific context directory.


---

### Related
<!-- If this PR addresses an issue, please provide the issue number below. -->

---

### Context
<!-- Add any other context that may help reviewers (e.g., code that requires special attention, etc.). -->



loewenstein: @natalieparellano once the spec changes are in, this would be worth it a look. Or already now, if there is review feedback that requires changes.
natalieparellano: @modulo11 @loewenstein thank you for this - apologies for being slow to review the changes. It looks like there might be a couple of tests to fix, but I'll take a deeper look tomorrow to see if I could offer more useful feedback.
loewenstein: > @modulo11 @loewenstein thank you for this - apologies for being slow to review the changes. It looks like there might be a couple of tests to fix, but I'll take a deeper look tomorrow to see if I could offer more useful feedback.

@modulo11 did we have a look for what causes the test failures?
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) Report
Attention: `40 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`3c8876c`)](https://app.codecov.io/gh/buildpacks/lifecycle/commit/3c8876ce042837896e76d2d31e8c18c48a8aa738?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) 63.99% compared to head [(`63b1b73`)](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) 63.91%.
> Report is 3 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276/graphs/tree.svg?width=650&height=150&src=pr&token=Zmm2KHcGn9&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks)](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks)

```diff
@@            Coverage Diff             @@
##             main    #1276      +/-   ##
==========================================
- Coverage   63.99%   63.91%   -0.08%     
==========================================
  Files         100      101       +1     
  Lines        6909     6993      +84     
==========================================
+ Hits         4421     4469      +48     
- Misses       2081     2109      +28     
- Partials      407      415       +8     
```

| [Flag](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) | Coverage Δ | |
|---|---|---|
| [os_linux](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) | `63.38% <59.60%> (-0.07%)` | :arrow_down: |
| [os_windows](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) | `56.00% <0.00%> (-0.70%)` | :arrow_down: |
| [unit](https://app.codecov.io/gh/buildpacks/lifecycle/pull/1276/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks) | `63.38% <59.60%> (-0.07%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=buildpacks#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>
pbusko: @natalieparellano we've addressed all the comments and introduced additional acceptance test with the extensions contexts

Patch:
diff --git a/README.md b/README.md
index b8ea39e9..62d89441 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ A reference implementation of the [Cloud Native Buildpacks specification](https:
 ## Supported APIs
 | Lifecycle Version | Platform APIs                                                                                                                                    | Buildpack APIs                                                                                                                 |
 |-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
-| 0.18.x            | [0.7][p/0.7], [0.8][p/0.8], [0.9][p/0.9], [0.10][p/0.10], [0.11][p/0.11], [0.12][p/0.12]                                                         | [0.7][b/0.7], [0.8][b/0.8], [0.9][b/0.9], [0.10][b/0.10]                                                                       |
+| 0.18.x            | [0.7][p/0.7], [0.8][p/0.8], [0.9][p/0.9], [0.10][p/0.10], [0.11][p/0.11], [0.12][p/0.12], [0.13][p/0.13]                                                         | [0.7][b/0.7], [0.8][b/0.8], [0.9][b/0.9], [0.10][b/0.10]                                                                       |
 | 0.17.x            | [0.3][p/0.3], [0.4][p/0.4], [0.5][p/0.5], [0.6][p/0.6], [0.7][p/0.7], [0.8][p/0.8], [0.9][p/0.9], [0.10][p/0.10], [0.11][p/0.11], [0.12][p/0.12] | [0.2][b/0.2], [0.3][b/0.3], [0.4][b/0.4], [0.5][b/0.5], [0.6][b/0.6], [0.7][b/0.7], [0.8][b/0.8], [0.9][b/0.9], [0.10][b/0.10] |
 | 0.16.x            | [0.3][p/0.3], [0.4][p/0.4], [0.5][p/0.5], [0.6][p/0.6], [0.7][p/0.7], [0.8][p/0.8], [0.9][p/0.9], [0.10][p/0.10], [0.11][p/0.11]                 | [0.2][b/0.2], [0.3][b/0.3], [0.4][b/0.4], [0.5][b/0.5], [0.6][b/0.6], [0.7][b/0.7], [0.8][b/0.8], [0.9][b/0.9]                 |
 | 0.15.x            | [0.3][p/0.3], [0.4][p/0.4], [0.5][p/0.5], [0.6][p/0.6], [0.7][p/0.7], [0.8][p/0.8], [0.9][p/0.9], [0.10][p/0.10]                                 | [0.2][b/0.2], [0.3][b/0.3], [0.4][b/0.4], [0.5][b/0.5], [0.6][b/0.6], [0.7][b/0.7], [0.8][b/0.8], [0.9][b/0.9]                 |
@@ -38,6 +38,7 @@ A reference implementation of the [Cloud Native Buildpacks specification](https:
 [p/0.10]: https://github.com/buildpacks/spec/blob/platform/v0.10/platform.md
 [p/0.11]: https://github.com/buildpacks/spec/blob/platform/v0.11/platform.md
 [p/0.12]: https://github.com/buildpacks/spec/blob/platform/v0.12/platform.md
+[p/0.13]: https://github.com/buildpacks/spec/blob/platform/v0.13/platform.md
 
 \* denotes unreleased version
 
diff --git a/api/apis.go b/api/apis.go
index 58f6d678..67be3702 100644
--- a/api/apis.go
+++ b/api/apis.go
@@ -11,7 +11,7 @@ var (
 	// Platform is a pair of lists of Platform API versions:
 	// 1. All supported versions (including deprecated versions)
 	// 2. The versions that are deprecated
-	Platform = newApisMustParse([]string{"0.7", "0.8", "0.9", "0.10", "0.11", "0.12"}, []string{})
+	Platform = newApisMustParse([]string{"0.7", "0.8", "0.9", "0.10", "0.11", "0.12", "0.13"}, []string{})
 	// Buildpack is a pair of lists of Buildpack API versions:
 	// 1. All supported versions (including deprecated versions)
 	// 2. The versions that are deprecated
diff --git a/buildpack/generate.go b/buildpack/generate.go
index ba32596d..a3aacd6a 100644
--- a/buildpack/generate.go
+++ b/buildpack/generate.go
@@ -31,6 +31,7 @@ type GenerateInputs struct {
 
 type GenerateOutputs struct {
 	Dockerfiles []DockerfileInfo
+	Contexts    []extend.ContextInfo
 	MetRequires []string
 }
 
@@ -114,6 +115,7 @@ func readOutputFilesExt(d ExtDescriptor, extOutputDir string, extPlanIn Plan, lo
 	var err error
 	var dfInfo DockerfileInfo
 	var found bool
+	var contexts []extend.ContextInfo
 
 	// set MetRequires
 	gr.MetRequires = names(extPlanIn.Entries)
@@ -128,15 +130,22 @@ func readOutputFilesExt(d ExtDescriptor, extOutputDir string, extPlanIn Plan, lo
 		return GenerateOutputs{}, err
 	} else if found {
 		gr.Dockerfiles = append(gr.Dockerfiles, dfInfo)
+		logger.Debugf("Found run.Dockerfile for processing")
 	}
 
 	if dfInfo, found, err = findDockerfileFor(d, extOutputDir, DockerfileKindBuild, logger); err != nil {
 		return GenerateOutputs{}, err
 	} else if found {
 		gr.Dockerfiles = append(gr.Dockerfiles, dfInfo)
+		logger.Debugf("Found build.Dockerfile for processing")
 	}
 
-	logger.Debugf("Found '%d' Dockerfiles for processing", len(gr.Dockerfiles))
+	if contexts, err = extend.FindContexts(d.Extension.ID, extOutputDir, logger); err != nil {
+		return GenerateOutputs{}, err
+	}
+
+	gr.Contexts = contexts
+	logger.Debugf("Found '%d' Build Contexts for processing", len(gr.Contexts))
 
 	return gr, nil
 }
diff --git a/go.mod b/go.mod
index 2a41e17e..a202fd80 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/sclevine/spec v1.4.0
 	golang.org/x/sync v0.6.0
-	golang.org/x/sys v0.16.0
+	golang.org/x/sys v0.17.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 8a3d482f..7e89245b 100644
--- a/go.sum
+++ b/go.sum
@@ -495,8 +495,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.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 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
diff --git a/internal/extend/buildcontext.go b/internal/extend/buildcontext.go
new file mode 100644
index 00000000..c2b010a4
--- /dev/null
+++ b/internal/extend/buildcontext.go
@@ -0,0 +1,63 @@
+// Package extend contains extensions specific helpers
+package extend
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/buildpacks/lifecycle/log"
+)
+
+const (
+	// BuildContextDir defines the context for build-image extensions
+	BuildContextDir = "context.build"
+	// RunContextDir defines the context for run-image extensions
+	RunContextDir = "context.run"
+	// SharedContextDir defines a shared context for build- and run-image extensions
+	SharedContextDir = "context"
+)
+
+// ContextInfo captures info about the context used for build- and run-image extensions
+type ContextInfo struct {
+	ExtensionID string
+	Path        string
+}
+
+// FindContexts returns paths to Build Context folders produced by an extension. If Shared Context is provided with Run or Build Contexts the error will be thrown.
+func FindContexts(extensionID string, extOutputDir string, logger log.Logger) ([]ContextInfo, error) {
+	var contexts []ContextInfo
+	var sharedIsProvided bool
+
+	sharedContextDir := filepath.Join(extOutputDir, SharedContextDir)
+	if s, err := os.Stat(sharedContextDir); err == nil && s.IsDir() {
+		logger.Debugf("found shared extension context %q", sharedContextDir)
+		sharedIsProvided = true
+		contexts = append(contexts, ContextInfo{
+			ExtensionID: extensionID,
+			Path:        sharedContextDir,
+		})
+	} else if err != nil && !os.IsNotExist(err) {
+		return nil, err
+	}
+
+	for _, dir := range []string{BuildContextDir, RunContextDir} {
+		contextDir := filepath.Join(extOutputDir, dir)
+
+		if s, err := os.Stat(contextDir); err == nil && s.IsDir() {
+			if sharedIsProvided {
+				return nil, fmt.Errorf("image-specific context dir is provided together with a shared context")
+			}
+
+			logger.Debugf("found extension context %q", contextDir)
+			contexts = append(contexts, ContextInfo{
+				ExtensionID: extensionID,
+				Path:        contextDir,
+			})
+		} else if err != nil && !os.IsNotExist(err) {
+			return nil, err
+		}
+	}
+
+	return contexts, nil
+}
diff --git a/internal/extend/dockerfile.go b/internal/extend/dockerfile.go
index 69e4a80a..eea2aa2d 100644
--- a/internal/extend/dockerfile.go
+++ b/internal/extend/dockerfile.go
@@ -4,6 +4,7 @@ type Dockerfile struct {
 	ExtensionID string
 	Path        string `toml:"path"`
 	Args        []Arg
+	ContextDir  string
 }
 
 type Arg struct {
diff --git a/internal/extend/kaniko/dockerfile_applier.go b/internal/extend/kaniko/dockerfile_applier.go
index 6ca581a3..e4e31d07 100644
--- a/internal/extend/kaniko/dockerfile_applier.go
+++ b/internal/extend/kaniko/dockerfile_applier.go
@@ -75,6 +75,7 @@ func createOptions(baseImageRef string, dockerfile extend.Dockerfile, options ex
 		Cache:             true,
 		CacheOptions:      config.CacheOptions{CacheTTL: options.CacheTTL},
 		CacheRunLayers:    true,
+		CacheCopyLayers:   true,
 		CacheRepo:         kanikoCacheImageRef,
 		Cleanup:           false,
 		CustomPlatform:    platforms.DefaultString(),
diff --git a/phase/extender.go b/phase/extender.go
index 58d2f8c0..0e62e870 100644
--- a/phase/extender.go
+++ b/phase/extender.go
@@ -18,6 +18,7 @@ import (
 	"github.com/google/uuid"
 	"golang.org/x/sync/errgroup"
 
+	"github.com/buildpacks/lifecycle/api"
 	"github.com/buildpacks/lifecycle/buildpack"
 	"github.com/buildpacks/lifecycle/internal/extend"
 	"github.com/buildpacks/lifecycle/launch"
@@ -37,6 +38,8 @@ type Extender struct {
 	CacheTTL          time.Duration            // a platform input
 	DockerfileApplier DockerfileApplier        // uses kaniko, BuildKit, or other to apply the provided Dockerfile to the provided image
 	Extensions        []buildpack.GroupElement // extensions are ordered from group.toml
+
+	PlatformAPI *api.Version
 }
 
 // DockerfileApplier given a base image and a `build.Dockerfile` or `run.Dockerfile` will apply it to the base image
@@ -60,6 +63,7 @@ func (f *HermeticFactory) NewExtender(inputs platform.LifecycleInputs, dockerfil
 		PlatformDir:       inputs.PlatformDir,
 		CacheTTL:          inputs.KanikoCacheTTL,
 		DockerfileApplier: dockerfileApplier,
+		PlatformAPI:       f.platformAPI,
 	}
 	var err error
 	if extender.ImageRef, err = f.getExtendImageRef(inputs, logger); err != nil {
@@ -262,10 +266,10 @@ func (e *Extender) extend(kind string, baseImage v1.Image, logger log.Logger) (v
 		return nil, err
 	}
 	workingHistory = configFile.History
-	buildOptions := e.extendOptions()
 	userID, groupID := userFrom(*configFile)
 	origUserID := userID
 	for _, dockerfile := range dockerfiles {
+		buildOptions := e.extendOptions(dockerfile)
 		dockerfile.Args = append([]extend.Arg{
 			{Name: argBuildID, Value: uuid.New().String()},
 			{Name: argUserID, Value: userID},
@@ -366,14 +370,27 @@ func (e *Extender) dockerfilesFor(kind string, logger log.Logger) ([]extend.Dock
 }
 
 func (e *Extender) dockerfileFor(kind, extID string) (*extend.Dockerfile, error) {
+	var err error
 	dockerfilePath := filepath.Join(e.GeneratedDir, kind, launch.EscapeID(extID), "Dockerfile")
+	configPath := filepath.Join(e.GeneratedDir, kind, launch.EscapeID(extID), "extend-config.toml")
+	contextDir := e.AppDir
+
+	if e.PlatformAPI.AtLeast("0.13") {
+		configPath = filepath.Join(e.GeneratedDir, launch.EscapeID(extID), "extend-config.toml")
+		dockerfilePath = filepath.Join(e.GeneratedDir, launch.EscapeID(extID), fmt.Sprintf("%s.Dockerfile", kind))
+
+		contextDir, err = e.contextDirFor(kind, extID)
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	if _, err := os.Stat(dockerfilePath); err != nil {
 		return nil, nil
 	}
 
-	configPath := filepath.Join(e.GeneratedDir, kind, launch.EscapeID(extID), "extend-config.toml")
 	var config extend.Config
-	_, err := toml.DecodeFile(configPath, &config)
+	_, err = toml.DecodeFile(configPath, &config)
 	if err != nil {
 		if !os.IsNotExist(err) {
 			return nil, err
@@ -391,12 +408,26 @@ func (e *Extender) dockerfileFor(kind, extID string) (*extend.Dockerfile, error)
 		ExtensionID: extID,
 		Path:        dockerfilePath,
 		Args:        args,
+		ContextDir:  contextDir,
 	}, nil
 }
 
-func (e *Extender) extendOptions() extend.Options {
+func (e *Extender) contextDirFor(kind, extID string) (string, error) {
+	sharedContextDir := filepath.Join(e.GeneratedDir, launch.EscapeID(extID), extend.SharedContextDir)
+	kindContextDir := filepath.Join(e.GeneratedDir, launch.EscapeID(extID), fmt.Sprintf("%s.%s", extend.SharedContextDir, kind))
+
+	for _, dir := range []string{kindContextDir, sharedContextDir} {
+		if s, err := os.Stat(dir); err == nil && s.IsDir() {
+			return dir, nil
+		}
+	}
+
+	return e.AppDir, nil
+}
+
+func (e *Extender) extendOptions(dockerfile extend.Dockerfile) extend.Options {
 	return extend.Options{
-		BuildContext: e.AppDir,
+		BuildContext: dockerfile.ContextDir,
 		CacheTTL:     e.CacheTTL,
 		IgnorePaths:  []string{e.AppDir, e.LayersDir, e.PlatformDir},
 	}
diff --git a/phase/generator.go b/phase/generator.go
index 1b26d81d..ac80ba01 100644
--- a/phase/generator.go
+++ b/phase/generator.go
@@ -71,12 +71,20 @@ type GenerateResult struct {
 func (g *Generator) Generate() (GenerateResult, error) {
 	defer log.NewMeasurement("Generator", g.Logger)()
 	inputs := g.getGenerateInputs()
-	extensionOutputParentDir, err := os.MkdirTemp("", "cnb-extensions-generated.")
-	if err != nil {
-		return GenerateResult{}, err
+
+	var err error
+	if g.PlatformAPI.LessThan("0.13") {
+		extensionOutputParentDir, err := os.MkdirTemp("", "cnb-extensions-generated.")
+		if err != nil {
+			return GenerateResult{}, err
+		}
+		defer func() {
+			err = os.RemoveAll(extensionOutputParentDir)
+		}()
+		inputs.OutputDir = extensionOutputParentDir
+	} else {
+		inputs.OutputDir = g.GeneratedDir
 	}
-	defer os.RemoveAll(extensionOutputParentDir)
-	inputs.OutputDir = extensionOutputParentDir
 
 	var dockerfiles []buildpack.DockerfileInfo
 	filteredPlan := g.Plan
@@ -130,14 +138,14 @@ func (g *Generator) Generate() (GenerateResult, error) {
 	}
 
 	g.Logger.Debug("Copying Dockerfiles")
-	if err = g.copyDockerfiles(dockerfiles); err != nil {
+	if err := g.copyDockerfiles(dockerfiles); err != nil {
 		return GenerateResult{}, err
 	}
 
 	return GenerateResult{
 		AnalyzedMD: finalAnalyzedMD,
 		Plan:       filteredPlan,
-	}, nil
+	}, err
 }
 
 func (g *Generator) getGenerateInputs() buildpack.GenerateInputs {
@@ -153,11 +161,24 @@ func (g *Generator) getGenerateInputs() buildpack.GenerateInputs {
 
 func (g *Generator) copyDockerfiles(dockerfiles []buildpack.DockerfileInfo) error {
 	for _, dockerfile := range dockerfiles {
+		ignoreDockerfile := dockerfile.Kind == buildpack.DockerfileKindRun && dockerfile.Ignore
+
+		if g.PlatformAPI.AtLeast("0.13") {
+			if ignoreDockerfile {
+				if err := fsutil.RenameWithWindowsFallback(dockerfile.Path, dockerfile.Path+".ignore"); err != nil {
+					return fmt.Errorf("failed to rename Dockerfile at %s: %w", dockerfile.Path, err)
+				}
+			}
+
+			continue
+		}
+
 		targetDir := filepath.Join(g.GeneratedDir, dockerfile.Kind, launch.EscapeID(dockerfile.ExtensionID))
-		var targetPath = filepath.Join(targetDir, "Dockerfile")
-		if dockerfile.Kind == buildpack.DockerfileKindRun && dockerfile.Ignore {
+		targetPath := filepath.Join(targetDir, "Dockerfile")
+		if ignoreDockerfile {
 			targetPath += ".ignore"
 		}
+
 		if err := os.MkdirAll(targetDir, os.ModePerm); err != nil {
 			return err
 		}


Test patch:
diff --git a/acceptance/detector_test.go b/acceptance/detector_test.go
index aeb5794f..d4261890 100644
--- a/acceptance/detector_test.go
+++ b/acceptance/detector_test.go
@@ -52,355 +52,368 @@ func TestDetector(t *testing.T) {
 	)
 	defer h.DockerImageRemove(t, detectImage)
 
-	spec.Run(t, "acceptance-detector", testDetector, spec.Parallel(), spec.Report(report.Terminal{}))
+	for _, platformAPI := range api.Platform.Supported {
+		if platformAPI.LessThan("0.12") {
+			continue
+		}
+
+		spec.Run(t, "acceptance-detector/"+platformAPI.String(), testDetectorFunc(platformAPI.String()), spec.Parallel(), spec.Report(report.Terminal{}))
+	}
 }
 
-func testDetector(t *testing.T, when spec.G, it spec.S) {
-	when("called with arguments", func() {
-		it("errors", func() {
-			command := exec.Command(
-				"docker",
-				"run",
-				"--rm",
-				"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-				detectImage,
-				"some-arg",
-			)
-			output, err := command.CombinedOutput()
-			h.AssertNotNil(t, err)
-			expected := "failed to parse arguments: received unexpected arguments"
-			h.AssertStringContains(t, string(output), expected)
+func testDetectorFunc(platformAPI string) func(t *testing.T, when spec.G, it spec.S) {
+	return func(t *testing.T, when spec.G, it spec.S) {
+		when("called with arguments", func() {
+			it("errors", func() {
+				command := exec.Command(
+					"docker",
+					"run",
+					"--rm",
+					"--env", "CNB_PLATFORM_API="+platformAPI,
+					detectImage,
+					"some-arg",
+				)
+				output, err := command.CombinedOutput()
+				h.AssertNotNil(t, err)
+				expected := "failed to parse arguments: received unexpected arguments"
+				h.AssertStringContains(t, string(output), expected)
+			})
 		})
-	})
-
-	when("running as a root", func() {
-		it("errors", func() {
-			command := exec.Command(
-				"docker",
-				"run",
-				"--rm",
-				"--user",
-				"root",
-				"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-				detectImage,
-			)
-			output, err := command.CombinedOutput()
-			h.AssertNotNil(t, err)
-			expected := "failed to detect: refusing to run as root"
-			h.AssertStringContains(t, string(output), expected)
+
+		when("running as a root", func() {
+			it("errors", func() {
+				command := exec.Command(
+					"docker",
+					"run",
+					"--rm",
+					"--user",
+					"root",
+					"--env", "CNB_PLATFORM_API="+platformAPI,
+					detectImage,
+				)
+				output, err := command.CombinedOutput()
+				h.AssertNotNil(t, err)
+				expected := "failed to detect: refusing to run as root"
+				h.AssertStringContains(t, string(output), expected)
+			})
 		})
-	})
-
-	when("read buildpack order file failed", func() {
-		it("errors", func() {
-			// no order.toml file in the default search locations
-			command := exec.Command(
-				"docker",
-				"run",
-				"--rm",
-				"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-				detectImage,
-			)
-			output, err := command.CombinedOutput()
-			h.AssertNotNil(t, err)
-			expected := "failed to initialize detector: reading order"
-			h.AssertStringContains(t, string(output), expected)
+
+		when("read buildpack order file failed", func() {
+			it("errors", func() {
+				// no order.toml file in the default search locations
+				command := exec.Command(
+					"docker",
+					"run",
+					"--rm",
+					"--env", "CNB_PLATFORM_API="+platformAPI,
+					detectImage,
+				)
+				output, err := command.CombinedOutput()
+				h.AssertNotNil(t, err)
+				expected := "failed to initialize detector: reading order"
+				h.AssertStringContains(t, string(output), expected)
+			})
 		})
-	})
-
-	when("no buildpack group passed detection", func() {
-		it("errors and exits with the expected code", func() {
-			command := exec.Command(
-				"docker",
-				"run",
-				"--rm",
-				"--env", "CNB_ORDER_PATH=/cnb/orders/fail_detect_order.toml",
-				"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-				detectImage,
-			)
-			output, err := command.CombinedOutput()
-			h.AssertNotNil(t, err)
-			failErr, ok := err.(*exec.ExitError)
-			if !ok {
-				t.Fatalf("expected an error of type exec.ExitError")
-			}
-			h.AssertEq(t, failErr.ExitCode(), 20) // platform code for failed detect
-
-			expected1 := `======== Output: fail_detect_buildpack@some_version ========
+
+		when("no buildpack group passed detection", func() {
+			it("errors and exits with the expected code", func() {
+				command := exec.Command(
+					"docker",
+					"run",
+					"--rm",
+					"--env", "CNB_ORDER_PATH=/cnb/orders/fail_detect_order.toml",
+					"--env", "CNB_PLATFORM_API="+platformAPI,
+					detectImage,
+				)
+				output, err := command.CombinedOutput()
+				h.AssertNotNil(t, err)
+				failErr, ok := err.(*exec.ExitError)
+				if !ok {
+					t.Fatalf("expected an error of type exec.ExitError")
+				}
+				h.AssertEq(t, failErr.ExitCode(), 20) // platform code for failed detect
+
+				expected1 := `======== Output: fail_detect_buildpack@some_version ========
 Opted out of detection
 ======== Results ========
-fail: fail_detect_buildpack@some_version
-`
-			h.AssertStringContains(t, string(output), expected1)
-			expected2 := "No buildpack groups passed detection."
-			h.AssertStringContains(t, string(output), expected2)
+fail: fail_detect_buildpack@some_version`
+				h.AssertStringContains(t, string(output), expected1)
+				expected2 := "No buildpack groups passed detection."
+				h.AssertStringContains(t, string(output), expected2)
+			})
 		})
-	})
 
-	when("there is a buildpack group that passes detection", func() {
-		var copyDir, containerName string
+		when("there is a buildpack group that passes detection", func() {
+			var copyDir, containerName string
 
-		it.Before(func() {
-			containerName = "test-container-" + h.RandString(10)
-			var err error
-			copyDir, err = os.MkdirTemp("", "test-docker-copy-")
-			h.AssertNil(t, err)
-		})
+			it.Before(func() {
+				containerName = "test-container-" + h.RandString(10)
+				var err error
+				copyDir, err = os.MkdirTemp("", "test-docker-copy-")
+				h.AssertNil(t, err)
+			})
 
-		it.After(func() {
-			if h.DockerContainerExists(t, containerName) {
-				h.Run(t, exec.Command("docker", "rm", containerName))
-			}
-			os.RemoveAll(copyDir)
-		})
+			it.After(func() {
+				if h.DockerContainerExists(t, containerName) {
+					h.Run(t, exec.Command("docker", "rm", containerName))
+				}
+				os.RemoveAll(copyDir)
+			})
 
-		it("writes group.toml and plan.toml at the default locations", func() {
-			output := h.DockerRunAndCopy(t,
-				containerName,
-				copyDir,
-				"/layers",
-				detectImage,
-				h.WithFlags("--user", userID,
-					"--env", "CNB_ORDER_PATH=/cnb/orders/simple_order.toml",
-					"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-				),
-				h.WithArgs(),
-			)
-
-			// check group.toml
-			foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
-			group, err := files.Handler.ReadGroup(foundGroupTOML)
-			h.AssertNil(t, err)
-			h.AssertEq(t, group.Group[0].ID, "simple_buildpack")
-			h.AssertEq(t, group.Group[0].Version, "simple_buildpack_version")
-
-			// check plan.toml
-			foundPlanTOML := filepath.Join(copyDir, "layers", "plan.toml")
-			buildPlan, err := files.Handler.ReadPlan(foundPlanTOML)
-			h.AssertNil(t, err)
-			h.AssertEq(t, buildPlan.Entries[0].Providers[0].ID, "simple_buildpack")
-			h.AssertEq(t, buildPlan.Entries[0].Providers[0].Version, "simple_buildpack_version")
-			h.AssertEq(t, buildPlan.Entries[0].Requires[0].Name, "some_requirement")
-			h.AssertEq(t, buildPlan.Entries[0].Requires[0].Metadata["some_metadata_key"], "some_metadata_val")
-			h.AssertEq(t, buildPlan.Entries[0].Requires[0].Metadata["version"], "some_version")
-
-			// check output
-			h.AssertStringContains(t, output, "simple_buildpack simple_buildpack_version")
-			h.AssertStringDoesNotContain(t, output, "======== Results ========") // log output is info level as detect passed
-		})
-	})
+			it("writes group.toml and plan.toml at the default locations", func() {
+				output := h.DockerRunAndCopy(t,
+					containerName,
+					copyDir,
+					"/layers",
+					detectImage,
+					h.WithFlags("--user", userID,
+						"--env", "CNB_ORDER_PATH=/cnb/orders/simple_order.toml",
+						"--env", "CNB_PLATFORM_API="+platformAPI,
+					),
+					h.WithArgs(),
+				)
 
-	when("environment variables are provided for buildpack and app directories and for the output files", func() {
-		var copyDir, containerName string
+				// check group.toml
+				foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
+				group, err := files.Handler.ReadGroup(foundGroupTOML)
+				h.AssertNil(t, err)
+				h.AssertEq(t, group.Group[0].ID, "simple_buildpack")
+				h.AssertEq(t, group.Group[0].Version, "simple_buildpack_version")
 
-		it.Before(func() {
-			containerName = "test-container-" + h.RandString(10)
-			var err error
-			copyDir, err = os.MkdirTemp("", "test-docker-copy-")
-			h.AssertNil(t, err)
+				// check plan.toml
+				foundPlanTOML := filepath.Join(copyDir, "layers", "plan.toml")
+				buildPlan, err := files.Handler.ReadPlan(foundPlanTOML)
+				h.AssertNil(t, err)
+				h.AssertEq(t, buildPlan.Entries[0].Providers[0].ID, "simple_buildpack")
+				h.AssertEq(t, buildPlan.Entries[0].Providers[0].Version, "simple_buildpack_version")
+				h.AssertEq(t, buildPlan.Entries[0].Requires[0].Name, "some_requirement")
+				h.AssertEq(t, buildPlan.Entries[0].Requires[0].Metadata["some_metadata_key"], "some_metadata_val")
+				h.AssertEq(t, buildPlan.Entries[0].Requires[0].Metadata["version"], "some_version")
+
+				// check output
+				h.AssertStringContains(t, output, "simple_buildpack simple_buildpack_version")
+				h.AssertStringDoesNotContain(t, output, "======== Results ========") // log output is info level as detect passed
+			})
 		})
 
-		it.After(func() {
-			if h.DockerContainerExists(t, containerName) {
-				h.Run(t, exec.Command("docker", "rm", containerName))
-			}
-			os.RemoveAll(copyDir)
-		})
+		when("environment variables are provided for buildpack and app directories and for the output files", func() {
+			var copyDir, containerName string
 
-		it("writes group.toml and plan.toml in the right locations and with the right names", func() {
-			h.DockerRunAndCopy(t,
-				containerName,
-				copyDir,
-				"/layers",
-				detectImage,
-				h.WithFlags("--user", userID,
-					"--env", "CNB_ORDER_PATH=/cnb/orders/always_detect_order.toml",
-					"--env", "CNB_BUILDPACKS_DIR=/cnb/custom_buildpacks",
-					"--env", "CNB_APP_DIR=/custom_workspace",
-					"--env", "CNB_GROUP_PATH=./custom_group.toml",
-					"--env", "CNB_PLAN_PATH=./custom_plan.toml",
-					"--env", "CNB_PLATFORM_DIR=/custom_platform",
-					"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-				),
-				h.WithArgs("-log-level=debug"),
-			)
-
-			// check group.toml
-			foundGroupTOML := filepath.Join(copyDir, "layers", "custom_group.toml")
-			group, err := files.Handler.ReadGroup(foundGroupTOML)
-			h.AssertNil(t, err)
-			h.AssertEq(t, group.Group[0].ID, "always_detect_buildpack")
-			h.AssertEq(t, group.Group[0].Version, "always_detect_buildpack_version")
-
-			// check plan.toml - should be empty since we're using always_detect_order.toml so there is no "actual plan"
-			tempPlanToml := filepath.Join(copyDir, "layers", "custom_plan.toml")
-			planContents, err := os.ReadFile(tempPlanToml)
-			h.AssertNil(t, err)
-			h.AssertEq(t, len(planContents) == 0, true)
-
-			// check platform directory
-			logs := h.Run(t, exec.Command("docker", "logs", containerName))
-			expectedPlatformPath := "platform_path: /custom_platform"
-			expectedAppDir := "app_dir: /custom_workspace"
-			h.AssertStringContains(t, logs, expectedPlatformPath)
-			h.AssertStringContains(t, logs, expectedAppDir)
-		})
-	})
+			it.Before(func() {
+				containerName = "test-container-" + h.RandString(10)
+				var err error
+				copyDir, err = os.MkdirTemp("", "test-docker-copy-")
+				h.AssertNil(t, err)
+			})
+
+			it.After(func() {
+				if h.DockerContainerExists(t, containerName) {
+					h.Run(t, exec.Command("docker", "rm", containerName))
+				}
+				os.RemoveAll(copyDir)
+			})
 
-	when("-order is provided", func() {
-		var copyDir, containerName, expectedOrderTOMLPath string
+			it("writes group.toml and plan.toml in the right locations and with the right names", func() {
+				h.DockerRunAndCopy(t,
+					containerName,
+					copyDir,
+					"/layers",
+					detectImage,
+					h.WithFlags("--user", userID,
+						"--env", "CNB_ORDER_PATH=/cnb/orders/always_detect_order.toml",
+						"--env", "CNB_BUILDPACKS_DIR=/cnb/custom_buildpacks",
+						"--env", "CNB_APP_DIR=/custom_workspace",
+						"--env", "CNB_GROUP_PATH=./custom_group.toml",
+						"--env", "CNB_PLAN_PATH=./custom_plan.toml",
+						"--env", "CNB_PLATFORM_DIR=/custom_platform",
+						"--env", "CNB_PLATFORM_API="+platformAPI,
+					),
+					h.WithArgs("-log-level=debug"),
+				)
 
-		it.Before(func() {
-			containerName = "test-container-" + h.RandString(10)
-			var err error
-			copyDir, err = os.MkdirTemp("", "test-docker-copy-")
-			h.AssertNil(t, err)
+				// check group.toml
+				foundGroupTOML := filepath.Join(copyDir, "layers", "custom_group.toml")
+				group, err := files.Handler.ReadGroup(foundGroupTOML)
+				h.AssertNil(t, err)
+				h.AssertEq(t, group.Group[0].ID, "always_detect_buildpack")
+				h.AssertEq(t, group.Group[0].Version, "always_detect_buildpack_version")
 
-			simpleOrderTOML := filepath.Join("testdata", "detector", "container", "cnb", "orders", "simple_order.toml")
-			expectedOrderTOMLPath, err = filepath.Abs(simpleOrderTOML)
-			h.AssertNil(t, err)
+				// check plan.toml - should be empty since we're using always_detect_order.toml so there is no "actual plan"
+				tempPlanToml := filepath.Join(copyDir, "layers", "custom_plan.toml")
+				planContents, err := os.ReadFile(tempPlanToml)
+				h.AssertNil(t, err)
+				h.AssertEq(t, len(planContents) == 0, true)
+
+				// check platform directory
+				logs := h.Run(t, exec.Command("docker", "logs", containerName))
+				expectedPlatformPath := "platform_path: /custom_platform"
+				expectedAppDir := "app_dir: /custom_workspace"
+				h.AssertStringContains(t, logs, expectedPlatformPath)
+				h.AssertStringContains(t, logs, expectedAppDir)
+			})
 		})
 
-		it.After(func() {
-			if h.DockerContainerExists(t, containerName) {
-				h.Run(t, exec.Command("docker", "rm", containerName))
-			}
-			os.RemoveAll(copyDir)
+		when("-order is provided", func() {
+			var copyDir, containerName, expectedOrderTOMLPath string
+
+			it.Before(func() {
+				containerName = "test-container-" + h.RandString(10)
+				var err error
+				copyDir, err = os.MkdirTemp("", "test-docker-copy-")
+				h.AssertNil(t, err)
+
+				simpleOrderTOML := filepath.Join("testdata", "detector", "container", "cnb", "orders", "simple_order.toml")
+				expectedOrderTOMLPath, err = filepath.Abs(simpleOrderTOML)
+				h.AssertNil(t, err)
+			})
+
+			it.After(func() {
+				if h.DockerContainerExists(t, containerName) {
+					h.Run(t, exec.Command("docker", "rm", containerName))
+				}
+				os.RemoveAll(copyDir)
+			})
+
+			when("the order.toml exists", func() {
+				it("processes the provided order.toml", func() {
+					h.DockerRunAndCopy(t,
+						containerName,
+						copyDir,
+						"/layers",
+						detectImage,
+						h.WithFlags("--user", userID,
+							"--volume", expectedOrderTOMLPath+":/custom/order.toml",
+							"--env", "CNB_PLATFORM_API="+platformAPI,
+						),
+						h.WithArgs(
+							"-log-level=debug",
+							"-order=/custom/order.toml",
+						),
+					)
+
+					// check group.toml
+					foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
+					group, err := files.Handler.ReadGroup(foundGroupTOML)
+					h.AssertNil(t, err)
+					h.AssertEq(t, group.Group[0].ID, "simple_buildpack")
+					h.AssertEq(t, group.Group[0].Version, "simple_buildpack_version")
+				})
+			})
+
+			when("the order.toml does not exist", func() {
+				it("errors", func() {
+					command := exec.Command("docker", "run",
+						"--user", userID,
+						"--rm",
+						"--env", "CNB_PLATFORM_API="+platformAPI,
+						detectImage,
+						"-order=/custom/order.toml")
+					output, err := command.CombinedOutput()
+					h.AssertNotNil(t, err)
+					expected := "failed to initialize detector: reading order: failed to read order file: open /custom/order.toml: no such file or directory"
+					h.AssertStringContains(t, string(output), expected)
+				})
+			})
+
+			when("the order.toml contains a buildpack using an unsupported api", func() {
+				it("errors", func() {
+					command := exec.Command("docker", "run",
+						"--user", userID,
+						"--rm",
+						"--env", "CNB_PLATFORM_API="+platformAPI,
+						detectImage,
+						"-order=/cnb/orders/bad_api.toml")
+					output, err := command.CombinedOutput()
+					h.AssertNotNil(t, err)
+					failErr, ok := err.(*exec.ExitError)
+					if !ok {
+						t.Fatalf("expected an error of type exec.ExitError")
+					}
+					h.AssertEq(t, failErr.ExitCode(), 12) // platform code for buildpack api error
+					expected := "buildpack API version '0.1' is incompatible with the lifecycle"
+					h.AssertStringContains(t, string(output), expected)
+				})
+			})
 		})
 
-		when("the order.toml exists", func() {
+		when("-order contains extensions", func() {
+			var containerName, copyDir, orderPath string
+
+			it.Before(func() {
+				containerName = "test-container-" + h.RandString(10)
+				var err error
+				copyDir, err = os.MkdirTemp("", "test-docker-copy-")
+				h.AssertNil(t, err)
+				orderPath, err = filepath.Abs(filepath.Join("testdata", "detector", "container", "cnb", "orders", "order_with_ext.toml"))
+				h.AssertNil(t, err)
+			})
+
+			it.After(func() {
+				if h.DockerContainerExists(t, containerName) {
+					h.Run(t, exec.Command("docker", "rm", containerName))
+				}
+				os.RemoveAll(copyDir)
+			})
+
 			it("processes the provided order.toml", func() {
-				h.DockerRunAndCopy(t,
+				output := h.DockerRunAndCopy(t,
 					containerName,
 					copyDir,
 					"/layers",
 					detectImage,
-					h.WithFlags("--user", userID,
-						"--volume", expectedOrderTOMLPath+":/custom/order.toml",
-						"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
+					h.WithFlags(
+						"--user", userID,
+						"--volume", orderPath+":/layers/order.toml",
+						"--env", "CNB_PLATFORM_API="+platformAPI,
+						"--env", "CNB_EXPERIMENTAL_MODE=warn", // required as the default is `error` if unset
 					),
 					h.WithArgs(
+						"-analyzed=/layers/analyzed.toml",
+						"-extensions=/cnb/extensions",
+						"-generated=/layers/generated",
 						"-log-level=debug",
-						"-order=/custom/order.toml",
+						"-run=/layers/run.toml", // /cnb/run.toml is the default location of run.toml
 					),
 				)
 
-				// check group.toml
+				t.Log("runs /bin/detect for buildpacks and extensions")
+				h.AssertStringContains(t, output, "Platform requested experimental feature 'Dockerfiles'")
+				h.AssertStringContains(t, output, "FOO=val-from-build-config")
+				h.AssertStringContains(t, output, "simple_extension: output from /bin/detect")
+				t.Log("writes group.toml")
 				foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
 				group, err := files.Handler.ReadGroup(foundGroupTOML)
 				h.AssertNil(t, err)
-				h.AssertEq(t, group.Group[0].ID, "simple_buildpack")
-				h.AssertEq(t, group.Group[0].Version, "simple_buildpack_version")
-			})
-		})
+				h.AssertEq(t, group.GroupExtensions[0].ID, "simple_extension")
+				h.AssertEq(t, group.GroupExtensions[0].Version, "simple_extension_version")
+				h.AssertEq(t, group.Group[0].ID, "buildpack_for_ext")
+				h.AssertEq(t, group.Group[0].Version, "buildpack_for_ext_version")
+				h.AssertEq(t, group.Group[0].Extension, false)
+				t.Log("writes plan.toml")
+				foundPlanTOML := filepath.Join(copyDir, "layers", "plan.toml")
+				buildPlan, err := files.Handler.ReadPlan(foundPlanTOML)
+				h.AssertNil(t, err)
+				h.AssertEq(t, len(buildPlan.Entries), 0) // this shows that the plan was filtered to remove `requires` provided by extensions
 
-		when("the order.toml does not exist", func() {
-			it("errors", func() {
-				command := exec.Command("docker", "run",
-					"--user", userID,
-					"--rm",
-					"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-					detectImage,
-					"-order=/custom/order.toml")
-				output, err := command.CombinedOutput()
-				h.AssertNotNil(t, err)
-				expected := "failed to initialize detector: reading order: failed to read order file: open /custom/order.toml: no such file or directory"
-				h.AssertStringContains(t, string(output), expected)
-			})
-		})
+				t.Log("runs /bin/generate for extensions")
+				h.AssertStringContains(t, output, "simple_extension: output from /bin/generate")
 
-		when("the order.toml contains a buildpack using an unsupported api", func() {
-			it("errors", func() {
-				command := exec.Command("docker", "run",
-					"--user", userID,
-					"--rm",
-					"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-					detectImage,
-					"-order=/cnb/orders/bad_api.toml")
-				output, err := command.CombinedOutput()
-				h.AssertNotNil(t, err)
-				failErr, ok := err.(*exec.ExitError)
-				if !ok {
-					t.Fatalf("expected an error of type exec.ExitError")
+				var dockerfilePath string
+				if api.MustParse(platformAPI).LessThan("0.13") {
+					t.Log("copies the generated Dockerfiles to the output directory")
+					dockerfilePath = filepath.Join(copyDir, "layers", "generated", "run", "simple_extension", "Dockerfile")
+				} else {
+					dockerfilePath = filepath.Join(copyDir, "layers", "generated", "simple_extension", "run.Dockerfile")
 				}
-				h.AssertEq(t, failErr.ExitCode(), 12) // platform code for buildpack api error
-				expected := "buildpack API version '0.1' is incompatible with the lifecycle"
-				h.AssertStringContains(t, string(output), expected)
+				h.AssertPathExists(t, dockerfilePath)
+				contents, err := os.ReadFile(dockerfilePath)
+				h.AssertEq(t, string(contents), "FROM some-run-image-from-extension\n")
+				t.Log("records the new run image in analyzed.toml")
+				foundAnalyzedTOML := filepath.Join(copyDir, "layers", "analyzed.toml")
+				analyzedMD, err := files.Handler.ReadAnalyzed(foundAnalyzedTOML, cmd.DefaultLogger)
+				h.AssertNil(t, err)
+				h.AssertEq(t, analyzedMD.RunImage.Image, "some-run-image-from-extension")
 			})
 		})
-	})
-
-	when("-order contains extensions", func() {
-		var containerName, copyDir, orderPath string
-
-		it.Before(func() {
-			containerName = "test-container-" + h.RandString(10)
-			var err error
-			copyDir, err = os.MkdirTemp("", "test-docker-copy-")
-			h.AssertNil(t, err)
-			orderPath, err = filepath.Abs(filepath.Join("testdata", "detector", "container", "cnb", "orders", "order_with_ext.toml"))
-			h.AssertNil(t, err)
-		})
-
-		it.After(func() {
-			if h.DockerContainerExists(t, containerName) {
-				h.Run(t, exec.Command("docker", "rm", containerName))
-			}
-			os.RemoveAll(copyDir)
-		})
-
-		it("processes the provided order.toml", func() {
-			output := h.DockerRunAndCopy(t,
-				containerName,
-				copyDir,
-				"/layers",
-				detectImage,
-				h.WithFlags(
-					"--user", userID,
-					"--volume", orderPath+":/layers/order.toml",
-					"--env", "CNB_PLATFORM_API="+latestPlatformAPI,
-					"--env", "CNB_EXPERIMENTAL_MODE=warn", // required as the default is `error` if unset
-				),
-				h.WithArgs(
-					"-analyzed=/layers/analyzed.toml",
-					"-extensions=/cnb/extensions",
-					"-generated=/layers/generated",
-					"-log-level=debug",
-					"-run=/layers/run.toml", // /cnb/run.toml is the default location of run.toml
-				),
-			)
-
-			t.Log("runs /bin/detect for buildpacks and extensions")
-			h.AssertStringContains(t, output, "Platform requested experimental feature 'Dockerfiles'")
-			h.AssertStringContains(t, output, "FOO=val-from-build-config")
-			h.AssertStringContains(t, output, "simple_extension: output from /bin/detect")
-			t.Log("writes group.toml")
-			foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
-			group, err := files.Handler.ReadGroup(foundGroupTOML)
-			h.AssertNil(t, err)
-			h.AssertEq(t, group.GroupExtensions[0].ID, "simple_extension")
-			h.AssertEq(t, group.GroupExtensions[0].Version, "simple_extension_version")
-			h.AssertEq(t, group.Group[0].ID, "buildpack_for_ext")
-			h.AssertEq(t, group.Group[0].Version, "buildpack_for_ext_version")
-			h.AssertEq(t, group.Group[0].Extension, false)
-			t.Log("writes plan.toml")
-			foundPlanTOML := filepath.Join(copyDir, "layers", "plan.toml")
-			buildPlan, err := files.Handler.ReadPlan(foundPlanTOML)
-			h.AssertNil(t, err)
-			h.AssertEq(t, len(buildPlan.Entries), 0) // this shows that the plan was filtered to remove `requires` provided by extensions
-
-			t.Log("runs /bin/generate for extensions")
-			h.AssertStringContains(t, output, "simple_extension: output from /bin/generate")
-			t.Log("copies the generated Dockerfiles to the output directory")
-			dockerfilePath := filepath.Join(copyDir, "layers", "generated", "run", "simple_extension", "Dockerfile")
-			h.AssertPathExists(t, dockerfilePath)
-			contents, err := os.ReadFile(dockerfilePath)
-			h.AssertEq(t, string(contents), "FROM some-run-image-from-extension\n")
-			t.Log("records the new run image in analyzed.toml")
-			foundAnalyzedTOML := filepath.Join(copyDir, "layers", "analyzed.toml")
-			analyzedMD, err := files.Handler.ReadAnalyzed(foundAnalyzedTOML, cmd.DefaultLogger)
-			h.AssertNil(t, err)
-			h.AssertEq(t, analyzedMD.RunImage.Image, "some-run-image-from-extension")
-		})
-	})
+	}
 }
diff --git a/acceptance/extender_test.go b/acceptance/extender_test.go
index 6e322f96..b1272e95 100644
--- a/acceptance/extender_test.go
+++ b/acceptance/extender_test.go
@@ -70,8 +70,13 @@ func TestExtender(t *testing.T) {
 
 func testExtenderFunc(platformAPI string) func(t *testing.T, when spec.G, it spec.S) {
 	return func(t *testing.T, when spec.G, it spec.S) {
+		var generatedDir = "/layers/generated"
+
 		it.Before(func() {
 			h.SkipIf(t, api.MustParse(platformAPI).LessThan("0.10"), "")
+			if api.MustParse(platformAPI).AtLeast("0.13") {
+				generatedDir = "/layers/generated-with-contexts"
+			}
 		})
 
 		when("kaniko case", func() {
@@ -133,7 +138,7 @@ func testExtenderFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 					extendArgs := []string{
 						ctrPath(extenderPath),
 						"-analyzed", "/layers/analyzed.toml",
-						"-generated", "/layers/generated",
+						"-generated", generatedDir,
 						"-log-level", "debug",
 						"-gid", "1000",
 						"-uid", "1234",
@@ -186,7 +191,7 @@ func testExtenderFunc(platformAPI string) func(t *testing.T, when spec.G, it spe
 						ctrPath(extenderPath),
 						"-analyzed", "/layers/analyzed.toml",
 						"-extended", "/layers/extended",
-						"-generated", "/layers/generated",
+						"-generated", generatedDir,
 						"-kind", "run",
 						"-log-level", "debug",
 						"-gid", "1000",
@@ -255,10 +260,16 @@ func assertExpectedImage(t *testing.T, imagePath, platformAPI string) {
 	h.AssertEq(t, configFile.Config.Labels["io.buildpacks.rebasable"], "false")
 	layers, err := image.Layers()
 	h.AssertNil(t, err)
-	h.AssertEq(t, len(layers), 5) // base (3), curl (1), tree (1)
-	if api.MustParse(platformAPI).AtLeast("0.12") {
-		history := configFile.History
-		h.AssertEq(t, len(history), len(configFile.RootFS.DiffIDs))
+	history := configFile.History
+	h.AssertEq(t, len(history), len(configFile.RootFS.DiffIDs))
+	if api.MustParse(platformAPI).AtLeast("0.13") {
+		h.AssertEq(t, len(layers), 7) // base (3), curl (2), tree (2)
+		h.AssertEq(t, history[3].CreatedBy, "Layer: 'RUN apt-get update && apt-get install -y curl', Created by extension: curl")
+		h.AssertEq(t, history[4].CreatedBy, "Layer: 'COPY run-file /', Created by extension: curl")
+		h.AssertEq(t, history[5].CreatedBy, "Layer: 'RUN apt-get update && apt-get install -y tree', Created by extension: tree")
+		h.AssertEq(t, history[6].CreatedBy, "Layer: 'COPY shared-file /shared-run', Created by extension: tree")
+	} else {
+		h.AssertEq(t, len(layers), 5) // base (3), curl (1), tree (1)
 		h.AssertEq(t, history[3].CreatedBy, "Layer: 'RUN apt-get update && apt-get install -y curl', Created by extension: curl")
 		h.AssertEq(t, history[4].CreatedBy, "Layer: 'RUN apt-get update && apt-get install -y tree', Created by extension: tree")
 	}
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/build.Dockerfile b/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/build.Dockerfile
new file mode 100644
index 00000000..26252787
--- /dev/null
+++ b/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/build.Dockerfile
@@ -0,0 +1,9 @@
+ARG base_image
+FROM ${base_image}
+
+USER root
+RUN apt-get update && apt-get install -y curl
+COPY build-file /
+
+ARG build_id=0
+RUN echo ${build_id}
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/context.build/build-file b/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/context.build/build-file
new file mode 100644
index 00000000..e69de29b
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/context.run/run-file b/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/context.run/run-file
new file mode 100644
index 00000000..e69de29b
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/run.Dockerfile b/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/run.Dockerfile
new file mode 100644
index 00000000..da0154db
--- /dev/null
+++ b/acceptance/testdata/extender/container/layers/generated-with-contexts/curl/run.Dockerfile
@@ -0,0 +1,15 @@
+ARG base_image
+FROM ${base_image}
+
+USER root
+RUN apt-get update && apt-get install -y curl
+COPY run-file /
+
+ARG build_id=0
+RUN echo ${build_id}
+
+# This should not create a layer as the path should be ignored by kaniko
+RUN echo "some-content" > /workspace/some-file
+
+ARG user_id
+USER ${user_id}
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/build.Dockerfile b/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/build.Dockerfile
new file mode 100644
index 00000000..27883776
--- /dev/null
+++ b/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/build.Dockerfile
@@ -0,0 +1,11 @@
+ARG base_image
+FROM ${base_image}
+
+USER root
+RUN apt-get update && apt-get install -y tree
+COPY shared-file /shared-build
+
+ENV CNB_STACK_ID=stack-id-from-ext-tree
+
+ARG build_id=0
+RUN echo ${build_id}
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/context/shared-file b/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/context/shared-file
new file mode 100644
index 00000000..e69de29b
diff --git a/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/run.Dockerfile b/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/run.Dockerfile
new file mode 100644
index 00000000..84c2d734
--- /dev/null
+++ b/acceptance/testdata/extender/container/layers/generated-with-contexts/tree/run.Dockerfile
@@ -0,0 +1,17 @@
+ARG base_image
+FROM ${base_image}
+
+USER root
+RUN apt-get update && apt-get install -y tree
+COPY shared-file /shared-run
+
+ENV CNB_STACK_ID=stack-id-from-ext-tree
+
+ARG build_id=0
+RUN echo ${build_id}
+
+# tree is not really rebasable, but we set the label here to test that the label in the extended image is set correctly
+LABEL io.buildpacks.rebasable=true
+
+ARG user_id
+USER ${user_id}
diff --git a/internal/extend/buildcontext_test.go b/internal/extend/buildcontext_test.go
new file mode 100644
index 00000000..dc189d90
--- /dev/null
+++ b/internal/extend/buildcontext_test.go
@@ -0,0 +1,96 @@
+package extend_test
+
+import (
+	"io"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/sclevine/spec"
+	"github.com/sclevine/spec/report"
+
+	"github.com/buildpacks/lifecycle/internal/extend"
+	"github.com/buildpacks/lifecycle/log"
+	h "github.com/buildpacks/lifecycle/testhelpers"
+)
+
+func TestContext(t *testing.T) {
+	spec.Run(t, "unit-context", testContext, spec.Report(report.Terminal{}))
+}
+
+func testContext(t *testing.T, when spec.G, it spec.S) {
+	var tmpDir string
+	var logger log.Logger
+
+	it.Before(func() {
+		var err error
+		tmpDir, err = os.MkdirTemp("", "contexts")
+		logger = log.NewDefaultLogger(io.Discard)
+
+		h.AssertNil(t, err)
+	})
+
+	it.After(func() {
+		_ = os.RemoveAll(tmpDir)
+	})
+
+	when("#FindContexts", func() {
+		when("only shared context is provided", func() {
+			it.Before(func() {
+				h.Mkdir(t, filepath.Join(tmpDir, extend.SharedContextDir))
+			})
+
+			it("succeeds", func() {
+				contexts, err := extend.FindContexts("A", tmpDir, logger)
+				h.AssertNil(t, err)
+
+				h.AssertEq(t, len(contexts), 1)
+				h.AssertEq(t, contexts[0], extend.ContextInfo{
+					ExtensionID: "A",
+					Path:        filepath.Join(tmpDir, extend.SharedContextDir),
+				})
+			})
+		})
+
+		when("image specific contexts are provided", func() {
+			it.Before(func() {
+				h.Mkdir(t, filepath.Join(tmpDir, extend.RunContextDir), filepath.Join(tmpDir, extend.BuildContextDir))
+			})
+
+			it("succeeds", func() {
+				contexts, err := extend.FindContexts("A", tmpDir, logger)
+				h.AssertNil(t, err)
+
+				h.AssertEq(t, len(contexts), 2)
+				h.AssertEq(t, contexts[0], extend.ContextInfo{
+					ExtensionID: "A",
+					Path:        filepath.Join(tmpDir, extend.BuildContextDir),
+				})
+				h.AssertEq(t, contexts[1], extend.ContextInfo{
+					ExtensionID: "A",
+					Path:        filepath.Join(tmpDir, extend.RunContextDir),
+				})
+			})
+		})
+
+		when("no context is provided", func() {
+			it("succeeds", func() {
+				contexts, err := extend.FindContexts("A", tmpDir, logger)
+				h.AssertNil(t, err)
+
+				h.AssertEq(t, len(contexts), 0)
+			})
+		})
+
+		when("shared and image-specific contexts are provided", func() {
+			it.Before(func() {
+				h.Mkdir(t, filepath.Join(tmpDir, extend.SharedContextDir), filepath.Join(tmpDir, extend.BuildContextDir))
+			})
+
+			it("fails", func() {
+				_, err := extend.FindContexts("A", tmpDir, logger)
+				h.AssertNotNil(t, err)
+			})
+		})
+	})
+}
diff --git a/internal/extend/config_test.go b/internal/extend/config_test.go
index 23163e89..e4022eb2 100644
--- a/internal/extend/config_test.go
+++ b/internal/extend/config_test.go
@@ -12,8 +12,8 @@ import (
 	h "github.com/buildpacks/lifecycle/testhelpers"
 )
 
-func TestGenerate(t *testing.T) {
-	spec.Run(t, "unit-extend", testConfig, spec.Report(report.Terminal{}))
+func TestConfig(t *testing.T) {
+	spec.Run(t, "unit-config", testConfig, spec.Report(report.Terminal{}))
 }
 
 func testConfig(t *testing.T, when spec.G, it spec.S) {
diff --git a/phase/extender_test.go b/phase/extender_test.go
index a5f15a60..567c0ae4 100644
--- a/phase/extender_test.go
+++ b/phase/extender_test.go
@@ -196,305 +196,622 @@ func testExtender(t *testing.T, when spec.G, it spec.S) {
 			mockCtrl.Finish()
 		})
 
-		when("build base image", func() {
-			it("applies the Dockerfile with the expected args and opts", func() {
-				expectedDockerfileA := extend.Dockerfile{
-					Path: filepath.Join(generatedDir, "build", "A", "Dockerfile"),
-					Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
-				}
-				h.Mkdir(t, filepath.Join(generatedDir, "build", "A"))
-				h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "build", "A", "Dockerfile"))
-				buf := new(bytes.Buffer)
-				data := extend.Config{Build: extend.BuildConfig{Args: expectedDockerfileA.Args}}
-				h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
-				h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "build", "A", "extend-config.toml"))
-
-				expectedDockerfileB := extend.Dockerfile{
-					Path: filepath.Join(generatedDir, "build", "B", "Dockerfile"),
-					Args: []extend.Arg{{Name: "argB", Value: "valueB"}},
+		when("using the platform API 0.12", func() {
+			it.Before(func() {
+				extender.PlatformAPI = api.MustParse("0.12")
+			})
+
+			when("build base image", func() {
+				it("applies the Dockerfile with the expected args and opts", func() {
+					expectedDockerfileA := extend.Dockerfile{
+						Path: filepath.Join(generatedDir, "build", "A", "Dockerfile"),
+						Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
+					}
+					h.Mkdir(t, filepath.Join(generatedDir, "build", "A"))
+					h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "build", "A", "Dockerfile"))
+					buf := new(bytes.Buffer)
+					data := extend.Config{Build: extend.BuildConfig{Args: expectedDockerfileA.Args}}
+					h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+					h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "build", "A", "extend-config.toml"))
+
+					expectedDockerfileB := extend.Dockerfile{
+						Path: filepath.Join(generatedDir, "build", "B", "Dockerfile"),
+						Args: []extend.Arg{{Name: "argB", Value: "valueB"}},
+					}
+					h.Mkdir(t, filepath.Join(generatedDir, "build", "B"))
+					h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "build", "B", "Dockerfile"))
+					buf = new(bytes.Buffer)
+					data = extend.Config{Build: extend.BuildConfig{Args: expectedDockerfileB.Args}}
+					h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+					h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "build", "B", "extend-config.toml"))
+
+					fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
+					someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
+
+					// first dockerfile
+
+					firstConfig := &v1.ConfigFile{Config: v1.Config{
+						User: "0:5678",
+					}}
+					someFakeImage.ConfigFileReturnsOnCall(0, firstConfig, nil)
+					fakeDockerfileApplier.EXPECT().Apply(
+						gomock.Any(),
+						gomock.Any(), // we mutate the provided image so we can't expect the fake image
+						extend.Options{
+							BuildContext: "some-app-dir",
+							IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+							CacheTTL:     7 * (24 * time.Hour),
+						},
+						logger,
+					).DoAndReturn(
+						func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+							h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
+							h.AssertEq(t, len(dockerfile.Args), 4)
+							h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+							_, err := uuid.Parse(dockerfile.Args[0].Value)
+							h.AssertNil(t, err)
+							h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+							h.AssertEq(t, dockerfile.Args[1].Value, "0")
+							h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+							h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+							h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
+
+							return someFakeImage, nil
+						})
+					secondConfig := &v1.ConfigFile{Config: v1.Config{
+						User: "2345:6789",
+						Env:  []string{"SOME_VAR=some-val"},
+					}}
+					someFakeImage.ConfigFileReturnsOnCall(1, secondConfig, nil)
+
+					// second dockerfile
+
+					someFakeImage.ConfigFileReturnsOnCall(2, secondConfig, nil)
+					fakeDockerfileApplier.EXPECT().Apply(
+						gomock.Any(),
+						someFakeImage,
+						extend.Options{
+							BuildContext: "some-app-dir",
+							IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+							CacheTTL:     7 * (24 * time.Hour),
+						},
+						logger,
+					).DoAndReturn(
+						func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+							h.AssertEq(t, dockerfile.Path, expectedDockerfileB.Path)
+							h.AssertEq(t, len(dockerfile.Args), 4)
+							h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+							_, err := uuid.Parse(dockerfile.Args[0].Value)
+							h.AssertNil(t, err)
+							h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+							h.AssertEq(t, dockerfile.Args[1].Value, "2345")
+							h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+							h.AssertEq(t, dockerfile.Args[2].Value, "6789")
+							h.AssertEq(t, dockerfile.Args[3], expectedDockerfileB.Args[0])
+
+							return someFakeImage, nil
+						})
+					someFakeImage.ConfigFileReturnsOnCall(3, secondConfig, nil)
+
+					fakeDockerfileApplier.EXPECT().Cleanup().Return(nil)
+
+					h.AssertNil(t, extender.Extend("build", logger))
+					h.AssertEq(t, os.Getenv("SOME_VAR"), "some-val")
+					h.AssertNil(t, os.Unsetenv("SOME_VAR"))
+				})
+			})
+
+			when("run base image", func() {
+				type testCase struct {
+					firstDockerfileRebasable  bool
+					secondDockerfileRebasable bool
+					expectedImageSHA          string
 				}
-				h.Mkdir(t, filepath.Join(generatedDir, "build", "B"))
-				h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "build", "B", "Dockerfile"))
-				buf = new(bytes.Buffer)
-				data = extend.Config{Build: extend.BuildConfig{Args: expectedDockerfileB.Args}}
-				h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
-				h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "build", "B", "extend-config.toml"))
-
-				fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
-				someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
-
-				// first dockerfile
-
-				firstConfig := &v1.ConfigFile{Config: v1.Config{
-					User: "0:5678",
-				}}
-				someFakeImage.ConfigFileReturnsOnCall(0, firstConfig, nil)
-				fakeDockerfileApplier.EXPECT().Apply(
-					gomock.Any(),
-					gomock.Any(), // we mutate the provided image so we can't expect the fake image
-					extend.Options{
-						BuildContext: "some-app-dir",
-						IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
-						CacheTTL:     7 * (24 * time.Hour),
+				var (
+					rebasableSHA    = "sha256:2407b98f3bcee33d24f47fc3d7beaf0928b3fa799a8a84aa9f9f239fcded6e62"
+					notRebasableSHA = "sha256:cc3b90b798b76e7d41de01d4f02f2917694ef0b09d0d923314c8d6c31c4ae8e9"
+				)
+				for _, tc := range []testCase{
+					{
+						firstDockerfileRebasable:  true,
+						secondDockerfileRebasable: true,
+						expectedImageSHA:          rebasableSHA,
 					},
-					logger,
-				).DoAndReturn(
-					func(dockerfile extend.Dockerfile, toBaseImage v1.Image, withBuildOptions extend.Options, logger llog.Logger) (v1.Image, error) {
-						h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
-						h.AssertEq(t, len(dockerfile.Args), 4)
-						h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
-						_, err := uuid.Parse(dockerfile.Args[0].Value)
-						h.AssertNil(t, err)
-						h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
-						h.AssertEq(t, dockerfile.Args[1].Value, "0")
-						h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
-						h.AssertEq(t, dockerfile.Args[2].Value, "5678")
-						h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
-
-						return someFakeImage, nil
-					})
-				secondConfig := &v1.ConfigFile{Config: v1.Config{
-					User: "2345:6789",
-					Env:  []string{"SOME_VAR=some-val"},
-				}}
-				someFakeImage.ConfigFileReturnsOnCall(1, secondConfig, nil)
-
-				// second dockerfile
-
-				someFakeImage.ConfigFileReturnsOnCall(2, secondConfig, nil)
-				fakeDockerfileApplier.EXPECT().Apply(
-					gomock.Any(),
-					someFakeImage,
-					extend.Options{
-						BuildContext: "some-app-dir",
-						IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
-						CacheTTL:     7 * (24 * time.Hour),
+					{
+						firstDockerfileRebasable:  true,
+						secondDockerfileRebasable: false,
+						expectedImageSHA:          notRebasableSHA,
+					},
+					{
+						firstDockerfileRebasable:  false,
+						secondDockerfileRebasable: true,
+						expectedImageSHA:          notRebasableSHA,
 					},
-					logger,
-				).DoAndReturn(
-					func(dockerfile extend.Dockerfile, toBaseImage v1.Image, withBuildOptions extend.Options, logger llog.Logger) (v1.Image, error) {
-						h.AssertEq(t, dockerfile.Path, expectedDockerfileB.Path)
-						h.AssertEq(t, len(dockerfile.Args), 4)
-						h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
-						_, err := uuid.Parse(dockerfile.Args[0].Value)
-						h.AssertNil(t, err)
-						h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
-						h.AssertEq(t, dockerfile.Args[1].Value, "2345")
-						h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
-						h.AssertEq(t, dockerfile.Args[2].Value, "6789")
-						h.AssertEq(t, dockerfile.Args[3], expectedDockerfileB.Args[0])
-
-						return someFakeImage, nil
+					{
+						firstDockerfileRebasable:  false,
+						secondDockerfileRebasable: false,
+						expectedImageSHA:          notRebasableSHA,
+					},
+				} {
+					tc := tc
+					when := when
+					desc := func(b bool) string {
+						if b {
+							return "rebasable"
+						}
+						return "not rebasable"
+					}
+					when(fmt.Sprintf("first Dockerfile is %s, second Dockerfile is %s", desc(tc.firstDockerfileRebasable), desc(tc.secondDockerfileRebasable)), func() {
+						it("applies the Dockerfile with the expected args and opts", func() {
+							expectedDockerfileA := extend.Dockerfile{
+								Path: filepath.Join(generatedDir, "run", "A", "Dockerfile"),
+								Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
+							}
+							h.Mkdir(t, filepath.Join(generatedDir, "run", "A"))
+							h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "run", "A", "Dockerfile"))
+							buf := new(bytes.Buffer)
+							data := extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileA.Args}}
+							h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+							h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "run", "A", "extend-config.toml"))
+
+							expectedDockerfileB := extend.Dockerfile{
+								Path: filepath.Join(generatedDir, "run", "B", "Dockerfile"),
+								Args: []extend.Arg{{Name: "argB", Value: "valueB"}},
+							}
+							h.Mkdir(t, filepath.Join(generatedDir, "run", "B"))
+							h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "run", "B", "Dockerfile"))
+							buf = new(bytes.Buffer)
+							data = extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileB.Args}}
+							h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+							h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "run", "B", "extend-config.toml"))
+
+							fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
+							someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
+							someFakeImage.ConfigFileReturnsOnCall(0, &v1.ConfigFile{Config: v1.Config{
+								User: "1234:5678",
+							}}, nil)
+
+							// first dockerfile
+
+							fakeDockerfileApplier.EXPECT().Apply(
+								gomock.Any(),
+								gomock.Any(), // we mutate the provided image so we can't expect the fake image
+								extend.Options{
+									BuildContext: "some-app-dir",
+									IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+									CacheTTL:     7 * (24 * time.Hour),
+								},
+								logger,
+							).DoAndReturn(
+								func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+									h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
+									h.AssertEq(t, len(dockerfile.Args), 4) // build_id, user_id, group_id
+									h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+									_, err := uuid.Parse(dockerfile.Args[0].Value)
+									h.AssertNil(t, err)
+									h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+									h.AssertEq(t, dockerfile.Args[1].Value, "1234")
+									h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+									h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+									h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
+
+									return someFakeImage, nil
+								})
+							firstConfig := &v1.ConfigFile{Config: v1.Config{
+								User:   "1234:5678",
+								Labels: map[string]string{phase.RebasableLabel: fmt.Sprintf("%t", tc.firstDockerfileRebasable)},
+							}}
+							someFakeImage.ConfigFileReturnsOnCall(1, firstConfig, nil)
+
+							// second dockerfile
+
+							fakeDockerfileApplier.EXPECT().Apply(
+								gomock.Any(),
+								someFakeImage,
+								extend.Options{
+									BuildContext: "some-app-dir",
+									IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+									CacheTTL:     7 * (24 * time.Hour),
+								},
+								logger,
+							).DoAndReturn(
+								func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+									h.AssertEq(t, dockerfile.Path, expectedDockerfileB.Path)
+									h.AssertEq(t, len(dockerfile.Args), 4) // build_id, user_id, group_id
+									h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+									_, err := uuid.Parse(dockerfile.Args[0].Value)
+									h.AssertNil(t, err)
+									h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+									h.AssertEq(t, dockerfile.Args[1].Value, "1234")
+									h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+									h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+									h.AssertEq(t, dockerfile.Args[3], expectedDockerfileB.Args[0])
+
+									return someFakeImage, nil
+								})
+							secondConfig := &v1.ConfigFile{Config: v1.Config{
+								User:   "1234:5678",
+								Labels: map[string]string{phase.RebasableLabel: fmt.Sprintf("%t", tc.secondDockerfileRebasable)},
+							}}
+							someFakeImage.ConfigFileReturnsOnCall(2, secondConfig, nil)
+
+							// set label
+
+							someFakeImage.ConfigFileReturnsOnCall(3, secondConfig, nil)
+							someFakeImage.ConfigFileReturnsOnCall(4, secondConfig, nil)
+
+							// save selective
+
+							imageHash := v1.Hash{Algorithm: "sha256", Hex: "some-image-hex"}
+							someFakeImage.DigestReturns(imageHash, nil)
+							someFakeImage.ConfigNameReturns(v1.Hash{Algorithm: "sha256", Hex: "some-config-hex"}, nil)
+
+							fakeDockerfileApplier.EXPECT().Cleanup().Return(nil)
+
+							h.AssertNil(t, extender.Extend("run", logger))
+							outputImagePath := filepath.Join(extendedDir, "run", tc.expectedImageSHA)
+							h.AssertPathExists(t, outputImagePath)
+							fis, err := os.ReadDir(outputImagePath)
+							h.AssertNil(t, err)
+							h.AssertEq(t, len(fis), 3)
+						})
 					})
-				someFakeImage.ConfigFileReturnsOnCall(3, secondConfig, nil)
-
-				fakeDockerfileApplier.EXPECT().Cleanup().Return(nil)
+				}
 
-				h.AssertNil(t, extender.Extend("build", logger))
-				h.AssertEq(t, os.Getenv("SOME_VAR"), "some-val")
-				h.AssertNil(t, os.Unsetenv("SOME_VAR"))
+				it("errors if the last extension leaves the user as root", func() {
+					expectedDockerfileA := extend.Dockerfile{
+						Path: filepath.Join(generatedDir, "run", "A", "Dockerfile"),
+						Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
+					}
+					h.Mkdir(t, filepath.Join(generatedDir, "run", "A"))
+					h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "run", "A", "Dockerfile"))
+					buf := new(bytes.Buffer)
+					data := extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileA.Args}}
+					h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+					h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "run", "A", "extend-config.toml"))
+
+					fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
+					firstConfig := &v1.ConfigFile{Config: v1.Config{
+						User: "0:5678",
+					}}
+					someFakeImage.ConfigFileReturns(firstConfig, nil)
+					someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
+
+					fakeDockerfileApplier.EXPECT().Apply(
+						gomock.Any(),
+						gomock.Any(),
+						gomock.Any(),
+						logger,
+					).DoAndReturn(
+						func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+							h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
+							h.AssertEq(t, len(dockerfile.Args), 4)
+							h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+							_, err := uuid.Parse(dockerfile.Args[0].Value)
+							h.AssertNil(t, err)
+							h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+							h.AssertEq(t, dockerfile.Args[1].Value, "0")
+							h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+							h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+							h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
+
+							return someFakeImage, nil
+						})
+
+					err := extender.Extend("run", logger)
+					h.AssertError(t, err, "extending run image: the final user ID is 0 (root); please add another extension that resets the user to non-root")
+				})
 			})
 		})
 
-		when("run base image", func() {
-			type testCase struct {
-				firstDockerfileRebasable  bool
-				secondDockerfileRebasable bool
-				expectedImageSHA          string
-			}
-			var (
-				rebasableSHA    = "sha256:2407b98f3bcee33d24f47fc3d7beaf0928b3fa799a8a84aa9f9f239fcded6e62"
-				notRebasableSHA = "sha256:cc3b90b798b76e7d41de01d4f02f2917694ef0b09d0d923314c8d6c31c4ae8e9"
-			)
-			for _, tc := range []testCase{
-				{
-					firstDockerfileRebasable:  true,
-					secondDockerfileRebasable: true,
-					expectedImageSHA:          rebasableSHA,
-				},
-				{
-					firstDockerfileRebasable:  true,
-					secondDockerfileRebasable: false,
-					expectedImageSHA:          notRebasableSHA,
-				},
-				{
-					firstDockerfileRebasable:  false,
-					secondDockerfileRebasable: true,
-					expectedImageSHA:          notRebasableSHA,
-				},
-				{
-					firstDockerfileRebasable:  false,
-					secondDockerfileRebasable: false,
-					expectedImageSHA:          notRebasableSHA,
-				},
-			} {
-				tc := tc
-				when := when
-				desc := func(b bool) string {
-					if b {
-						return "rebasable"
+		when("using platform API 0.13", func() {
+			it.Before(func() {
+				extender.PlatformAPI = api.MustParse("0.13")
+			})
+
+			when("build base image", func() {
+				it("applies the Dockerfile with the expected args and opts", func() {
+					expectedDockerfileA := extend.Dockerfile{
+						Path: filepath.Join(generatedDir, "A", "build.Dockerfile"),
+						Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
 					}
-					return "not rebasable"
-				}
-				when(fmt.Sprintf("first Dockerfile is %s, second Dockerfile is %s", desc(tc.firstDockerfileRebasable), desc(tc.secondDockerfileRebasable)), func() {
-					it("applies the Dockerfile with the expected args and opts", func() {
-						expectedDockerfileA := extend.Dockerfile{
-							Path: filepath.Join(generatedDir, "run", "A", "Dockerfile"),
-							Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
-						}
-						h.Mkdir(t, filepath.Join(generatedDir, "run", "A"))
-						h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "run", "A", "Dockerfile"))
-						buf := new(bytes.Buffer)
-						data := extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileA.Args}}
-						h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
-						h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "run", "A", "extend-config.toml"))
-
-						expectedDockerfileB := extend.Dockerfile{
-							Path: filepath.Join(generatedDir, "run", "B", "Dockerfile"),
-							Args: []extend.Arg{{Name: "argB", Value: "valueB"}},
-						}
-						h.Mkdir(t, filepath.Join(generatedDir, "run", "B"))
-						h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "run", "B", "Dockerfile"))
-						buf = new(bytes.Buffer)
-						data = extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileB.Args}}
-						h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
-						h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "run", "B", "extend-config.toml"))
-
-						fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
-						someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
-						someFakeImage.ConfigFileReturnsOnCall(0, &v1.ConfigFile{Config: v1.Config{
-							User: "1234:5678",
-						}}, nil)
-
-						// first dockerfile
-
-						fakeDockerfileApplier.EXPECT().Apply(
-							gomock.Any(),
-							gomock.Any(), // we mutate the provided image so we can't expect the fake image
-							extend.Options{
-								BuildContext: "some-app-dir",
-								IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
-								CacheTTL:     7 * (24 * time.Hour),
-							},
-							logger,
-						).DoAndReturn(
-							func(dockerfile extend.Dockerfile, toBaseImage v1.Image, withBuildOptions extend.Options, logger llog.Logger) (v1.Image, error) {
-								h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
-								h.AssertEq(t, len(dockerfile.Args), 4) // build_id, user_id, group_id
-								h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
-								_, err := uuid.Parse(dockerfile.Args[0].Value)
-								h.AssertNil(t, err)
-								h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
-								h.AssertEq(t, dockerfile.Args[1].Value, "1234")
-								h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
-								h.AssertEq(t, dockerfile.Args[2].Value, "5678")
-								h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
-
-								return someFakeImage, nil
-							})
-						firstConfig := &v1.ConfigFile{Config: v1.Config{
-							User:   "1234:5678",
-							Labels: map[string]string{phase.RebasableLabel: fmt.Sprintf("%t", tc.firstDockerfileRebasable)},
-						}}
-						someFakeImage.ConfigFileReturnsOnCall(1, firstConfig, nil)
-
-						// second dockerfile
-
-						fakeDockerfileApplier.EXPECT().Apply(
-							gomock.Any(),
-							someFakeImage,
-							extend.Options{
-								BuildContext: "some-app-dir",
-								IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
-								CacheTTL:     7 * (24 * time.Hour),
-							},
-							logger,
-						).DoAndReturn(
-							func(dockerfile extend.Dockerfile, toBaseImage v1.Image, withBuildOptions extend.Options, logger llog.Logger) (v1.Image, error) {
-								h.AssertEq(t, dockerfile.Path, expectedDockerfileB.Path)
-								h.AssertEq(t, len(dockerfile.Args), 4) // build_id, user_id, group_id
-								h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
-								_, err := uuid.Parse(dockerfile.Args[0].Value)
-								h.AssertNil(t, err)
-								h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
-								h.AssertEq(t, dockerfile.Args[1].Value, "1234")
-								h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
-								h.AssertEq(t, dockerfile.Args[2].Value, "5678")
-								h.AssertEq(t, dockerfile.Args[3], expectedDockerfileB.Args[0])
-
-								return someFakeImage, nil
-							})
-						secondConfig := &v1.ConfigFile{Config: v1.Config{
-							User:   "1234:5678",
-							Labels: map[string]string{phase.RebasableLabel: fmt.Sprintf("%t", tc.secondDockerfileRebasable)},
-						}}
-						someFakeImage.ConfigFileReturnsOnCall(2, secondConfig, nil)
-
-						// set label
-
-						someFakeImage.ConfigFileReturnsOnCall(3, secondConfig, nil)
-						someFakeImage.ConfigFileReturnsOnCall(4, secondConfig, nil)
-
-						// save without base layers
-
-						imageHash := v1.Hash{Algorithm: "sha256", Hex: "some-image-hex"}
-						someFakeImage.DigestReturns(imageHash, nil)
-						someFakeImage.ConfigNameReturns(v1.Hash{Algorithm: "sha256", Hex: "some-config-hex"}, nil)
-
-						fakeDockerfileApplier.EXPECT().Cleanup().Return(nil)
-
-						h.AssertNil(t, extender.Extend("run", logger))
-						outputImagePath := filepath.Join(extendedDir, "run", tc.expectedImageSHA)
-						h.AssertPathExists(t, outputImagePath)
-						fis, err := os.ReadDir(outputImagePath)
-						h.AssertNil(t, err)
-						h.AssertEq(t, len(fis), 3)
-					})
+					h.Mkdir(t, filepath.Join(generatedDir, "A"))
+					h.Mkdir(t, filepath.Join(generatedDir, "A", "context.build"))
+					h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "A", "build.Dockerfile"))
+					buf := new(bytes.Buffer)
+					data := extend.Config{Build: extend.BuildConfig{Args: expectedDockerfileA.Args}}
+					h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+					h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "A", "extend-config.toml"))
+
+					expectedDockerfileB := extend.Dockerfile{
+						Path:       filepath.Join(generatedDir, "B", "build.Dockerfile"),
+						Args:       []extend.Arg{{Name: "argB", Value: "valueB"}},
+						ContextDir: "dirB",
+					}
+					h.Mkdir(t, filepath.Join(generatedDir, "B"))
+					h.Mkdir(t, filepath.Join(generatedDir, "B", "context"))
+					h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "B", "build.Dockerfile"))
+					buf = new(bytes.Buffer)
+					data = extend.Config{Build: extend.BuildConfig{Args: expectedDockerfileB.Args}}
+					h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+					h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "B", "extend-config.toml"))
+
+					fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
+					someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
+
+					// first dockerfile
+
+					firstConfig := &v1.ConfigFile{Config: v1.Config{
+						User: "0:5678",
+					}}
+					someFakeImage.ConfigFileReturnsOnCall(0, firstConfig, nil)
+					fakeDockerfileApplier.EXPECT().Apply(
+						gomock.Any(),
+						gomock.Any(), // we mutate the provided image so we can't expect the fake image
+						extend.Options{
+							BuildContext: filepath.Join(generatedDir, "A", "context.build"),
+							IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+							CacheTTL:     7 * (24 * time.Hour),
+						},
+						logger,
+					).DoAndReturn(
+						func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+							h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
+							h.AssertEq(t, len(dockerfile.Args), 4)
+							h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+							_, err := uuid.Parse(dockerfile.Args[0].Value)
+							h.AssertNil(t, err)
+							h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+							h.AssertEq(t, dockerfile.Args[1].Value, "0")
+							h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+							h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+							h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
+
+							return someFakeImage, nil
+						})
+					secondConfig := &v1.ConfigFile{Config: v1.Config{
+						User: "2345:6789",
+						Env:  []string{"SOME_VAR=some-val"},
+					}}
+					someFakeImage.ConfigFileReturnsOnCall(1, secondConfig, nil)
+
+					// second dockerfile
+
+					someFakeImage.ConfigFileReturnsOnCall(2, secondConfig, nil)
+					fakeDockerfileApplier.EXPECT().Apply(
+						gomock.Any(),
+						someFakeImage,
+						extend.Options{
+							BuildContext: filepath.Join(generatedDir, "B", "context"),
+							IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+							CacheTTL:     7 * (24 * time.Hour),
+						},
+						logger,
+					).DoAndReturn(
+						func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+							h.AssertEq(t, dockerfile.Path, expectedDockerfileB.Path)
+							h.AssertEq(t, len(dockerfile.Args), 4)
+							h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+							_, err := uuid.Parse(dockerfile.Args[0].Value)
+							h.AssertNil(t, err)
+							h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+							h.AssertEq(t, dockerfile.Args[1].Value, "2345")
+							h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+							h.AssertEq(t, dockerfile.Args[2].Value, "6789")
+							h.AssertEq(t, dockerfile.Args[3], expectedDockerfileB.Args[0])
+
+							return someFakeImage, nil
+						})
+					someFakeImage.ConfigFileReturnsOnCall(3, secondConfig, nil)
+
+					fakeDockerfileApplier.EXPECT().Cleanup().Return(nil)
+
+					h.AssertNil(t, extender.Extend("build", logger))
+					h.AssertEq(t, os.Getenv("SOME_VAR"), "some-val")
+					h.AssertNil(t, os.Unsetenv("SOME_VAR"))
 				})
-			}
+			})
 
-			it("errors if the last extension leaves the user as root", func() {
-				expectedDockerfileA := extend.Dockerfile{
-					Path: filepath.Join(generatedDir, "run", "A", "Dockerfile"),
-					Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
+			when("run base image", func() {
+				type testCase struct {
+					firstDockerfileRebasable  bool
+					secondDockerfileRebasable bool
+					expectedImageSHA          string
 				}
-				h.Mkdir(t, filepath.Join(generatedDir, "run", "A"))
-				h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "run", "A", "Dockerfile"))
-				buf := new(bytes.Buffer)
-				data := extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileA.Args}}
-				h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
-				h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "run", "A", "extend-config.toml"))
-
-				fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
-				firstConfig := &v1.ConfigFile{Config: v1.Config{
-					User: "0:5678",
-				}}
-				someFakeImage.ConfigFileReturns(firstConfig, nil)
-				someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
-
-				fakeDockerfileApplier.EXPECT().Apply(
-					gomock.Any(),
-					gomock.Any(),
-					gomock.Any(),
-					logger,
-				).DoAndReturn(
-					func(dockerfile extend.Dockerfile, toBaseImage v1.Image, withBuildOptions extend.Options, logger llog.Logger) (v1.Image, error) {
-						h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
-						h.AssertEq(t, len(dockerfile.Args), 4)
-						h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
-						_, err := uuid.Parse(dockerfile.Args[0].Value)
-						h.AssertNil(t, err)
-						h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
-						h.AssertEq(t, dockerfile.Args[1].Value, "0")
-						h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
-						h.AssertEq(t, dockerfile.Args[2].Value, "5678")
-						h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
-
-						return someFakeImage, nil
+				var (
+					rebasableSHA    = "sha256:2407b98f3bcee33d24f47fc3d7beaf0928b3fa799a8a84aa9f9f239fcded6e62"
+					notRebasableSHA = "sha256:cc3b90b798b76e7d41de01d4f02f2917694ef0b09d0d923314c8d6c31c4ae8e9"
+				)
+				for _, tc := range []testCase{
+					{
+						firstDockerfileRebasable:  true,
+						secondDockerfileRebasable: true,
+						expectedImageSHA:          rebasableSHA,
+					},
+					{
+						firstDockerfileRebasable:  true,
+						secondDockerfileRebasable: false,
+						expectedImageSHA:          notRebasableSHA,
+					},
+					{
+						firstDockerfileRebasable:  false,
+						secondDockerfileRebasable: true,
+						expectedImageSHA:          notRebasableSHA,
+					},
+					{
+						firstDockerfileRebasable:  false,
+						secondDockerfileRebasable: false,
+						expectedImageSHA:          notRebasableSHA,
+					},
+				} {
+					tc := tc
+					when := when
+					desc := func(b bool) string {
+						if b {
+							return "rebasable"
+						}
+						return "not rebasable"
+					}
+					when(fmt.Sprintf("first Dockerfile is %s, second Dockerfile is %s", desc(tc.firstDockerfileRebasable), desc(tc.secondDockerfileRebasable)), func() {
+						it("applies the Dockerfile with the expected args and opts", func() {
+							expectedDockerfileA := extend.Dockerfile{
+								Path: filepath.Join(generatedDir, "A", "run.Dockerfile"),
+								Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
+							}
+							h.Mkdir(t, filepath.Join(generatedDir, "A"))
+							h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "A", "run.Dockerfile"))
+							buf := new(bytes.Buffer)
+							data := extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileA.Args}}
+							h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+							h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "A", "extend-config.toml"))
+
+							expectedDockerfileB := extend.Dockerfile{
+								Path: filepath.Join(generatedDir, "B", "run.Dockerfile"),
+								Args: []extend.Arg{{Name: "argB", Value: "valueB"}},
+							}
+							h.Mkdir(t, filepath.Join(generatedDir, "B"))
+							h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "B", "run.Dockerfile"))
+							buf = new(bytes.Buffer)
+							data = extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileB.Args}}
+							h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+							h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "B", "extend-config.toml"))
+
+							fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
+							someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
+							someFakeImage.ConfigFileReturnsOnCall(0, &v1.ConfigFile{Config: v1.Config{
+								User: "1234:5678",
+							}}, nil)
+
+							// first dockerfile
+
+							fakeDockerfileApplier.EXPECT().Apply(
+								gomock.Any(),
+								gomock.Any(), // we mutate the provided image so we can't expect the fake image
+								extend.Options{
+									BuildContext: "some-app-dir",
+									IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+									CacheTTL:     7 * (24 * time.Hour),
+								},
+								logger,
+							).DoAndReturn(
+								func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+									h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
+									h.AssertEq(t, len(dockerfile.Args), 4) // build_id, user_id, group_id
+									h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+									_, err := uuid.Parse(dockerfile.Args[0].Value)
+									h.AssertNil(t, err)
+									h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+									h.AssertEq(t, dockerfile.Args[1].Value, "1234")
+									h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+									h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+									h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
+
+									return someFakeImage, nil
+								})
+							firstConfig := &v1.ConfigFile{Config: v1.Config{
+								User:   "1234:5678",
+								Labels: map[string]string{phase.RebasableLabel: fmt.Sprintf("%t", tc.firstDockerfileRebasable)},
+							}}
+							someFakeImage.ConfigFileReturnsOnCall(1, firstConfig, nil)
+
+							// second dockerfile
+
+							fakeDockerfileApplier.EXPECT().Apply(
+								gomock.Any(),
+								someFakeImage,
+								extend.Options{
+									BuildContext: "some-app-dir",
+									IgnorePaths:  []string{"some-app-dir", "some-layers-dir", "some-platform-dir"},
+									CacheTTL:     7 * (24 * time.Hour),
+								},
+								logger,
+							).DoAndReturn(
+								func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+									h.AssertEq(t, dockerfile.Path, expectedDockerfileB.Path)
+									h.AssertEq(t, len(dockerfile.Args), 4) // build_id, user_id, group_id
+									h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+									_, err := uuid.Parse(dockerfile.Args[0].Value)
+									h.AssertNil(t, err)
+									h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+									h.AssertEq(t, dockerfile.Args[1].Value, "1234")
+									h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+									h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+									h.AssertEq(t, dockerfile.Args[3], expectedDockerfileB.Args[0])
+
+									return someFakeImage, nil
+								})
+							secondConfig := &v1.ConfigFile{Config: v1.Config{
+								User:   "1234:5678",
+								Labels: map[string]string{phase.RebasableLabel: fmt.Sprintf("%t", tc.secondDockerfileRebasable)},
+							}}
+							someFakeImage.ConfigFileReturnsOnCall(2, secondConfig, nil)
+
+							// set label
+
+							someFakeImage.ConfigFileReturnsOnCall(3, secondConfig, nil)
+							someFakeImage.ConfigFileReturnsOnCall(4, secondConfig, nil)
+
+							// save without base layers
+
+							imageHash := v1.Hash{Algorithm: "sha256", Hex: "some-image-hex"}
+							someFakeImage.DigestReturns(imageHash, nil)
+							someFakeImage.ConfigNameReturns(v1.Hash{Algorithm: "sha256", Hex: "some-config-hex"}, nil)
+
+							fakeDockerfileApplier.EXPECT().Cleanup().Return(nil)
+
+							h.AssertNil(t, extender.Extend("run", logger))
+							outputImagePath := filepath.Join(extendedDir, "run", tc.expectedImageSHA)
+							h.AssertPathExists(t, outputImagePath)
+							fis, err := os.ReadDir(outputImagePath)
+							h.AssertNil(t, err)
+							h.AssertEq(t, len(fis), 3)
+						})
 					})
+				}
 
-				err := extender.Extend("run", logger)
-				h.AssertError(t, err, "extending run image: the final user ID is 0 (root); please add another extension that resets the user to non-root")
+				it("errors if the last extension leaves the user as root", func() {
+					expectedDockerfileA := extend.Dockerfile{
+						Path: filepath.Join(generatedDir, "A", "run.Dockerfile"),
+						Args: []extend.Arg{{Name: "argA", Value: "valueA"}},
+					}
+					h.Mkdir(t, filepath.Join(generatedDir, "A"))
+					h.Mkfile(t, "some dockerfile content", filepath.Join(generatedDir, "A", "run.Dockerfile"))
+					buf := new(bytes.Buffer)
+					data := extend.Config{Run: extend.BuildConfig{Args: expectedDockerfileA.Args}}
+					h.AssertNil(t, toml.NewEncoder(buf).Encode(data))
+					h.Mkfile(t, buf.String(), filepath.Join(generatedDir, "A", "extend-config.toml"))
+
+					fakeDockerfileApplier.EXPECT().ImageFor(extender.ImageRef).Return(someFakeImage, nil)
+					firstConfig := &v1.ConfigFile{Config: v1.Config{
+						User: "0:5678",
+					}}
+					someFakeImage.ConfigFileReturns(firstConfig, nil)
+					someFakeImage.ManifestReturns(&v1.Manifest{Layers: []v1.Descriptor{}}, nil)
+
+					fakeDockerfileApplier.EXPECT().Apply(
+						gomock.Any(),
+						gomock.Any(),
+						gomock.Any(),
+						logger,
+					).DoAndReturn(
+						func(dockerfile extend.Dockerfile, _ v1.Image, _ extend.Options, _ llog.Logger) (v1.Image, error) {
+							h.AssertEq(t, dockerfile.Path, expectedDockerfileA.Path)
+							h.AssertEq(t, len(dockerfile.Args), 4)
+							h.AssertEq(t, dockerfile.Args[0].Name, "build_id")
+							_, err := uuid.Parse(dockerfile.Args[0].Value)
+							h.AssertNil(t, err)
+							h.AssertEq(t, dockerfile.Args[1].Name, "user_id")
+							h.AssertEq(t, dockerfile.Args[1].Value, "0")
+							h.AssertEq(t, dockerfile.Args[2].Name, "group_id")
+							h.AssertEq(t, dockerfile.Args[2].Value, "5678")
+							h.AssertEq(t, dockerfile.Args[3], expectedDockerfileA.Args[0])
+
+							return someFakeImage, nil
+						})
+
+					err := extender.Extend("run", logger)
+					h.AssertError(t, err, "extending run image: the final user ID is 0 (root); please add another extension that resets the user to non-root")
+				})
 			})
 		})
 	})
diff --git a/phase/generator_test.go b/phase/generator_test.go
index d77371e7..c014878e 100644
--- a/phase/generator_test.go
+++ b/phase/generator_test.go
@@ -91,7 +91,6 @@ func testGeneratorFactory(t *testing.T, when spec.G, it spec.S) {
 				GroupPath:      "some-group-path",
 				GeneratedDir:   "some-output-dir",
 				PlanPath:       "some-plan-path",
-				PlatformAPI:    api.Platform.Latest(),
 				PlatformDir:    "some-platform-dir",
 				RunPath:        "some-run-path",
 			}, stdout, stderr, logger,
@@ -159,7 +158,6 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) {
 			Logger:       &log.Logger{Handler: logHandler},
 			GeneratedDir: generatedDir,
 			Plan:         files.Plan{},
-			PlatformAPI:  api.Platform.Latest(),
 			PlatformDir:  platformDir,
 			Err:          stderr,
 			Out:          stdout,
@@ -176,465 +174,696 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) {
 		extB := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "ext/B", Version: "v1"}}}
 		extC := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "C", Version: "v1"}}}
 
-		it("provides a subset of the build plan to each extension", func() {
-			generator.Plan = files.Plan{
-				Entries: []files.BuildPlanEntry{
-					{
-						Providers: []buildpack.GroupElement{
-							{ID: "A", Version: "v1"}, // not provided to any extension because Extension is false
-						},
-						Requires: []buildpack.Require{
-							{Name: "buildpack-dep", Version: "v1"},
-						},
-					},
-					{
-						Providers: []buildpack.GroupElement{
-							{ID: "A", Version: "v1", Extension: true},
-							{ID: "ext/B", Version: "v2", Extension: true},
-						},
-						Requires: []buildpack.Require{
-							{Name: "some-dep", Version: "v1"}, // not provided to B because it is met
-						},
-					},
-					{
-						Providers: []buildpack.GroupElement{
-							{ID: "A", Version: "v1", Extension: true},
-							{ID: "ext/B", Version: "v2", Extension: true},
+		when("using the platform API 0.12", func() {
+			it.Before(func() {
+				generator.PlatformAPI = api.MustParse("0.12")
+			})
+
+			it("provides a subset of the build plan to each extension", func() {
+				generator.Plan = files.Plan{
+					Entries: []files.BuildPlanEntry{
+						{
+							Providers: []buildpack.GroupElement{
+								{ID: "A", Version: "v1"}, // not provided to any extension because Extension is false
+							},
+							Requires: []buildpack.Require{
+								{Name: "buildpack-dep", Version: "v1"},
+							},
 						},
-						Requires: []buildpack.Require{
-							{Name: "some-unmet-dep", Version: "v2"}, // provided to ext/B because it is unmet
+						{
+							Providers: []buildpack.GroupElement{
+								{ID: "A", Version: "v1", Extension: true},
+								{ID: "ext/B", Version: "v2", Extension: true},
+							},
+							Requires: []buildpack.Require{
+								{Name: "some-dep", Version: "v1"}, // not provided to B because it is met
+							},
 						},
-					},
-					{
-						Providers: []buildpack.GroupElement{
-							{ID: "ext/B", Version: "v2", Extension: true},
+						{
+							Providers: []buildpack.GroupElement{
+								{ID: "A", Version: "v1", Extension: true},
+								{ID: "ext/B", Version: "v2", Extension: true},
+							},
+							Requires: []buildpack.Require{
+								{Name: "some-unmet-dep", Version: "v2"}, // provided to ext/B because it is unmet
+							},
 						},
-						Requires: []buildpack.Require{
-							{Name: "other-dep", Version: "v4"}, // only provided to ext/B
+						{
+							Providers: []buildpack.GroupElement{
+								{ID: "ext/B", Version: "v2", Extension: true},
+							},
+							Requires: []buildpack.Require{
+								{Name: "other-dep", Version: "v4"}, // only provided to ext/B
+							},
 						},
 					},
-				},
-			}
-			expectedInputs := buildpack.GenerateInputs{
-				AppDir:      appDir,
-				PlatformDir: platformDir,
-				// OutputDir is ephemeral directory
-			}
-
-			dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
-			expectedAInputs := expectedInputs
-			expectedAInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{
-				{Name: "some-dep", Version: "v1"},
-				{Name: "some-unmet-dep", Version: "v2"},
-			}}
-			executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).DoAndReturn(
-				func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) {
-					h.AssertEq(t, inputs.Plan, expectedAInputs.Plan)
-					h.AssertEq(t, inputs.AppDir, expectedAInputs.AppDir)
-					h.AssertEq(t, inputs.PlatformDir, expectedAInputs.PlatformDir)
-					h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir)
-					return buildpack.GenerateOutputs{MetRequires: []string{"some-dep"}}, nil
-				})
-
-			dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
-			expectedBInputs := expectedInputs
-			expectedBInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{
-				{Name: "some-unmet-dep", Version: "v2"},
-				{Name: "other-dep", Version: "v4"},
-			}}
-			executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Do(
-				func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) {
-					h.AssertEq(t, inputs.Plan, expectedBInputs.Plan)
-					h.AssertEq(t, inputs.AppDir, expectedBInputs.AppDir)
-					h.AssertEq(t, inputs.PlatformDir, expectedBInputs.PlatformDir)
-					h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir)
-					return buildpack.GenerateOutputs{}, nil
-				})
-
-			_, err := generator.Generate()
-			h.AssertNil(t, err)
-		})
-
-		it("passes through CNB_TARGET environment variables", func() {
-			generator.AnalyzedMD = files.Analyzed{
-				RunImage: &files.RunImage{
-					TargetMetadata: &files.TargetMetadata{
-						OS:   "linux",
-						Arch: "amd64",
-					},
-				},
-			}
-			// mock generate for extensions  - these are tested elsewhere, so we just need to return anything...
-			dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extA, nil)
-			dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extB, nil)
-			// extension A has a build.Dockerfile and an extend-config.toml
-			h.Mkdir(t, filepath.Join(tmpDir, "A"))
-			buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile")
-			h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA)
-			extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml")
-			h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA)
-			executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
-				func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) {
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH=amd64")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH_VARIANT=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_OS=linux")
-					return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID,
-						Kind: "build", Path: buildDockerfilePathA}}}, nil
-				})
-			executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
-				func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) {
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH=amd64")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH_VARIANT=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_OS=linux")
-					return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID,
-						Kind: "build", Path: buildDockerfilePathA}}}, nil
-				})
-			_, err := generator.Generate()
-			h.AssertNil(t, err)
-		})
-
-		it("copies Dockerfiles and extend-config.toml files to the correct locations", func() {
-			// mock generate for extension A
-			dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
-			// extension A has a build.Dockerfile and an extend-config.toml
-			h.Mkdir(t, filepath.Join(tmpDir, "A"))
-			buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile")
-			h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA)
-			extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml")
-			h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA)
-			executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
-				Dockerfiles: []buildpack.DockerfileInfo{
-					{
-						ExtensionID: "A",
-						Kind:        "build",
-						Path:        buildDockerfilePathA,
-					},
-				},
-			}, nil)
-
-			// mock generate for extension B
-			dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
-			// extension B has a run.Dockerfile
-			h.Mkdir(t, filepath.Join(tmpDir, "B"))
-			runDockerfilePathB := filepath.Join(tmpDir, "B", "build.Dockerfile")
-			h.Mkfile(t, "some-run.Dockerfile-content-B", runDockerfilePathB)
-			executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
-				Dockerfiles: []buildpack.DockerfileInfo{
-					{
-						ExtensionID: "B",
-						Kind:        "run",
-						Path:        runDockerfilePathB,
-					},
-				},
-			}, nil)
-
-			// mock generate for extension C
-			dirStore.EXPECT().LookupExt("C", "v1").Return(&extC, nil)
-			// extension C has a build.Dockerfile, run.Dockerfile, and extend-config.toml
-			h.Mkdir(t, filepath.Join(tmpDir, "C"))
-			buildDockerfilePathC := filepath.Join(tmpDir, "C", "build.Dockerfile")
-			h.Mkfile(t, "some-build.Dockerfile-content-C", buildDockerfilePathC)
-			runDockerfilePathC := filepath.Join(tmpDir, "C", "run.Dockerfile")
-			h.Mkfile(t, "some-run.Dockerfile-content-C", runDockerfilePathC)
-			extendConfigPathC := filepath.Join(tmpDir, "C", "extend-config.toml")
-			h.Mkfile(t, "some-extend-config.toml-content-C", extendConfigPathC)
-			executor.EXPECT().Generate(extC, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
-				Dockerfiles: []buildpack.DockerfileInfo{
-					{
-						ExtensionID: "C",
-						Kind:        "build",
-						Path:        buildDockerfilePathC,
-					},
-					{
-						ExtensionID: "C",
-						Kind:        "run",
-						Path:        runDockerfilePathC,
-					},
-				},
-			}, nil)
-
-			// add extension C to the group
-			generator.Extensions = append(generator.Extensions, buildpack.GroupElement{ID: "C", Version: "v1", API: api.Buildpack.Latest().String()})
-			// do generate
-			_, err := generator.Generate()
-			h.AssertNil(t, err)
+				}
+				expectedInputs := buildpack.GenerateInputs{
+					AppDir:      appDir,
+					PlatformDir: platformDir,
+					// OutputDir is ephemeral directory
+				}
 
-			t.Log("copies Dockerfiles")
-			contents := h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "Dockerfile"))
-			h.AssertEq(t, string(contents), "some-build.Dockerfile-content-A")
-			contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile"))
-			h.AssertEq(t, string(contents), "some-run.Dockerfile-content-B")
-			contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "Dockerfile"))
-			h.AssertEq(t, string(contents), "some-build.Dockerfile-content-C")
-			contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "Dockerfile"))
-			h.AssertEq(t, string(contents), "some-run.Dockerfile-content-C")
-
-			t.Log("copies extend-config.toml files if they exist")
-			contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "extend-config.toml"))
-			h.AssertEq(t, string(contents), "some-extend-config.toml-content-A")
-			contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "extend-config.toml"))
-			h.AssertEq(t, string(contents), "some-extend-config.toml-content-C")
-			contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "extend-config.toml"))
-			h.AssertEq(t, string(contents), "some-extend-config.toml-content-C")
-
-			t.Log("does not pollute the output directory")
-			h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "A", "run.Dockerfile"))
-			h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "B", "build.Dockerfile"))
-			h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "run.Dockerfile"))
-			h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "build.Dockerfile"))
-		})
+				dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
+				expectedAInputs := expectedInputs
+				expectedAInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{
+					{Name: "some-dep", Version: "v1"},
+					{Name: "some-unmet-dep", Version: "v2"},
+				}}
+				executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).DoAndReturn(
+					func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) {
+						h.AssertEq(t, inputs.Plan, expectedAInputs.Plan)
+						h.AssertEq(t, inputs.AppDir, expectedAInputs.AppDir)
+						h.AssertEq(t, inputs.PlatformDir, expectedAInputs.PlatformDir)
+						h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir)
+						return buildpack.GenerateOutputs{MetRequires: []string{"some-dep"}}, nil
+					})
 
-		when("returning run image metadata", func() {
-			var (
-				runDockerfilePathA = filepath.Join(tmpDir, "run.Dockerfile.A")
-				runDockerfilePathB = filepath.Join(tmpDir, "run.Dockerfile.B")
-			)
+				dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
+				expectedBInputs := expectedInputs
+				expectedBInputs.Plan = buildpack.Plan{Entries: []buildpack.Require{
+					{Name: "some-unmet-dep", Version: "v2"},
+					{Name: "other-dep", Version: "v4"},
+				}}
+				executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Do(
+					func(_ buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ llog.Logger) (buildpack.GenerateOutputs, error) {
+						h.AssertEq(t, inputs.Plan, expectedBInputs.Plan)
+						h.AssertEq(t, inputs.AppDir, expectedBInputs.AppDir)
+						h.AssertEq(t, inputs.PlatformDir, expectedBInputs.PlatformDir)
+						h.AssertEq(t, inputs.BuildConfigDir, generator.BuildConfigDir)
+						return buildpack.GenerateOutputs{}, nil
+					})
 
-			it.Before(func() {
-				h.Mkfile(t, "some-dockerfile-content-A", runDockerfilePathA)
-				h.Mkfile(t, "some-dockerfile-content-B", runDockerfilePathB)
+				_, err := generator.Generate()
+				h.AssertNil(t, err)
+			})
 
+			it("passes through CNB_TARGET environment variables", func() {
 				generator.AnalyzedMD = files.Analyzed{
 					RunImage: &files.RunImage{
-						Reference: "some-existing-run-image@sha256:s0m3d1g3st",
-						Image:     "some-existing-run-image",
+						TargetMetadata: &files.TargetMetadata{
+							OS:   "linux",
+							Arch: "amd64",
+						},
 					},
 				}
+				// mock generate for extensions  - these are tested elsewhere, so we just need to return anything...
+				dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extA, nil)
+				dirStore.EXPECT().LookupExt(gomock.Any(), gomock.Any()).Return(&extB, nil)
+				// extension A has a build.Dockerfile and an extend-config.toml
+				h.Mkdir(t, filepath.Join(tmpDir, "A"))
+				buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile")
+				h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA)
+				extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml")
+				h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA)
+				executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
+					func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) {
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH=amd64")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH_VARIANT=")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_OS=linux")
+						return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID,
+							Kind: "build", Path: buildDockerfilePathA}}}, nil
+					})
+				executor.EXPECT().Generate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
+					func(d buildpack.ExtDescriptor, inputs buildpack.GenerateInputs, _ *log.Logger) (buildpack.GenerateOutputs, error) {
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH=amd64")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_ARCH_VARIANT=")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
+						h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_OS=linux")
+						return buildpack.GenerateOutputs{Dockerfiles: []buildpack.DockerfileInfo{{ExtensionID: d.Extension.ID,
+							Kind: "build", Path: buildDockerfilePathA}}}, nil
+					})
+				_, err := generator.Generate()
+				h.AssertNil(t, err)
 			})
 
-			type testCase struct {
-				before                    func()
-				descCondition             string
-				descResult                string
-				aDockerfiles              []buildpack.DockerfileInfo
-				bDockerfiles              []buildpack.DockerfileInfo
-				expectedRunImageImage     string
-				expectedRunImageReference string
-				expectedRunImageExtend    bool
-				expectedErr               string
-				assertAfter               func()
-			}
-			for _, tc := range []testCase{
-				{
-					descCondition: "all run.Dockerfiles declare `FROM ${base_image}`",
-					descResult:    "returns the original run image in the result",
-					aDockerfiles: []buildpack.DockerfileInfo{{
-						ExtensionID: "A",
-						Kind:        "run",
-						Path:        runDockerfilePathA,
-						WithBase:    "",
-						Extend:      true,
-					}},
-					bDockerfiles: []buildpack.DockerfileInfo{{
-						ExtensionID: "B",
-						Kind:        "run",
-						Path:        runDockerfilePathB,
-						WithBase:    "",
-						Extend:      true,
-					}},
-					expectedRunImageImage:     "some-existing-run-image",
-					expectedRunImageReference: "some-existing-run-image@sha256:s0m3d1g3st",
-					expectedRunImageExtend:    true,
-				},
-				{
-					descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow",
-					descResult:    "returns the last image referenced in the `FROM` statement of the last run.Dockerfile not to declare `FROM ${base_image}`, and sets extend to true",
-					aDockerfiles: []buildpack.DockerfileInfo{
+			it("copies Dockerfiles and extend-config.toml files to the correct locations", func() {
+				// mock generate for extension A
+				dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
+				// extension A has a build.Dockerfile and an extend-config.toml
+				h.Mkdir(t, filepath.Join(tmpDir, "A"))
+				buildDockerfilePathA := filepath.Join(tmpDir, "A", "build.Dockerfile")
+				h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA)
+				extendConfigPathA := filepath.Join(tmpDir, "A", "extend-config.toml")
+				h.Mkfile(t, "some-extend-config.toml-content-A", extendConfigPathA)
+				executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+					Dockerfiles: []buildpack.DockerfileInfo{
 						{
 							ExtensionID: "A",
-							Kind:        "run",
-							Path:        runDockerfilePathA,
-							WithBase:    "some-new-run-image",
-							Extend:      false,
+							Kind:        "build",
+							Path:        buildDockerfilePathA,
 						},
 					},
-					bDockerfiles: []buildpack.DockerfileInfo{
+				}, nil)
+
+				// mock generate for extension B
+				dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
+				// extension B has a run.Dockerfile
+				h.Mkdir(t, filepath.Join(tmpDir, "B"))
+				runDockerfilePathB := filepath.Join(tmpDir, "B", "run.Dockerfile")
+				h.Mkfile(t, "some-run.Dockerfile-content-B", runDockerfilePathB)
+				executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+					Dockerfiles: []buildpack.DockerfileInfo{
 						{
 							ExtensionID: "B",
 							Kind:        "run",
 							Path:        runDockerfilePathB,
-							WithBase:    "",
-							Extend:      true,
 						},
 					},
-					expectedRunImageImage:     "some-new-run-image",
-					expectedRunImageReference: "some-new-run-image",
-					expectedRunImageExtend:    true,
-				},
-				{
-					descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow",
-					descResult:    "returns the referenced base image, and sets extend to false",
-					aDockerfiles: []buildpack.DockerfileInfo{
+				}, nil)
+
+				// mock generate for extension C
+				dirStore.EXPECT().LookupExt("C", "v1").Return(&extC, nil)
+				// extension C has a build.Dockerfile, run.Dockerfile, and extend-config.toml
+				h.Mkdir(t, filepath.Join(tmpDir, "C"))
+				buildDockerfilePathC := filepath.Join(tmpDir, "C", "build.Dockerfile")
+				h.Mkfile(t, "some-build.Dockerfile-content-C", buildDockerfilePathC)
+				runDockerfilePathC := filepath.Join(tmpDir, "C", "run.Dockerfile")
+				h.Mkfile(t, "some-run.Dockerfile-content-C", runDockerfilePathC)
+				extendConfigPathC := filepath.Join(tmpDir, "C", "extend-config.toml")
+				h.Mkfile(t, "some-extend-config.toml-content-C", extendConfigPathC)
+				executor.EXPECT().Generate(extC, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+					Dockerfiles: []buildpack.DockerfileInfo{
 						{
-							ExtensionID: "A",
-							Kind:        "run",
-							Path:        runDockerfilePathA,
-							WithBase:    "some-new-run-image",
-							Extend:      true,
+							ExtensionID: "C",
+							Kind:        "build",
+							Path:        buildDockerfilePathC,
 						},
-					},
-					bDockerfiles: []buildpack.DockerfileInfo{
 						{
-							ExtensionID: "B",
+							ExtensionID: "C",
 							Kind:        "run",
-							Path:        runDockerfilePathB,
-							WithBase:    "some-other-base-image",
-							Extend:      false,
+							Path:        runDockerfilePathC,
 						},
 					},
-					expectedRunImageImage:     "some-other-base-image",
-					expectedRunImageReference: "some-other-base-image",
-					expectedRunImageExtend:    false,
-					assertAfter: func() {
-						t.Log("copies Dockerfiles to the correct locations")
-						t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory")
-						aContents := h.MustReadFile(t, filepath.Join(generatedDir, "run", "A", "Dockerfile.ignore"))
-						h.AssertEq(t, string(aContents), `some-dockerfile-content-A`)
-						BContents := h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile"))
-						h.AssertEq(t, string(BContents), `some-dockerfile-content-B`)
-					},
-				},
-				{
-					descCondition: "a run.Dockerfile declares a new base image and also extends",
-					descResult:    "returns the referenced base image, and sets extend to true",
-					aDockerfiles: []buildpack.DockerfileInfo{
-						{
+				}, nil)
+
+				// add extension C to the group
+				generator.Extensions = append(generator.Extensions, buildpack.GroupElement{ID: "C", Version: "v1", API: api.Buildpack.Latest().String()})
+				// do generate
+				_, err := generator.Generate()
+				h.AssertNil(t, err)
+
+				t.Log("copies Dockerfiles")
+				contents := h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "Dockerfile"))
+				h.AssertEq(t, string(contents), "some-build.Dockerfile-content-A")
+				contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile"))
+				h.AssertEq(t, string(contents), "some-run.Dockerfile-content-B")
+				contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "Dockerfile"))
+				h.AssertEq(t, string(contents), "some-build.Dockerfile-content-C")
+				contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "Dockerfile"))
+				h.AssertEq(t, string(contents), "some-run.Dockerfile-content-C")
+
+				t.Log("copies extend-config.toml files if they exist")
+				contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "A", "extend-config.toml"))
+				h.AssertEq(t, string(contents), "some-extend-config.toml-content-A")
+				contents = h.MustReadFile(t, filepath.Join(generatedDir, "build", "C", "extend-config.toml"))
+				h.AssertEq(t, string(contents), "some-extend-config.toml-content-C")
+				contents = h.MustReadFile(t, filepath.Join(generatedDir, "run", "C", "extend-config.toml"))
+				h.AssertEq(t, string(contents), "some-extend-config.toml-content-C")
+
+				t.Log("does not pollute the output directory")
+				h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "A", "run.Dockerfile"))
+				h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "B", "build.Dockerfile"))
+				h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "run.Dockerfile"))
+				h.AssertPathDoesNotExist(t, filepath.Join(generatedDir, "C", "build.Dockerfile"))
+			})
+
+			when("returning run image metadata", func() {
+				var (
+					runDockerfilePathA = filepath.Join(tmpDir, "run.Dockerfile.A")
+					runDockerfilePathB = filepath.Join(tmpDir, "run.Dockerfile.B")
+				)
+
+				it.Before(func() {
+					h.Mkfile(t, "some-dockerfile-content-A", runDockerfilePathA)
+					h.Mkfile(t, "some-dockerfile-content-B", runDockerfilePathB)
+
+					generator.AnalyzedMD = files.Analyzed{
+						RunImage: &files.RunImage{
+							Reference: "some-existing-run-image@sha256:s0m3d1g3st",
+							Image:     "some-existing-run-image",
+						},
+					}
+				})
+
+				type testCase struct {
+					before                    func()
+					descCondition             string
+					descResult                string
+					aDockerfiles              []buildpack.DockerfileInfo
+					bDockerfiles              []buildpack.DockerfileInfo
+					expectedRunImageImage     string
+					expectedRunImageReference string
+					expectedRunImageExtend    bool
+					expectedErr               string
+					assertAfter               func()
+				}
+				for _, tc := range []testCase{
+					{
+						descCondition: "all run.Dockerfiles declare `FROM ${base_image}`",
+						descResult:    "returns the original run image in the result",
+						aDockerfiles: []buildpack.DockerfileInfo{{
 							ExtensionID: "A",
 							Kind:        "run",
 							Path:        runDockerfilePathA,
-							WithBase:    "some-new-run-image",
+							WithBase:    "",
 							Extend:      true,
-						},
+						}},
+						bDockerfiles: []buildpack.DockerfileInfo{{
+							ExtensionID: "B",
+							Kind:        "run",
+							Path:        runDockerfilePathB,
+							WithBase:    "",
+							Extend:      true,
+						}},
+						expectedRunImageImage:     "some-existing-run-image",
+						expectedRunImageReference: "some-existing-run-image@sha256:s0m3d1g3st",
+						expectedRunImageExtend:    true,
 					},
-					bDockerfiles:              []buildpack.DockerfileInfo{},
-					expectedRunImageImage:     "some-new-run-image",
-					expectedRunImageReference: "some-new-run-image",
-					expectedRunImageExtend:    true,
-				},
-				{
-					before: func() {
-						generator.RunMetadata = files.Run{
-							Images: []files.RunImageForExport{
-								{Image: "some-run-image"},
+					{
+						descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow",
+						descResult:    "returns the last image referenced in the `FROM` statement of the last run.Dockerfile not to declare `FROM ${base_image}`, and sets extend to true",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								Path:        runDockerfilePathA,
+								WithBase:    "some-new-run-image",
+								Extend:      false,
 							},
-						}
+						},
+						bDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "B",
+								Kind:        "run",
+								Path:        runDockerfilePathB,
+								WithBase:    "",
+								Extend:      true,
+							},
+						},
+						expectedRunImageImage:     "some-new-run-image",
+						expectedRunImageReference: "some-new-run-image",
+						expectedRunImageExtend:    true,
 					},
-					descCondition: "run metadata is provided and contains new run image",
-					descResult:    "succeeds",
-					aDockerfiles: []buildpack.DockerfileInfo{
-						{
-							ExtensionID: "A",
-							Kind:        "run",
-							Path:        runDockerfilePathA,
-							WithBase:    "some-new-run-image",
-							Extend:      false,
+					{
+						descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow",
+						descResult:    "returns the referenced base image, and sets extend to false",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								Path:        runDockerfilePathA,
+								WithBase:    "some-new-run-image",
+								Extend:      true,
+							},
+						},
+						bDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "B",
+								Kind:        "run",
+								Path:        runDockerfilePathB,
+								WithBase:    "some-other-base-image",
+								Extend:      false,
+							},
+						},
+						expectedRunImageImage:     "some-other-base-image",
+						expectedRunImageReference: "some-other-base-image",
+						expectedRunImageExtend:    false,
+						assertAfter: func() {
+							t.Log("copies Dockerfiles to the correct locations")
+							t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory")
+							aContents := h.MustReadFile(t, filepath.Join(generatedDir, "run", "A", "Dockerfile.ignore"))
+							h.AssertEq(t, string(aContents), `some-dockerfile-content-A`)
+							BContents := h.MustReadFile(t, filepath.Join(generatedDir, "run", "B", "Dockerfile"))
+							h.AssertEq(t, string(BContents), `some-dockerfile-content-B`)
 						},
 					},
-					bDockerfiles:              []buildpack.DockerfileInfo{},
-					expectedRunImageImage:     "some-new-run-image",
-					expectedRunImageReference: "some-new-run-image",
-					expectedRunImageExtend:    false,
-				},
-				{
-					before: func() {
-						generator.RunMetadata = files.Run{
-							Images: []files.RunImageForExport{
-								{Image: "some-run-image"},
+					{
+						descCondition: "a run.Dockerfile declares a new base image and also extends",
+						descResult:    "returns the referenced base image, and sets extend to true",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								Path:        runDockerfilePathA,
+								WithBase:    "some-new-run-image",
+								Extend:      true,
 							},
-						}
+						},
+						bDockerfiles:              []buildpack.DockerfileInfo{},
+						expectedRunImageImage:     "some-new-run-image",
+						expectedRunImageReference: "some-new-run-image",
+						expectedRunImageExtend:    true,
 					},
-					descCondition: "run metadata is provided and does not contain new run image",
-					descResult:    "succeeds with warning",
-					aDockerfiles: []buildpack.DockerfileInfo{
-						{
-							ExtensionID: "A",
-							Kind:        "run",
-							Path:        runDockerfilePathA,
-							WithBase:    "some-other-run-image",
-							Extend:      false,
+					{
+						before: func() {
+							generator.RunMetadata = files.Run{
+								Images: []files.RunImageForExport{
+									{Image: "some-run-image"},
+								},
+							}
+						},
+						descCondition: "run metadata is provided and contains new run image",
+						descResult:    "succeeds",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								Path:        runDockerfilePathA,
+								WithBase:    "some-new-run-image",
+								Extend:      false,
+							},
 						},
+						bDockerfiles:              []buildpack.DockerfileInfo{},
+						expectedRunImageImage:     "some-new-run-image",
+						expectedRunImageReference: "some-new-run-image",
+						expectedRunImageExtend:    false,
 					},
-					bDockerfiles:              []buildpack.DockerfileInfo{},
-					expectedRunImageImage:     "some-other-run-image",
-					expectedRunImageReference: "some-other-run-image",
-					assertAfter: func() {
-						h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata")
+					{
+						before: func() {
+							generator.RunMetadata = files.Run{
+								Images: []files.RunImageForExport{
+									{Image: "some-run-image"},
+								},
+							}
+						},
+						descCondition: "run metadata is provided and does not contain new run image",
+						descResult:    "succeeds with warning",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								Path:        runDockerfilePathA,
+								WithBase:    "some-other-run-image",
+								Extend:      false,
+							},
+						},
+						bDockerfiles:              []buildpack.DockerfileInfo{},
+						expectedRunImageImage:     "some-other-run-image",
+						expectedRunImageReference: "some-other-run-image",
+						assertAfter: func() {
+							h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata")
+						},
 					},
-				},
-			} {
-				tc := tc
-				when := when
-				when(tc.descCondition, func() {
-					if tc.before != nil {
-						it.Before(tc.before)
-					}
+				} {
+					tc := tc
+					when := when
+					when(tc.descCondition, func() {
+						if tc.before != nil {
+							it.Before(tc.before)
+						}
 
-					it(tc.descResult, func() {
-						// mock generate for extension A
+						it(tc.descResult, func() {
+							// mock generate for extension A
+							dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
+							executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+								Dockerfiles: tc.aDockerfiles,
+							}, nil)
+
+							// mock generate for extension B
+							dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
+							executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+								Dockerfiles: tc.bDockerfiles,
+							}, nil)
+
+							// do generate
+							result, err := generator.Generate()
+							if err == nil {
+								h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage)
+								h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference)
+								h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend)
+							} else {
+								t.Log(err)
+								h.AssertError(t, err, tc.expectedErr)
+							}
+
+							if tc.assertAfter != nil {
+								tc.assertAfter()
+							}
+						})
+					})
+				}
+			})
+
+			when("extension generate failed", func() {
+				when("first extension fails", func() {
+					it("errors", func() {
+						extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}}
 						dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
-						executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
-							Dockerfiles: tc.aDockerfiles,
-						}, nil)
+						executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error"))
 
-						// mock generate for extension B
-						dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
-						executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
-							Dockerfiles: tc.bDockerfiles,
-						}, nil)
-
-						// do generate
-						result, err := generator.Generate()
-						if err == nil {
-							h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage)
-							h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference)
-							h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend)
-						} else {
-							t.Log(err)
-							h.AssertError(t, err, tc.expectedErr)
+						if _, err := generator.Generate(); err == nil {
+							t.Fatal("Expected error.\n")
+						} else if !strings.Contains(err.Error(), "some error") {
+							t.Fatalf("Incorrect error: %s\n", err)
 						}
+					})
+				})
+
+				when("later extension fails", func() {
+					it("errors", func() {
+						extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}}
+						dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
+						executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, nil)
+						extB := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "ext/B", Version: "v1"}}}
+						dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
+						executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error"))
 
-						if tc.assertAfter != nil {
-							tc.assertAfter()
+						if _, err := generator.Generate(); err == nil {
+							t.Fatal("Expected error.\n")
+						} else if !strings.Contains(err.Error(), "some error") {
+							t.Fatalf("Incorrect error: %s\n", err)
 						}
 					})
 				})
-			}
+			})
 		})
 
-		when("extension generate failed", func() {
-			when("first extension fails", func() {
-				it("errors", func() {
-					extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}}
-					dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
-					executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error"))
-
-					if _, err := generator.Generate(); err == nil {
-						t.Fatal("Expected error.\n")
-					} else if !strings.Contains(err.Error(), "some error") {
-						t.Fatalf("Incorrect error: %s\n", err)
-					}
-				})
+		when("using the platform API 0.13", func() {
+			it.Before(func() {
+				generator.PlatformAPI = api.MustParse("0.13")
 			})
 
-			when("later extension fails", func() {
-				it("errors", func() {
-					extA := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "A", Version: "v1"}}}
-					dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
-					executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, nil)
-					extB := buildpack.ExtDescriptor{Extension: buildpack.ExtInfo{BaseInfo: buildpack.BaseInfo{ID: "ext/B", Version: "v1"}}}
-					dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
-					executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{}, errors.New("some error"))
-
-					if _, err := generator.Generate(); err == nil {
-						t.Fatal("Expected error.\n")
-					} else if !strings.Contains(err.Error(), "some error") {
-						t.Fatalf("Incorrect error: %s\n", err)
+			when("returning run image metadata", func() {
+				var (
+					runDockerfilePathA string
+					runDockerfilePathB string
+				)
+
+				it.Before(func() {
+					runDockerfilePathA = filepath.Join(generatedDir, "A", "run.Dockerfile.A")
+					runDockerfilePathB = filepath.Join(generatedDir, "B", "run.Dockerfile.B")
+
+					h.Mkdir(t, filepath.Join(generatedDir, "A"), filepath.Join(generatedDir, "B"))
+					h.Mkfile(t, "some-dockerfile-content-A", runDockerfilePathA)
+					h.Mkfile(t, "some-dockerfile-content-B", runDockerfilePathB)
+
+					generator.AnalyzedMD = files.Analyzed{
+						RunImage: &files.RunImage{
+							Reference: "some-existing-run-image@sha256:s0m3d1g3st",
+							Image:     "some-existing-run-image",
+						},
 					}
 				})
+
+				type testCase struct {
+					before                    func()
+					descCondition             string
+					descResult                string
+					aDockerfiles              []buildpack.DockerfileInfo
+					bDockerfiles              []buildpack.DockerfileInfo
+					expectedRunImageImage     string
+					expectedRunImageReference string
+					expectedRunImageExtend    bool
+					expectedErr               string
+					assertAfter               func()
+				}
+				for _, tc := range []testCase{
+					{
+						descCondition: "all run.Dockerfiles declare `FROM ${base_image}`",
+						descResult:    "returns the original run image in the result",
+						aDockerfiles: []buildpack.DockerfileInfo{{
+							ExtensionID: "A",
+							Kind:        "run",
+							WithBase:    "",
+							Extend:      true,
+						}},
+						bDockerfiles: []buildpack.DockerfileInfo{{
+							ExtensionID: "B",
+							Kind:        "run",
+							WithBase:    "",
+							Extend:      true,
+						}},
+						expectedRunImageImage:     "some-existing-run-image",
+						expectedRunImageReference: "some-existing-run-image@sha256:s0m3d1g3st",
+						expectedRunImageExtend:    true,
+					},
+					{
+						descCondition: "a run.Dockerfile declares a new base image and run.Dockerfiles follow",
+						descResult:    "returns the last image referenced in the `FROM` statement of the last run.Dockerfile not to declare `FROM ${base_image}`, and sets extend to true",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								WithBase:    "some-new-run-image",
+								Extend:      false,
+							},
+						},
+						bDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "B",
+								Kind:        "run",
+								WithBase:    "",
+								Extend:      true,
+							},
+						},
+						expectedRunImageImage:     "some-new-run-image",
+						expectedRunImageReference: "some-new-run-image",
+						expectedRunImageExtend:    true,
+					},
+					{
+						descCondition: "a run.Dockerfile declares a new base image (only) and no run.Dockerfiles follow",
+						descResult:    "returns the referenced base image, and sets extend to false",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								WithBase:    "some-new-run-image",
+								Extend:      true,
+							},
+						},
+						bDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "B",
+								Kind:        "run",
+								WithBase:    "some-other-base-image",
+								Extend:      false,
+							},
+						},
+						expectedRunImageImage:     "some-other-base-image",
+						expectedRunImageReference: "some-other-base-image",
+						expectedRunImageExtend:    false,
+						assertAfter: func() {
+							t.Log("renames earlier run.Dockerfiles to Dockerfile.ignore in the output directory")
+							aContents := h.MustReadFile(t, filepath.Join(generatedDir, "A", "run.Dockerfile.A.ignore"))
+							h.AssertEq(t, string(aContents), `some-dockerfile-content-A`)
+							BContents := h.MustReadFile(t, filepath.Join(generatedDir, "B", "run.Dockerfile.B"))
+							h.AssertEq(t, string(BContents), `some-dockerfile-content-B`)
+						},
+					},
+					{
+						descCondition: "a run.Dockerfile declares a new base image and also extends",
+						descResult:    "returns the referenced base image, and sets extend to true",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								WithBase:    "some-new-run-image",
+								Extend:      true,
+							},
+						},
+						bDockerfiles:              []buildpack.DockerfileInfo{},
+						expectedRunImageImage:     "some-new-run-image",
+						expectedRunImageReference: "some-new-run-image",
+						expectedRunImageExtend:    true,
+					},
+					{
+						before: func() {
+							generator.RunMetadata = files.Run{
+								Images: []files.RunImageForExport{
+									{Image: "some-run-image"},
+								},
+							}
+						},
+						descCondition: "run metadata is provided and contains new run image",
+						descResult:    "succeeds",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								WithBase:    "some-new-run-image",
+								Extend:      false,
+							},
+						},
+						bDockerfiles:              []buildpack.DockerfileInfo{},
+						expectedRunImageImage:     "some-new-run-image",
+						expectedRunImageReference: "some-new-run-image",
+						expectedRunImageExtend:    false,
+					},
+					{
+						before: func() {
+							generator.RunMetadata = files.Run{
+								Images: []files.RunImageForExport{
+									{Image: "some-run-image"},
+								},
+							}
+						},
+						descCondition: "run metadata is provided and does not contain new run image",
+						descResult:    "succeeds with warning",
+						aDockerfiles: []buildpack.DockerfileInfo{
+							{
+								ExtensionID: "A",
+								Kind:        "run",
+								WithBase:    "some-other-run-image",
+								Extend:      false,
+							},
+						},
+						bDockerfiles:              []buildpack.DockerfileInfo{},
+						expectedRunImageImage:     "some-other-run-image",
+						expectedRunImageReference: "some-other-run-image",
+						assertAfter: func() {
+							h.AssertLogEntry(t, logHandler, "new runtime base image 'some-other-run-image' not found in run metadata")
+						},
+					},
+				} {
+					tc := tc
+					when := when
+					when(tc.descCondition, func() {
+						if tc.before != nil {
+							it.Before(tc.before)
+						}
+
+						it(tc.descResult, func() {
+							for i := range tc.aDockerfiles {
+								tc.aDockerfiles[i].Path = runDockerfilePathA
+							}
+
+							for i := range tc.bDockerfiles {
+								tc.bDockerfiles[i].Path = runDockerfilePathB
+							}
+
+							// mock generate for extension A
+							dirStore.EXPECT().LookupExt("A", "v1").Return(&extA, nil)
+							executor.EXPECT().Generate(extA, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+								Dockerfiles: tc.aDockerfiles,
+							}, nil)
+
+							// mock generate for extension B
+							dirStore.EXPECT().LookupExt("ext/B", "v2").Return(&extB, nil)
+							executor.EXPECT().Generate(extB, gomock.Any(), gomock.Any()).Return(buildpack.GenerateOutputs{
+								Dockerfiles: tc.bDockerfiles,
+							}, nil)
+
+							// do generate
+							result, err := generator.Generate()
+							if err == nil {
+								h.AssertEq(t, result.AnalyzedMD.RunImage.Image, tc.expectedRunImageImage)
+								h.AssertEq(t, result.AnalyzedMD.RunImage.Reference, tc.expectedRunImageReference)
+								h.AssertEq(t, result.AnalyzedMD.RunImage.Extend, tc.expectedRunImageExtend)
+							} else {
+								t.Log(err)
+								h.AssertError(t, err, tc.expectedErr)
+							}
+
+							if tc.assertAfter != nil {
+								tc.assertAfter()
+							}
+						})
+					})
+				}
 			})
 		})
 	})