use crate::Env;
use std::cmp::Ordering;
use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsString;
use std::fs;
use std::path::Path;
#[derive(Eq, PartialEq, Debug, Default, Clone)]
pub struct LayerEnv {
all: LayerEnvDelta,
build: LayerEnvDelta,
launch: LayerEnvDelta,
process: HashMap<String, LayerEnvDelta>,
layer_paths_build: LayerEnvDelta,
layer_paths_launch: LayerEnvDelta,
}
impl LayerEnv {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn apply(&self, scope: Scope, env: &Env) -> Env {
let deltas = match scope {
Scope::All => vec![&self.all],
Scope::Build => vec![&self.layer_paths_build, &self.all, &self.build],
Scope::Launch => vec![&self.layer_paths_launch, &self.all, &self.launch],
Scope::Process(process) => {
let mut process_deltas = vec![&self.all];
if let Some(process_specific_delta) = self.process.get(&process) {
process_deltas.push(process_specific_delta);
}
process_deltas
}
};
deltas
.iter()
.fold(env.clone(), |env, delta| delta.apply(&env))
}
#[must_use]
pub fn apply_to_empty(&self, scope: Scope) -> Env {
self.apply(scope, &Env::new())
}
pub fn insert(
&mut self,
scope: Scope,
modification_behavior: ModificationBehavior,
name: impl Into<OsString>,
value: impl Into<OsString>,
) {
let target_delta = match scope {
Scope::All => &mut self.all,
Scope::Build => &mut self.build,
Scope::Launch => &mut self.launch,
Scope::Process(process_type_name) => match self.process.entry(process_type_name) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => entry.insert(LayerEnvDelta::new()),
},
};
target_delta.insert(modification_behavior, name, value);
}
#[must_use]
pub fn chainable_insert(
mut self,
scope: Scope,
modification_behavior: ModificationBehavior,
name: impl Into<OsString>,
value: impl Into<OsString>,
) -> Self {
self.insert(scope, modification_behavior, name, value);
self
}
pub fn read_from_layer_dir(layer_dir: impl AsRef<Path>) -> Result<Self, std::io::Error> {
let mut result_layer_env = Self::new();
let bin_path = layer_dir.as_ref().join("bin");
let lib_path = layer_dir.as_ref().join("lib");
let include_path = layer_dir.as_ref().join("include");
let pkgconfig_path = layer_dir.as_ref().join("pkgconfig");
let layer_path_specs = [
("PATH", Scope::Build, &bin_path),
("LIBRARY_PATH", Scope::Build, &lib_path),
("LD_LIBRARY_PATH", Scope::Build, &lib_path),
("CPATH", Scope::Build, &include_path),
("PKG_CONFIG_PATH", Scope::Build, &pkgconfig_path),
("PATH", Scope::Launch, &bin_path),
("LD_LIBRARY_PATH", Scope::Launch, &lib_path),
];
for (name, scope, path) in layer_path_specs {
if path.is_dir() {
let target_delta = match scope {
Scope::Build => &mut result_layer_env.layer_paths_build,
Scope::Launch => &mut result_layer_env.layer_paths_launch,
_ => unreachable!(
"Unexpected Scope in read_from_layer_dir implementation. This is a libcnb implementation error!"
),
};
target_delta.insert(ModificationBehavior::Prepend, name, path);
target_delta.insert(ModificationBehavior::Delimiter, name, PATH_LIST_SEPARATOR);
}
}
let env_path = layer_dir.as_ref().join("env");
if env_path.is_dir() {
result_layer_env.all = LayerEnvDelta::read_from_env_dir(env_path)?;
}
let env_build_path = layer_dir.as_ref().join("env.build");
if env_build_path.is_dir() {
result_layer_env.build = LayerEnvDelta::read_from_env_dir(env_build_path)?;
}
let env_launch_path = layer_dir.as_ref().join("env.launch");
if env_launch_path.is_dir() {
result_layer_env.launch = LayerEnvDelta::read_from_env_dir(env_launch_path)?;
}
Ok(result_layer_env)
}
pub fn write_to_layer_dir(&self, layer_dir: impl AsRef<Path>) -> std::io::Result<()> {
self.all.write_to_env_dir(layer_dir.as_ref().join("env"))?;
self.build
.write_to_env_dir(layer_dir.as_ref().join("env.build"))?;
let launch_env_dir = layer_dir.as_ref().join("env.launch");
self.launch.write_to_env_dir(&launch_env_dir)?;
for (process_name, delta) in &self.process {
delta.write_to_env_dir(launch_env_dir.join(process_name))?;
}
Ok(())
}
}
#[derive(Eq, PartialEq, Debug, Clone)]
pub enum ModificationBehavior {
Append,
Default,
Delimiter,
Override,
Prepend,
}
impl Ord for ModificationBehavior {
fn cmp(&self, other: &Self) -> Ordering {
fn index(value: &ModificationBehavior) -> i32 {
match value {
ModificationBehavior::Append => 0,
ModificationBehavior::Default => 1,
ModificationBehavior::Delimiter => 2,
ModificationBehavior::Override => 3,
ModificationBehavior::Prepend => 4,
}
}
index(self).cmp(&index(other))
}
}
impl PartialOrd for ModificationBehavior {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Eq, PartialEq, Debug, Clone)]
pub enum Scope {
All,
Build,
Launch,
Process(String),
}
#[derive(Eq, PartialEq, Debug, Default, Clone)]
struct LayerEnvDelta {
entries: BTreeMap<(ModificationBehavior, OsString), OsString>,
}
impl LayerEnvDelta {
fn new() -> Self {
Self::default()
}
fn apply(&self, env: &Env) -> Env {
let mut result_env = env.clone();
for ((modification_behavior, name), value) in &self.entries {
match modification_behavior {
ModificationBehavior::Override => {
result_env.insert(name, value);
}
ModificationBehavior::Default => {
if !result_env.contains_key(name) {
result_env.insert(name, value);
}
}
ModificationBehavior::Append => {
let mut previous_value = result_env.get(name).cloned().unwrap_or_default();
if !previous_value.is_empty() {
previous_value.push(self.delimiter_for(name));
}
previous_value.push(value);
result_env.insert(name, previous_value);
}
ModificationBehavior::Prepend => {
let previous_value = result_env.get(name).cloned().unwrap_or_default();
let mut new_value = OsString::new();
new_value.push(value);
if !previous_value.is_empty() {
new_value.push(self.delimiter_for(name));
new_value.push(previous_value);
}
result_env.insert(name, new_value);
}
ModificationBehavior::Delimiter => (),
}
}
result_env
}
fn delimiter_for(&self, key: impl Into<OsString>) -> OsString {
self.entries
.get(&(ModificationBehavior::Delimiter, key.into()))
.cloned()
.unwrap_or_default()
}
fn read_from_env_dir(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
let mut layer_env = Self::new();
for dir_entry in fs::read_dir(path.as_ref())? {
let path = dir_entry?.path();
#[cfg(target_family = "unix")]
let file_contents = {
use std::os::unix::ffi::OsStringExt;
OsString::from_vec(fs::read(&path)?)
};
#[cfg(not(target_family = "unix"))]
let file_contents = OsString::from(&fs::read_to_string(&path)?);
let file_name_stem = path.file_stem();
let file_name_extension = path.extension();
if let Some(file_name_stem) = file_name_stem {
let modification_behavior = match file_name_extension {
None => Some(ModificationBehavior::Override),
Some(file_name_extension) => match file_name_extension.to_str() {
Some("append") => Some(ModificationBehavior::Append),
Some("default") => Some(ModificationBehavior::Default),
Some("delim") => Some(ModificationBehavior::Delimiter),
Some("override") => Some(ModificationBehavior::Override),
Some("prepend") => Some(ModificationBehavior::Prepend),
Some(_) | None => None,
},
};
if let Some(modification_behavior) = modification_behavior {
layer_env.insert(
modification_behavior,
file_name_stem.to_os_string(),
file_contents,
);
}
}
}
Ok(layer_env)
}
fn write_to_env_dir(&self, path: impl AsRef<Path>) -> Result<(), std::io::Error> {
if path.as_ref().exists() {
fs::remove_dir_all(path.as_ref())?;
}
if !self.entries.is_empty() {
fs::create_dir_all(path.as_ref())?;
for ((modification_behavior, name), value) in &self.entries {
let file_extension = match modification_behavior {
ModificationBehavior::Append => ".append",
ModificationBehavior::Default => ".default",
ModificationBehavior::Delimiter => ".delim",
ModificationBehavior::Override => ".override",
ModificationBehavior::Prepend => ".prepend",
};
let mut file_name = name.clone();
file_name.push(file_extension);
let file_path = path.as_ref().join(file_name);
#[cfg(target_family = "unix")]
{
use std::os::unix::ffi::OsStrExt;
fs::write(file_path, value.as_bytes())?;
}
#[cfg(not(target_family = "unix"))]
fs::write(file_path, &value.to_string_lossy().as_bytes())?;
}
}
Ok(())
}
fn insert(
&mut self,
modification_behavior: ModificationBehavior,
name: impl Into<OsString>,
value: impl Into<OsString>,
) -> &Self {
self.entries
.insert((modification_behavior, name.into()), value.into());
self
}
}
#[cfg(target_family = "unix")]
const PATH_LIST_SEPARATOR: &str = ":";
#[cfg(target_family = "windows")]
const PATH_LIST_SEPARATOR: &str = ";";
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs;
use tempfile::tempdir;
use crate::layer_env::{Env, LayerEnv, ModificationBehavior, Scope};
use super::LayerEnvDelta;
#[test]
fn reference_impl_env_files_have_a_suffix_it_performs_the_matching_action() {
let temp_dir = tempdir().unwrap();
let mut files = HashMap::new();
files.insert("VAR_APPEND.append", "value-append");
files.insert("VAR_APPEND_NEW.append", "value-append");
files.insert("VAR_APPEND_DELIM.append", "value-append-delim");
files.insert("VAR_APPEND_DELIM_NEW.append", "value-append-delim");
files.insert("VAR_APPEND_DELIM.delim", "[]");
files.insert("VAR_APPEND_DELIM_NEW.delim", "[]");
files.insert("VAR_PREPEND.prepend", "value-prepend");
files.insert("VAR_PREPEND_NEW.prepend", "value-prepend");
files.insert("VAR_PREPEND_DELIM.prepend", "value-prepend-delim");
files.insert("VAR_PREPEND_DELIM_NEW.prepend", "value-prepend-delim");
files.insert("VAR_PREPEND_DELIM.delim", "[]");
files.insert("VAR_PREPEND_DELIM_NEW.delim", "[]");
files.insert("VAR_DEFAULT.default", "value-default");
files.insert("VAR_DEFAULT_NEW.default", "value-default");
files.insert("VAR_OVERRIDE.override", "value-override");
files.insert("VAR_OVERRIDE_NEW.override", "value-override");
files.insert("VAR_IGNORE.ignore", "value-ignore");
for (file_name, file_contents) in files {
fs::write(temp_dir.path().join(file_name), file_contents).unwrap();
}
let mut original_env = Env::new();
original_env.insert("VAR_APPEND", "value-append-orig");
original_env.insert("VAR_APPEND_DELIM", "value-append-delim-orig");
original_env.insert("VAR_PREPEND", "value-prepend-orig");
original_env.insert("VAR_PREPEND_DELIM", "value-prepend-delim-orig");
original_env.insert("VAR_DEFAULT", "value-default-orig");
original_env.insert("VAR_OVERRIDE", "value-override-orig");
let layer_env_delta = LayerEnvDelta::read_from_env_dir(temp_dir.path()).unwrap();
let modified_env = layer_env_delta.apply(&original_env);
assert_eq!(
vec![
("VAR_APPEND", "value-append-origvalue-append"),
(
"VAR_APPEND_DELIM",
"value-append-delim-orig[]value-append-delim"
),
("VAR_APPEND_DELIM_NEW", "value-append-delim"),
("VAR_APPEND_NEW", "value-append"),
("VAR_DEFAULT", "value-default-orig"),
("VAR_DEFAULT_NEW", "value-default"),
("VAR_OVERRIDE", "value-override"),
("VAR_OVERRIDE_NEW", "value-override"),
("VAR_PREPEND", "value-prependvalue-prepend-orig"),
(
"VAR_PREPEND_DELIM",
"value-prepend-delim[]value-prepend-delim-orig"
),
("VAR_PREPEND_DELIM_NEW", "value-prepend-delim"),
("VAR_PREPEND_NEW", "value-prepend"),
],
environment_as_sorted_vector(&modified_env)
);
}
#[test]
fn reference_impl_env_files_have_no_suffix_default_action_is_override() {
let temp_dir = tempdir().unwrap();
let mut files = HashMap::new();
files.insert("VAR_NORMAL", "value-normal");
files.insert("VAR_NORMAL_NEW", "value-normal");
files.insert("VAR_NORMAL_DELIM", "value-normal-delim");
files.insert("VAR_NORMAL_DELIM_NEW", "value-normal-delim");
files.insert("VAR_NORMAL_DELIM.delim", "[]");
files.insert("VAR_NORMAL_DELIM_NEW.delim", "[]");
for (file_name, file_contents) in files {
fs::write(temp_dir.path().join(file_name), file_contents).unwrap();
}
let mut original_env = Env::new();
original_env.insert("VAR_NORMAL", "value-normal-orig");
original_env.insert("VAR_NORMAL_DELIM", "value-normal-delim-orig");
let layer_env_delta = LayerEnvDelta::read_from_env_dir(temp_dir.path()).unwrap();
let modified_env = layer_env_delta.apply(&original_env);
assert_eq!(
vec![
("VAR_NORMAL", "value-normal"),
("VAR_NORMAL_DELIM", "value-normal-delim"),
("VAR_NORMAL_DELIM_NEW", "value-normal-delim"),
("VAR_NORMAL_NEW", "value-normal"),
],
environment_as_sorted_vector(&modified_env)
);
}
#[test]
fn reference_impl_add_root_dir_should_append_posix_directories() {
let temp_dir = tempdir().unwrap();
fs::create_dir_all(temp_dir.path().join("bin")).unwrap();
fs::create_dir_all(temp_dir.path().join("lib")).unwrap();
let mut original_env = Env::new();
original_env.insert("PATH", "some");
original_env.insert("LD_LIBRARY_PATH", "some-ld");
original_env.insert("LIBRARY_PATH", "some-library");
let layer_env = LayerEnv::read_from_layer_dir(temp_dir.path()).unwrap();
let modified_env = layer_env.apply(Scope::Build, &original_env);
assert_eq!(
vec![
(
"LD_LIBRARY_PATH",
format!("{}:some-ld", temp_dir.path().join("lib").to_str().unwrap()).as_str()
),
(
"LIBRARY_PATH",
format!(
"{}:some-library",
temp_dir.path().join("lib").to_str().unwrap()
)
.as_str()
),
(
"PATH",
format!("{}:some", temp_dir.path().join("bin").to_str().unwrap()).as_str()
)
],
environment_as_sorted_vector(&modified_env)
);
}
#[test]
fn layer_env_delta_fs_read_write() {
let mut original_delta = LayerEnvDelta::new();
original_delta.insert(ModificationBehavior::Append, "APPEND_TO_ME", "NEW_VALUE");
original_delta.insert(
ModificationBehavior::Default,
"SET_THE_DEFAULT",
"DEFAULT_VALUE",
);
original_delta.insert(ModificationBehavior::Delimiter, "APPEND_TO_ME", ";");
original_delta.insert(
ModificationBehavior::Override,
"OVERRIDE_THIS",
"OVERRIDE_VALUE",
);
original_delta.insert(
ModificationBehavior::Prepend,
"PREPEND_THIS",
"PREPEND_VALUE",
);
let temp_dir = tempdir().unwrap();
original_delta.write_to_env_dir(temp_dir.path()).unwrap();
let disk_delta = LayerEnvDelta::read_from_env_dir(temp_dir.path()).unwrap();
assert_eq!(original_delta, disk_delta);
}
#[test]
fn layer_env_insert() {
let mut layer_env = LayerEnv::new();
layer_env.insert(
Scope::Build,
ModificationBehavior::Append,
"MAVEN_OPTS",
"-Dskip.tests=true",
);
layer_env.insert(
Scope::All,
ModificationBehavior::Override,
"JAVA_TOOL_OPTIONS",
"-Xmx1G",
);
layer_env.insert(
Scope::Build,
ModificationBehavior::Override,
"JAVA_TOOL_OPTIONS",
"-Xmx2G",
);
layer_env.insert(
Scope::Launch,
ModificationBehavior::Append,
"JAVA_TOOL_OPTIONS",
"-XX:+UseSerialGC",
);
let result_env = layer_env.apply_to_empty(Scope::Build);
assert_eq!(
vec![
("JAVA_TOOL_OPTIONS", "-Xmx2G"),
("MAVEN_OPTS", "-Dskip.tests=true")
],
environment_as_sorted_vector(&result_env)
);
}
#[test]
fn modification_behavior_order() {
let tests = [
(
ModificationBehavior::Append,
ModificationBehavior::Default,
Ordering::Less,
),
(
ModificationBehavior::Append,
ModificationBehavior::Override,
Ordering::Less,
),
(
ModificationBehavior::Prepend,
ModificationBehavior::Append,
Ordering::Greater,
),
(
ModificationBehavior::Default,
ModificationBehavior::Delimiter,
Ordering::Less,
),
(
ModificationBehavior::Default,
ModificationBehavior::Default,
Ordering::Equal,
),
];
for (a, b, expected) in tests {
assert_eq!(expected, a.cmp(&b));
}
}
#[test]
fn layer_env_delta_eq() {
let mut delta_1 = LayerEnvDelta::new();
delta_1.insert(ModificationBehavior::Default, "a", "avalue");
delta_1.insert(ModificationBehavior::Default, "b", "bvalue");
delta_1.insert(ModificationBehavior::Override, "c", "cvalue");
let mut delta_2 = LayerEnvDelta::new();
delta_2.insert(ModificationBehavior::Default, "b", "bvalue");
delta_2.insert(ModificationBehavior::Override, "c", "cvalue");
delta_2.insert(ModificationBehavior::Default, "a", "avalue");
assert_eq!(delta_1, delta_2);
}
#[test]
fn read_from_layer_dir_layer_paths_launch() {
let temp_dir = tempdir().unwrap();
let layer_dir = temp_dir.path();
fs::create_dir_all(layer_dir.join("bin")).unwrap();
fs::create_dir_all(layer_dir.join("lib")).unwrap();
fs::create_dir_all(layer_dir.join("include")).unwrap();
fs::create_dir_all(layer_dir.join("pkgconfig")).unwrap();
let layer_env = LayerEnv::read_from_layer_dir(layer_dir).unwrap();
let env = layer_env.apply_to_empty(Scope::Launch);
assert_eq!(env.get("PATH").unwrap(), &layer_dir.join("bin"));
assert_eq!(env.get("LD_LIBRARY_PATH").unwrap(), &layer_dir.join("lib"));
assert_eq!(env.get("LIBRARY_PATH"), None);
assert_eq!(env.get("CPATH"), None);
assert_eq!(env.get("PKG_CONFIG_PATH"), None);
}
#[test]
fn layer_paths_come_before_manually_added_paths() {
const TEST_ENV_VALUE: &str = "test-value";
let test_cases = [
("bin", "PATH", Scope::Build),
("bin", "PATH", Scope::Launch),
("lib", "LIBRARY_PATH", Scope::Build),
];
for (path, name, scope) in test_cases {
let temp_dir = tempdir().unwrap();
let layer_dir = temp_dir.path();
let absolute_path = layer_dir.join(path);
fs::create_dir_all(&absolute_path).unwrap();
let mut layer_env = LayerEnv::new();
layer_env.insert(
scope.clone(),
ModificationBehavior::Prepend,
name,
TEST_ENV_VALUE,
);
layer_env.write_to_layer_dir(layer_dir).unwrap();
let env = LayerEnv::read_from_layer_dir(layer_dir)
.unwrap()
.apply_to_empty(scope.clone());
let mut expected_env_value = OsString::new();
expected_env_value.push(TEST_ENV_VALUE);
expected_env_value.push(absolute_path.into_os_string());
assert_eq!(
env.get(name),
Some(&expected_env_value),
"For ENV var `{name}` scope `{scope:?}`"
);
}
}
#[test]
fn read_from_layer_dir_layer_paths_build() {
let temp_dir = tempdir().unwrap();
let layer_dir = temp_dir.path();
fs::create_dir_all(layer_dir.join("bin")).unwrap();
fs::create_dir_all(layer_dir.join("lib")).unwrap();
fs::create_dir_all(layer_dir.join("include")).unwrap();
fs::create_dir_all(layer_dir.join("pkgconfig")).unwrap();
let layer_env = LayerEnv::read_from_layer_dir(layer_dir).unwrap();
let env = layer_env.apply_to_empty(Scope::Build);
assert_eq!(env.get("PATH").unwrap(), &layer_dir.join("bin"));
assert_eq!(env.get("LD_LIBRARY_PATH").unwrap(), &layer_dir.join("lib"));
assert_eq!(env.get("LIBRARY_PATH").unwrap(), &layer_dir.join("lib"));
assert_eq!(env.get("CPATH").unwrap(), &layer_dir.join("include"));
assert_eq!(
env.get("PKG_CONFIG_PATH").unwrap(),
&layer_dir.join("pkgconfig")
);
}
fn environment_as_sorted_vector(environment: &Env) -> Vec<(&str, &str)> {
let mut result: Vec<(&str, &str)> = environment
.iter()
.map(|(k, v)| (k.to_str().unwrap(), v.to_str().unwrap()))
.collect();
result.sort_by_key(|kv| kv.0);
result
}
}