Repository: buildpacks/lifecycle (go)

Issue:
`CNB_TARGET_` environment variables are not set when expected
### Summary

In [Buildpack API 0.10](https://github.com/buildpacks/spec/blob/buildpack/0.10/buildpack.md?plain=1#L750-L760), it says:

> The following environment variables MUST be set by the lifecycle during the detect and build phases to describe the target runtime image, if inputs are provided.

But `lifecycle` fails to provide these `CNB_TARGET_*` environment variables if the Platform API is less than `0.12`:

https://github.com/buildpacks/lifecycle/blob/be881f29d5b4a33795e1b61bb262ce9ae439cc10/phase/analyzer.go#L84-L93

This means that a buildpack using Buildpack API 0.10 running on a platform using Platform API 0.9 won't have `CNB_TARGET_OS` and `CNB_TARGET_ARCH` set, even though these should be set according to the Buildpack API.

---

### Reproduction

##### Steps

1. Use an older version of `pack`, like `v0.27.0`, that uses a Platform API version < 0.12
2. Run `pack build myapp --builder heroku/builder@sha256:5856c2e1f6c55ec5e289453b872f57f4ebe1a32a6da31f053cd27eddccbf21c5`
3. Observe that the build fails (some of the included buildpacks try to read `CNB_TARGET_OS`, which is not present)
4. Use a newer version of `pack` that uses Platform API version >= 0.12
5. Run `pack build myapp --builder heroku/builder@sha256:5856c2e1f6c55ec5e289453b872f57f4ebe1a32a6da31f053cd27eddccbf21c5` again.
6. Observe that the build doesn't error on missing environment variables.

##### Current behavior

Buildpacks using Buildpack API 0.10 may not receive `CNB_TARGET_OS` / `CNB_TARGET_PLATFORM` env vars when an older Platform API is in use.


##### Expected behavior

Buildpacks using Buildpack API 0.10 always receive `CNB_TARGET_OS` / `CNB_TARGET_PLATFORM` (regardless of Platform API version).


---

### Context

##### lifecycle version

`0.18.5` in my testing, but I think older versions are affected as well.



##### platform version(s)

Anything < 0.12.



##### anything else?
<!-- Add any other context that may help (e.g., Tekton task version, kpack version, etc.). -->



Pull request description:
Always set CNB_TARGET_* variables during detect, build, and generate
when the Buildpack API version is at least 0.10

<!-- 🎉🎉🎉 Thank you for the PR!!! 🎉🎉🎉 -->

### Summary
<!-- Please describe your changes at a high level. -->

Previously, we only set these variables when the Platform API version was at least 0.12. But, newer Buildpack APIs expect these variables regardless of the Platform API version. If we are on an older platform, derive the target variables from the base image OS.

#### 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 -->

The lifecycle always set CNB_TARGET_* variables during detect, build, and generate when the Buildpack API version is at least 0.10

---

### Related
<!-- If this PR addresses an issue, please provide the issue number below. -->

Resolves #1308 and #1186

---

### Context
<!-- Add any other context that may help reviewers (e.g., code that requires special attention, etc.). -->



Patch:
diff --git a/buildpack/build.go b/buildpack/build.go
index 0e757e33..354e724c 100644
--- a/buildpack/build.go
+++ b/buildpack/build.go
@@ -35,6 +35,7 @@ type BuildInputs struct {
 	LayersDir      string
 	PlatformDir    string
 	Env            BuildEnv
+	TargetEnv      []string
 	Out, Err       io.Writer
 	Plan           Plan
 }
@@ -151,6 +152,9 @@ func runBuildCmd(d BpDescriptor, bpLayersDir, planPath string, inputs BuildInput
 			EnvLayersDir+"="+bpLayersDir,
 		)
 	}
