Skip to content

Add Execution Environments support (CNB_EXEC_ENV) per Buildpack API 0.12 #990

Description

@schneems

As a followup from this comment heroku/buildpacks-go#439 (comment). We should investigate adding support for CNB_EXEC_ENV. I had Claude Opus 4.6 do a rough pass on a summary and first suggested rough plan for what an API could look like. I take ownership of the contents (listing the generation mechanism for clarity and transparency...I am on the team that maintains this tool...in general, we don't want unfiltered LLM output in our issues).

Summary

Add framework-level support for the Execution Environments RFC (0134), which was finalized in Buildpack API 0.12. libcnb.rs currently supports Buildpack API 0.10.

Execution Environments allow buildpacks to adapt behavior based on a target environment (production, test, development, or custom values) set via the CNB_EXEC_ENV environment variable. The lifecycle (v0.21.0+) and Pack CLI (v0.40.0+) already have support.

Upstream tracking issue: buildpacks/rfcs#327

Motivation: heroku/buildpacks-go#439 needs this feature and currently implements it manually by reading CNB_EXEC_ENV from Env::from_current(). Adding framework-level support avoids each buildpack re-implementing the same pattern and keeps parity with the spec.

What the Spec Adds

1. CNB_EXEC_ENV environment variable

Provided by the lifecycle to both bin/detect and bin/build as a read-only, platform-provided env var. When not set, buildpacks MUST assume "production". The value MUST NOT contain / (reserved for future namespacing). Standard values are production, test, and development, but any valid string is allowed. See Execution Environments in the spec.

2. exec-env on processes in launch.toml

An optional array field on [[processes]] to restrict a process to specific execution environments. If omitted, the process applies to all environments.

[[processes]]
type = "web"
command = ["./server"]
exec-env = ["production"]

[[processes]]
type = "test"
command = ["go", "test", "./..."]
default = true
exec-env = ["test"]

3. [[buildpack.exec-env]] in buildpack.toml

Optional metadata declaring which execution environments a buildpack supports:

[[buildpack.exec-env]]
name = "production"

[[buildpack.exec-env]]
name = "test"

4. exec-env on [[order.group]] (composite buildpacks)

Restrict order group members to specific execution environments:

[[order.group]]
id = "heroku/go"
version = "2.1.8"
exec-env = ["test"]

5. exec-env in layer content metadata

Optional array in the [metadata] section of layer TOML to indicate environment-specific layers.

Proposed Changes

A. Core: Expose CNB_EXEC_ENV on BuildContext and DetectContext

Read CNB_EXEC_ENV in runtime.rs (similar to how context_target() reads CNB_TARGET_* env vars) and add an exec_env field to both context structs:

// libcnb/src/build.rs
pub struct BuildContext<B: Buildpack + ?Sized> {
    pub layers_dir: PathBuf,
    pub app_dir: PathBuf,
    pub buildpack_dir: PathBuf,
    pub target: Target,
    pub exec_env: ExecEnv, // NEW
    pub platform: B::Platform,
    pub buildpack_plan: BuildpackPlan,
    pub buildpack_descriptor: ComponentBuildpackDescriptor<B::Metadata>,
    pub store: Option<Store>,
}

// libcnb/src/detect.rs
pub struct DetectContext<B: Buildpack + ?Sized> {
    pub app_dir: PathBuf,
    pub buildpack_dir: PathBuf,
    pub target: Target,
    pub exec_env: ExecEnv, // NEW
    pub platform: B::Platform,
    pub buildpack_descriptor: ComponentBuildpackDescriptor<B::Metadata>,
}

Since the spec says to default to "production" when CNB_EXEC_ENV is unset, reading it should not produce an error (unlike CNB_TARGET_OS which is mandatory). Something like:

// libcnb/src/runtime.rs
fn context_exec_env<E: Debug>() -> crate::Result<ExecEnv, E> {
    let value = env::var("CNB_EXEC_ENV").unwrap_or_else(|_| String::from("production"));
    value.parse().map_err(Error::InvalidExecEnv)
}

B. The ExecEnv Type

The spec says CNB_EXEC_ENV is a string that MUST NOT contain / and reserves production, test, development as standard values while allowing any other conforming string. There are a few options for the Rust type:

Option 1: Validated newtype (recommended)

Follows the existing pattern used by ProcessType and LayerName:

// In libcnb-data
libcnb_newtype!(
    exec_env,
    /// Construct an [`ExecEnv`] value at compile time.
    exec_env,
    /// The execution environment for a buildpack build.
    ///
    /// Standard values are `production`, `test`, and `development`.
    /// MUST only contain numbers, letters, and the characters `.`, `_`, and `-`.
    ExecEnv,
    ExecEnvError,
    r"^[[:alnum:]._-]+$"
);

Usage by buildpack authors:

use libcnb_data::exec_env;

fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
    if context.exec_env == exec_env!("test") {
        // include test dependencies, leave toolchain on image, etc.
    }
    // ...
}

Option 2: Enum with known + custom variants

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExecEnv {
    Production,
    Test,
    Development,
    Other(String), // validated: no `/`
}

This is more ergonomic for match but diverges from the newtype pattern used elsewhere in the codebase and requires a custom Serialize/Deserialize implementation.

Option 3: Plain String

The simplest approach, matching how Target fields are plain Strings:

pub exec_env: String,

However, this loses the validation of the /-exclusion rule and provides no discoverability of standard values.

C. Data Types in libcnb-data

  1. Add the ExecEnv type (whichever option is chosen above).
  2. Add optional exec_env field to Process in libcnb-data/src/launch.rs:
    pub struct Process {
        pub r#type: ProcessType,
        pub command: Vec<String>,
        pub args: Vec<String>,
        pub default: bool,
        pub working_directory: WorkingDirectory,
        pub exec_env: Vec<ExecEnv>,  // NEW - empty means "all environments"
    }
  3. Add exec_env to ProcessBuilder.
  4. Add [[buildpack.exec-env]] support to ComponentBuildpackDescriptor.
  5. Add exec-env to order group in CompositeBuildpackDescriptor.

D. Buildpack API Version Bump

Update LIBCNB_SUPPORTED_BUILDPACK_API from { major: 0, minor: 10 } to { major: 0, minor: 12 } in libcnb/src/lib.rs.

Note: Buildpack API 0.11 exists between 0.10 and 0.12 and should be reviewed before bumping the version.

E. Test Framework (libcnb-test)

Verify that BuildConfig::env() in libcnb-test can set CNB_EXEC_ENV for integration tests. This likely already works via the generic env var support, but should be verified and documented.

Implementation Priority

The changes can be shipped incrementally:

  1. Phase 1 (minimum viable): Add ExecEnv type, read CNB_EXEC_ENV, expose on both contexts. This unblocks buildpacks like the Go buildpack.
  2. Phase 2: Add exec-env to Process in launch.toml, update ProcessBuilder.
  3. Phase 3: Add exec-env to buildpack.toml descriptors (component and composite).
  4. Phase 4: Bump LIBCNB_SUPPORTED_BUILDPACK_API to 0.12 once all pieces are in place.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions