Skip to content

Commit

Permalink
Env helper layers
Browse files Browse the repository at this point in the history
When a developer needs to set environment variables only, they have to make a layer struct and pass it to `handle_layer`. Instead of forcing them to write a boilerplate struct, this commit provides two utility structs:

- `DefaultEnvLayer` - Used to set default environment variables
- `ConfigureEnvLayer` - Can be used with `LayerEnv::new()` to configure any environment variables (versus `DefaultEnvLayer` only creates default environment variables).

An alternative to providing structs could be to provide functions that accept a context and layer name in addition to the required env data.
  • Loading branch information
schneems committed Jul 24, 2023
1 parent bc09604 commit 3d15e59
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ separate changelogs for each crate were used. If you need to refer to these old

### Added

- `libherokubuildpack`: Add `env::DefaultEnvLayer` and `env::ConfigureEnvLayer` structs for setting environment variables ([#598](https://github.com/heroku/libcnb.rs/pull/598))
- `libcnb-package`: Add cross-compilation assistance for Linux `aarch64-unknown-linux-musl`. ([#577](https://github.com/heroku/libcnb.rs/pull/577))

### Changed
Expand Down
3 changes: 2 additions & 1 deletion libherokubuildpack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ include = ["src/**/*", "LICENSE", "README.md"]
all-features = true

[features]
default = ["command", "download", "digest", "error", "log", "tar", "toml", "fs", "write"]
default = ["command", "download", "digest", "error", "env", "log", "tar", "toml", "fs", "write"]
download = ["dep:ureq", "dep:thiserror"]
digest = ["dep:sha2"]
env = ["dep:libcnb"]
error = ["log", "dep:libcnb"]
log = ["dep:termcolor"]
tar = ["dep:tar", "dep:flate2"]
Expand Down
209 changes: 209 additions & 0 deletions libherokubuildpack/src/env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
use libcnb::build::BuildContext;
use libcnb::data::layer_content_metadata::LayerTypes;
use libcnb::generic::GenericMetadata;
use libcnb::layer::{Layer, LayerResult, LayerResultBuilder};
use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope};
use std::ffi::OsString;
use std::marker::PhantomData;
use std::path::Path;

/// Set default environment variables
///
/// If all you need to do is set default environment values, you can use
/// the `DefaultEnvLayer::new` function to set those values without having
/// to create a struct from scratch.
///
/// ```no_run
///# use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
///# use libcnb::data::launch::{LaunchBuilder, ProcessBuilder};
///# use libcnb::data::process_type;
///# use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder};
///# use libcnb::generic::{GenericError, GenericMetadata, GenericPlatform};
///# use libcnb::{buildpack_main, Buildpack};
///# use libcnb::data::layer::LayerName;
///
///# pub(crate) struct HelloWorldBuildpack;
///
/// use libcnb::Env;
/// use libcnb::data::layer_name;
/// use libcnb::layer_env::Scope;
/// use libherokubuildpack::env::DefaultEnvLayer;
///
///# impl Buildpack for HelloWorldBuildpack {
///# type Platform = GenericPlatform;
///# type Metadata = GenericMetadata;
///# type Error = GenericError;
///
///# fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
///# todo!()
///# }
///
///# fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
/// let env = Env::from_current();
/// // Don't forget to apply context.platform.env() too;
///
/// let layer = context //
/// .handle_layer(
/// layer_name!("default_env"),
/// DefaultEnvLayer::new(
/// [
/// ("JRUBY_OPTS", "-Xcompile.invokedynamic=false"),
/// ("RACK_ENV", "production"),
/// ("RAILS_ENV", "production"),
/// ("RAILS_SERVE_STATIC_FILES", "enabled"),
/// ("RAILS_LOG_TO_STDOUT", "enabled"),
/// ("MALLOC_ARENA_MAX", "2"),
/// ("DISABLE_SPRING", "1"),
/// ]
/// .into_iter(),
/// ),
/// )?;
/// let env = layer.env.apply(Scope::Build, &env);
///
///# todo!()
///# }
///# }
///
/// ```
pub struct DefaultEnvLayer;

impl DefaultEnvLayer {
#[allow(clippy::new_ret_no_self)]
pub fn new<E, K, V, B>(env: E) -> ConfigureEnvLayer<B>
where
E: IntoIterator<Item = (K, V)> + Clone,
K: Into<OsString>,
V: Into<OsString>,
B: libcnb::Buildpack,
{
let mut layer_env = LayerEnv::new();
for (key, value) in env {
layer_env =
layer_env.chainable_insert(Scope::All, ModificationBehavior::Default, key, value);
}

ConfigureEnvLayer {
data: layer_env,
_buildpack: PhantomData,
}
}
}

/// Set environment variables
///
/// If you want to set many default environment variables you can use
/// `DefaultEnvLayer`. If you need to set different types of environment
/// variables you can use this struct `ConfigureEnvLayer`
///
/// Example:
///
/// ```no_run
///# use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
///# use libcnb::data::launch::{LaunchBuilder, ProcessBuilder};
///# use libcnb::data::process_type;
///# use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder};
///# use libcnb::generic::{GenericError, GenericMetadata, GenericPlatform};
///# use libcnb::{buildpack_main, Buildpack};
///# use libcnb::data::layer::LayerName;
///
///# pub(crate) struct HelloWorldBuildpack;
///
/// use libcnb::Env;
/// use libcnb::data::layer_name;
/// use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope};
/// use libherokubuildpack::env::ConfigureEnvLayer;
///
///# impl Buildpack for HelloWorldBuildpack {
///# type Platform = GenericPlatform;
///# type Metadata = GenericMetadata;
///# type Error = GenericError;
///
///# fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
///# todo!()
///# }
///
///# fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
/// let env = Env::from_current();
/// // Don't forget to apply context.platform.env() too;
///
/// let layer = context //
/// .handle_layer(
/// layer_name!("configure_env"),
/// ConfigureEnvLayer::new(
/// LayerEnv::new()
/// .chainable_insert(
/// Scope::All,
/// ModificationBehavior::Override,
/// "BUNDLE_GEMFILE", // Tells bundler where to find the `Gemfile`
/// context.app_dir.join("Gemfile"),
/// )
/// .chainable_insert(
/// Scope::All,
/// ModificationBehavior::Override,
/// "BUNDLE_CLEAN", // After successful `bundle install` bundler will automatically run `bundle clean`
/// "1",
/// )
/// .chainable_insert(
/// Scope::All,
/// ModificationBehavior::Override,
/// "BUNDLE_DEPLOYMENT", // Requires the `Gemfile.lock` to be in sync with the current `Gemfile`.
/// "1",
/// )
/// .chainable_insert(
/// Scope::All,
/// ModificationBehavior::Default,
/// "MY_ENV_VAR",
/// "Whatever I want"
/// )
/// ),
/// )?;
/// let env = layer.env.apply(Scope::Build, &env);
///
///# todo!()
///# }
///# }
///
/// ```
pub struct ConfigureEnvLayer<B: libcnb::Buildpack> {
pub(crate) data: LayerEnv,
pub(crate) _buildpack: std::marker::PhantomData<B>,
}

impl<B> ConfigureEnvLayer<B>
where
B: libcnb::Buildpack,
{
#[must_use]
pub fn new(env: LayerEnv) -> Self {
ConfigureEnvLayer {
data: env,
_buildpack: PhantomData,
}
}
}

impl<B> Layer for ConfigureEnvLayer<B>
where
B: libcnb::Buildpack,
{
type Buildpack = B;
type Metadata = GenericMetadata;

fn types(&self) -> LayerTypes {
LayerTypes {
build: true,
launch: true,
cache: false,
}
}

fn create(
&self,
_context: &BuildContext<Self::Buildpack>,
_layer_path: &Path,
) -> Result<LayerResult<Self::Metadata>, B::Error> {
LayerResultBuilder::new(GenericMetadata::default())
.env(self.data.clone())
.build()
}
}
2 changes: 2 additions & 0 deletions libherokubuildpack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub mod command;
pub mod digest;
#[cfg(feature = "download")]
pub mod download;
#[cfg(feature = "env")]
pub mod env;
#[cfg(feature = "error")]
pub mod error;
#[cfg(feature = "fs")]
Expand Down

0 comments on commit 3d15e59

Please sign in to comment.