+	if api.MustParse(d.API()).AtLeast("0.10") {
+		cmd.Env = append(cmd.Env, inputs.TargetEnv...)
+	}
 
 	if err = cmd.Run(); err != nil {
 		return NewError(err, ErrTypeBuildpack)
diff --git a/buildpack/detect.go b/buildpack/detect.go
index 80716d5d..d5bcb2a5 100644
--- a/buildpack/detect.go
+++ b/buildpack/detect.go
@@ -30,6 +30,7 @@ type DetectInputs struct {
 	BuildConfigDir string
 	PlatformDir    string
 	Env            BuildEnv
+	TargetEnv      []string
 }
 
 type DetectOutputs struct {
@@ -178,6 +179,9 @@ func runDetect(d detectable, inputs DetectInputs, planPath string, envRootDirKey
 			EnvBuildPlanPath+"="+planPath,
 		)
 	}
+	if api.MustParse(d.API()).AtLeast("0.10") {
+		cmd.Env = append(cmd.Env, inputs.TargetEnv...)
+	}
 
 	if err := cmd.Run(); err != nil {
 		if err, ok := err.(*exec.ExitError); ok {
diff --git a/buildpack/generate.go b/buildpack/generate.go
index a3aacd6a..3d0e2b95 100644
--- a/buildpack/generate.go
+++ b/buildpack/generate.go
@@ -7,6 +7,7 @@ import (
 	"os/exec"
 	"path/filepath"
 
+	"github.com/buildpacks/lifecycle/api"
 	"github.com/buildpacks/lifecycle/internal/extend"
 	"github.com/buildpacks/lifecycle/launch"
 	"github.com/buildpacks/lifecycle/log"
@@ -25,6 +26,7 @@ type GenerateInputs struct {
 	OutputDir      string // a temp directory provided by the lifecycle to capture extensions output
 	PlatformDir    string
 	Env            BuildEnv
+	TargetEnv      []string
 	Out, Err       io.Writer
 	Plan           Plan
 }
@@ -103,6 +105,9 @@ func runGenerateCmd(d ExtDescriptor, extOutputDir, planPath string, inputs Gener
 		EnvOutputDir+"="+extOutputDir,
 		EnvPlatformDir+"="+inputs.PlatformDir,
 	)
+	if api.MustParse(d.API()).AtLeast("0.10") {
+		cmd.Env = append(cmd.Env, inputs.TargetEnv...)
+	}
 
 	if err := cmd.Run(); err != nil {
 		return NewError(err, ErrTypeBuildpack)
diff --git a/env/build.go b/env/build.go
index 1843595c..ee2e7f25 100644
--- a/env/build.go
+++ b/env/build.go
@@ -8,11 +8,6 @@ import (
 // BuildEnvIncludelist are env vars that, if set in the lifecycle's execution environment - either in a builder or by the platform, are passed-through to buildpack executables
 var BuildEnvIncludelist = []string{
 	"CNB_STACK_ID", // deprecated as of api 0.12.0
-	"CNB_TARGET_OS",
-	"CNB_TARGET_ARCH",
-	"CNB_TARGET_ARCH_VARIANT",
-	"CNB_TARGET_DISTRO_NAME",
-	"CNB_TARGET_DISTRO_VERSION",
 	"HOSTNAME",
 	"HOME",
 	"HTTPS_PROXY",
diff --git a/phase/builder.go b/phase/builder.go
index 6f9ae736..eae12dec 100644
--- a/phase/builder.go
+++ b/phase/builder.go
@@ -67,11 +67,6 @@ func (b *Builder) Build() (*files.BuildMetadata, error) {
 	)
 	processMap := newProcessMap()
 	inputs := b.getBuildInputs()
-	if b.AnalyzeMD.RunImage != nil && b.AnalyzeMD.RunImage.TargetMetadata != nil && b.PlatformAPI.AtLeast("0.12") {
-		inputs.Env = env.NewBuildEnv(append(os.Environ(), platform.EnvVarsFor(*b.AnalyzeMD.RunImage.TargetMetadata)...))
-	} else {
-		inputs.Env = env.NewBuildEnv(os.Environ())
-	}
 
 	filteredPlan := b.Plan
 
@@ -153,6 +148,8 @@ func (b *Builder) getBuildInputs() buildpack.BuildInputs {
 		BuildConfigDir: b.BuildConfigDir,
 		LayersDir:      b.LayersDir,
 		PlatformDir:    b.PlatformDir,
+		Env:            env.NewBuildEnv(os.Environ()),
+		TargetEnv:      platform.EnvVarsFor(b.AnalyzeMD.RunImageTarget(), b.Logger),
 		Out:            b.Out,
 		Err:            b.Err,
 	}
diff --git a/phase/detector.go b/phase/detector.go
index f230a878..4324fdc6 100644
--- a/phase/detector.go
+++ b/phase/detector.go
@@ -238,11 +238,8 @@ func (d *Detector) detectGroup(group buildpack.Group, done []buildpack.GroupElem
 					AppDir:         d.AppDir,
 					BuildConfigDir: d.BuildConfigDir,
 					PlatformDir:    d.PlatformDir,
-				}
-				if d.AnalyzeMD.RunImage != nil && d.AnalyzeMD.RunImage.TargetMetadata != nil && d.PlatformAPI.AtLeast("0.12") {
-					inputs.Env = env.NewBuildEnv(append(os.Environ(), platform.EnvVarsFor(*d.AnalyzeMD.RunImage.TargetMetadata)...))
-				} else {
-					inputs.Env = env.NewBuildEnv(os.Environ())
+					Env:            env.NewBuildEnv(os.Environ()),
+					TargetEnv:      platform.EnvVarsFor(d.AnalyzeMD.RunImageTarget(), d.Logger),
 				}
 				d.Runs.Store(key, d.Executor.Detect(descriptor, inputs, d.Logger)) // this is where we finally invoke bin/detect
 			}
diff --git a/phase/generator.go b/phase/generator.go
index ac80ba01..7f6e5410 100644
--- a/phase/generator.go
+++ b/phase/generator.go
@@ -100,9 +100,6 @@ func (g *Generator) Generate() (GenerateResult, error) {
 		g.Logger.Debug("Finding plan")
 		inputs.Plan = filteredPlan.Find(buildpack.KindExtension, ext.ID)
 
-		if g.AnalyzedMD.RunImage != nil && g.AnalyzedMD.RunImage.TargetMetadata != nil && g.PlatformAPI.AtLeast("0.12") {
-			inputs.Env = env.NewBuildEnv(append(inputs.Env.List(), platform.EnvVarsFor(*g.AnalyzedMD.RunImage.TargetMetadata)...))
-		}
 		g.Logger.Debug("Invoking command")
 		result, err := g.Executor.Generate(*descriptor, inputs, g.Logger)
 		if err != nil {
@@ -154,6 +151,7 @@ func (g *Generator) getGenerateInputs() buildpack.GenerateInputs {
 		BuildConfigDir: g.BuildConfigDir,
 		PlatformDir:    g.PlatformDir,
 		Env:            env.NewBuildEnv(os.Environ()),
+		TargetEnv:      platform.EnvVarsFor(g.AnalyzedMD.RunImageTarget(), g.Logger),
 		Out:            g.Out,
 		Err:            g.Err,
 	}
diff --git a/platform/target_data.go b/platform/target_data.go
index 299af83c..26977d67 100644
--- a/platform/target_data.go
+++ b/platform/target_data.go
@@ -9,11 +9,19 @@ import (
 	"github.com/buildpacks/lifecycle/platform/files"
 )
 
-// Fulfills the prophecy set forth in https://github.com/buildpacks/rfcs/blob/b8abe33f2bdc58792acf0bd094dc4ce3c8a54dbb/text/0096-remove-stacks-mixins.md?plain=1#L97
+// EnvVarsFor fulfills the prophecy set forth in https://github.com/buildpacks/rfcs/blob/b8abe33f2bdc58792acf0bd094dc4ce3c8a54dbb/text/0096-remove-stacks-mixins.md?plain=1#L97
 // by returning an array of "VARIABLE=value" strings suitable for inclusion in your environment or complete breakfast.
-func EnvVarsFor(tm files.TargetMetadata) []string {
-	ret := []string{"CNB_TARGET_OS=" + tm.OS, "CNB_TARGET_ARCH=" + tm.Arch}
-	ret = append(ret, "CNB_TARGET_ARCH_VARIANT="+tm.ArchVariant)
+func EnvVarsFor(tm files.TargetMetadata, logger log.Logger) []string {
+	// we should always have os & arch,
+	// if they are not populated try to get target information from the build-time base image
+	if tm.OS == "" {
+		GetTargetOSFromFileSystem(&fsutil.Detect{}, &tm, logger)
+	}
+	ret := []string{
+		"CNB_TARGET_OS=" + tm.OS,
+		"CNB_TARGET_ARCH=" + tm.Arch,
+		"CNB_TARGET_ARCH_VARIANT=" + tm.ArchVariant,
+	}
 	var distName, distVersion string
 	if tm.Distro != nil {
 		distName = tm.Distro.Name


Test patch:
diff --git a/env/build_test.go b/env/build_test.go
index e5f51404..07db7e6c 100644
--- a/env/build_test.go
+++ b/env/build_test.go
@@ -35,11 +35,6 @@ func testBuildEnv(t *testing.T, when spec.G, it spec.S) {
 		it("includes expected vars", func() {
 			benv := env.NewBuildEnv([]string{
 				"CNB_STACK_ID=some-stack-id",
-				"CNB_TARGET_ARCH=st-louis",
-				"CNB_TARGET_ARCH_VARIANT=suburban",
-				"CNB_TARGET_OS=BeOS",
-				"CNB_TARGET_DISTRO_NAME=web",
-				"CNB_TARGET_DISTRO_VERSION=3.0",
 				"HOSTNAME=some-hostname",
 				"HOME=some-home",
 				"HTTPS_PROXY=some-https-proxy",
@@ -59,11 +54,6 @@ func testBuildEnv(t *testing.T, when spec.G, it spec.S) {
 			sort.Strings(out)
 			expectedVars := []string{
 				"CNB_STACK_ID=some-stack-id",
-				"CNB_TARGET_ARCH=st-louis",
-				"CNB_TARGET_ARCH_VARIANT=suburban",
-				"CNB_TARGET_DISTRO_NAME=web",
-				"CNB_TARGET_DISTRO_VERSION=3.0",
-				"CNB_TARGET_OS=BeOS",
 				"CPATH=some-cpath",
 				"HOME=some-home",
 				"HOSTNAME=some-hostname",
diff --git a/phase/builder_test.go b/phase/builder_test.go
index f4778761..04507307 100644
--- a/phase/builder_test.go
+++ b/phase/builder_test.go
@@ -188,11 +188,11 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 
 			executor.EXPECT().Build(*bpA, gomock.Any(), gomock.Any()).DoAndReturn(
 				func(_ buildpack.BpDescriptor, inputs buildpack.BuildInputs, logger llog.Logger) (buildpack.BuildOutputs, 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_OS=linux")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH_VARIANT=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_NAME=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_VERSION=")
 					return buildpack.BuildOutputs{}, nil
 				},
 			)
@@ -202,11 +202,11 @@ func testBuilder(t *testing.T, when spec.G, it spec.S) {
 			dirStore.EXPECT().LookupBp("B", "v2").Return(bpB, nil)
 			executor.EXPECT().Build(*bpB, gomock.Any(), gomock.Any()).Do(
 				func(_ buildpack.BpDescriptor, inputs buildpack.BuildInputs, _ llog.Logger) (buildpack.BuildOutputs, 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_OS=linux")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH_VARIANT=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_NAME=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_VERSION=")
 					return buildpack.BuildOutputs{}, nil
 				})
 
diff --git a/phase/detector_test.go b/phase/detector_test.go
index 3663c2d1..6820ef37 100644
--- a/phase/detector_test.go
+++ b/phase/detector_test.go
@@ -220,11 +220,11 @@ func testDetector(t *testing.T, when spec.G, it spec.S) {
 			dirStore.EXPECT().LookupBp("A", "v1").Return(bpA1, nil).AnyTimes()
 			executor.EXPECT().Detect(bpA1, gomock.Any(), gomock.Any()).Do(
 				func(_ buildpack.Descriptor, inputs buildpack.DetectInputs, _ log.Logger) buildpack.DetectOutputs {
-					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_OS=linux")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_NAME=")
-					h.AssertContains(t, inputs.Env.List(), "CNB_TARGET_DISTRO_VERSION=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH=amd64")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_ARCH_VARIANT=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_OS=linux")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_NAME=")
+					h.AssertContains(t, inputs.TargetEnv, "CNB_TARGET_DISTRO_VERSION=")
 					return buildpack.DetectOutputs{}
 				})
 
diff --git a/phase/generator_test.go b/phase/generator_test.go
index 607ff6b6..cd6895bf 100644
--- a/phase/generator_test.go
+++ b/phase/generator_test.go
@@ -281,7 +281,7 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) {
 					h.Mkfile(t, "some-build.Dockerfile-content-A", buildDockerfilePathA)
 					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(),
+							h.AssertContains(t, inputs.TargetEnv,
 								"CNB_TARGET_ARCH=amd64",
 								"CNB_TARGET_ARCH_VARIANT=",
 								"CNB_TARGET_DISTRO_NAME=",
@@ -293,7 +293,7 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) {
 						})
 					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(),
+							h.AssertContains(t, inputs.TargetEnv,
 								"CNB_TARGET_ARCH=amd64",
 								"CNB_TARGET_ARCH_VARIANT=",
 								"CNB_TARGET_DISTRO_NAME=",
diff --git a/platform/run_image_test.go b/platform/run_image_test.go
index deb9a05e..7a14e684 100644
--- a/platform/run_image_test.go
+++ b/platform/run_image_test.go
@@ -204,7 +204,7 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) {
 	when(".EnvVarsFor", func() {
 		it("returns the right thing", func() {
 			tm := files.TargetMetadata{Arch: "pentium", ArchVariant: "mmx", ID: "my-id", OS: "linux", Distro: &files.OSDistro{Name: "nix", Version: "22.11"}}
-			observed := platform.EnvVarsFor(tm)
+			observed := platform.EnvVarsFor(tm, nil)
 			h.AssertContains(t, observed, "CNB_TARGET_ARCH="+tm.Arch)
 			h.AssertContains(t, observed, "CNB_TARGET_ARCH_VARIANT="+tm.ArchVariant)
 			h.AssertContains(t, observed, "CNB_TARGET_DISTRO_NAME="+tm.Distro.Name)
@@ -215,7 +215,7 @@ func testRunImage(t *testing.T, when spec.G, it spec.S) {
 
 		it("does not return the wrong thing", func() {
 			tm := files.TargetMetadata{Arch: "pentium", OS: "linux"}
-			observed := platform.EnvVarsFor(tm)
+			observed := platform.EnvVarsFor(tm, nil)
 			h.AssertContains(t, observed, "CNB_TARGET_ARCH="+tm.Arch)
 			h.AssertContains(t, observed, "CNB_TARGET_OS="+tm.OS)
 			// note: per the spec only the ID field is optional, so I guess the others should always be set: https://github.com/buildpacks/rfcs/blob/main/text/0096-remove-stacks-mixins.md#runtime-metadata