From 2db46d1b7735f22b1f610009e460030361743687 Mon Sep 17 00:00:00 2001 From: Tiffany Merry Date: Fri, 20 Dec 2024 16:40:14 -0600 Subject: [PATCH] Added a FromStr implementation to debian::MultiarchName Implment functionality to load the environment variables associate with packages from the project.toml file --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 27 +++++-- project.toml | 16 +++++ src/config/environment.rs | 98 ++++++++++++++++++++++++++ src/config/mod.rs | 1 + src/debian/multiarch_name.rs | 24 +++++++ src/install_packages.rs | 14 +++- tests/fixtures/unit_tests/project.toml | 16 +++++ tests/integration_test.rs | 44 ++++++++++++ 10 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 project.toml create mode 100644 src/config/environment.rs create mode 100644 tests/fixtures/unit_tests/project.toml diff --git a/Cargo.lock b/Cargo.lock index fe90702..c0e34eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,6 +304,7 @@ dependencies = [ "ar", "async-compression", "bon", + "buildpacks-deb-packages", "bullet_stream", "debversion", "edit-distance", diff --git a/Cargo.toml b/Cargo.toml index 1e53b51..e6b1c7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ libcnb-test = "=0.26.0" regex = "1" strip-ansi-escapes = "0.2" tempfile = "3" +buildpacks-deb-packages = { path = "." } [lints.rust] unreachable_pub = "warn" diff --git a/README.md b/README.md index 44743a3..b39c627 100644 --- a/README.md +++ b/README.md @@ -51,20 +51,30 @@ The configuration for this buildpack must be added to the project descriptor fil project using the `com.heroku.buildpacks.deb-packages` table. The list of packages to install must be specified there. See below for the [configuration schema](#schema) and an [example](#example). +### Configuring Environment Variables + +You can configure environment variables for the packages installed by this buildpack by defining them in the `project.toml` file. The environment variables are specified under the `env` key for each package. + +During the build process, the buildpack will read the `project.toml` file and apply the specified environment variables. The `{install_dir}` placeholder will be replaced with the actual paths so the variables are available at both `build` and `launch` phases using [layer environment variables][cnb-environment]. + #### Example ```toml -# _.schema-version is required for the project descriptor [_] schema-version = "0.2" -# buildpack configuration goes here [com.heroku.buildpacks.deb-packages] install = [ - # string version of a dependency to install - "package-name", - # inline-table version of a dependency to install - { name = "package-name", skip_dependencies = true, force = true } + { name = "git", + env = { "GIT_EXEC_PATH" = "{install_dir}/usr/lib/git-core", + "GIT_TEMPLATE_DIR" = "{install_dir}/usr/share/git-core/templates" } + }, + { name = "babeld" }, + { name = "ghostscript", + skip_dependencies = true, + force = true, + env = { "GS_LIB" = "{install_dir}/var/lib/ghostscript" } + }, ] ``` @@ -97,6 +107,10 @@ install = [ If set to `true`, the package will be installed even if it's already installed on the system. + - `env` *__([inline-table][toml-inline-table], optional, default={})__* + + A table of environment variables to set for the package. The keys are the variable names and the values are the variable values. The `{build_dir}` placeholder can be used in the values and will be replaced with the actual build directory path. + > [!TIP] > Users of the [heroku-community/apt][classic-apt-buildpack] can migrate their Aptfile to the above configuration by > adding a `project.toml` file with: @@ -199,6 +213,7 @@ For each package added after [determining the packages to install](#step-2-deter | `INCLUDE_PATH` | `//usr/include/`
`//usr/include` | header files | | `CPATH` | Same as `INCLUDE_PATH` | header files | | `CPPPATH` | Same as `INCLUDE_PATH` | header files | + | `PKG_CONFIG_PATH` | `//usr/lib//pkgconfig`
`//usr/lib/pkgconfig` | pc files | ## Contributing diff --git a/project.toml b/project.toml new file mode 100644 index 0000000..4f7c506 --- /dev/null +++ b/project.toml @@ -0,0 +1,16 @@ +[_] +schema-version = "0.2" + +[com.heroku.buildpacks.deb-packages] +install = [ + { name = "git", + env = { "GIT_EXEC_PATH" = "{install_dir}/usr/lib/git-core", + "GIT_TEMPLATE_DIR" = "{install_dir}/usr/share/git-core/templates" } + }, + { name = "babeld" }, + { name = "ghostscript", + skip_dependencies = true, + force = true, + env = { "GS_LIB" = "{install_dir}/var/lib/ghostscript" } + }, +] \ No newline at end of file diff --git a/src/config/environment.rs b/src/config/environment.rs new file mode 100644 index 0000000..4152f1f --- /dev/null +++ b/src/config/environment.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; +use std::fs; +use std::path::Path; +use toml_edit::{DocumentMut}; + +#[derive(Debug, Default)] +pub(crate) struct Environment { + variables: HashMap, +} + +impl Environment { + /// Load environment variables from the project.toml file based on package names. + pub(crate) fn load_from_toml(file_path: &Path, install_dir: &str) -> Self { + let mut env = Environment::default(); + if let Ok(contents) = fs::read_to_string(file_path) { + let doc = contents.parse::().unwrap(); + if let Some(array_of_tables) = doc + .as_table() + .get("com") + .and_then(|item| item.as_table()?.get("heroku")) + .and_then(|item| item.as_table()?.get("buildpacks")) + .and_then(|item| item.as_table()?.get("deb-packages")) + .and_then(|item| item.as_table()?.get("install")) + .and_then(|item| item.as_array()) + { + for table in array_of_tables.iter() { + if let Some(env_table) = table + .as_inline_table() + .and_then(|t| t.get("env")) + .and_then(|e| e.as_inline_table()) + { + for (key, value) in env_table.iter() { + if let Some(value_str) = value.as_str() { + let value_with_install_dir = value_str.replace("{install_dir}", install_dir); + env.variables.insert(key.to_string(), value_with_install_dir); + } + } + } + } + } + } + env + } + + /// Apply environment variables to the current process. + // pub(crate) fn apply(&self) { + // for (key, value) in &self.variables { + // std::env::set_var(key, value); + // } + // } + + /// Get environment variables as a `HashMap`. + pub(crate) fn get_variables(&self) -> &HashMap { + &self.variables + } + +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + #[test] + fn test_load_from_toml() { + let toml_content = r#" + schema-version = "0.2" + + [com.heroku.buildpacks.deb-packages] + install = [ + { name = "git", env = { "GIT_EXEC_PATH" = "{install_dir}/usr/lib/git-core", "GIT_TEMPLATE_DIR" = "{install_dir}/usr/lib/git-core/templates" } }, + { name = "babeld" }, + { name = "ghostscript", skip_dependencies = true, force = true, env = { "GS_LIB" = "{install_dir}/var/lib/ghostscript", "GS_FONTPATH" = "{install_dir}/var/lib/ghostscript/fonts" } }, + ] + "#; + + let dir = tempdir().unwrap(); + let file_path = dir.path().join("project.toml"); + let mut file = File::create(&file_path).unwrap(); + file.write_all(toml_content.as_bytes()).unwrap(); + + let env = Environment::load_from_toml(&file_path, "/build"); + let variables = env.get_variables(); + + // Print the values of the variables + // println!("GIT_EXEC_PATH: {:?}", variables.get("GIT_EXEC_PATH")); + // println!("GIT_TEMPLATE_DIR: {:?}", variables.get("GIT_TEMPLATE_DIR")); + // println!("GS_LIB: {:?}", variables.get("GS_LIB")); + // println!("GS_FONTPATH: {:?}", variables.get("GS_FONTPATH")); + + assert_eq!(variables.get("GIT_EXEC_PATH"), Some(&"/build/usr/lib/git-core".to_string())); + assert_eq!(variables.get("GIT_TEMPLATE_DIR"), Some(&"/build/usr/lib/git-core/templates".to_string())); + assert_eq!(variables.get("GS_LIB"), Some(&"/build/var/lib/ghostscript".to_string())); + assert_eq!(variables.get("GS_FONTPATH"), Some(&"/build/var/lib/ghostscript/fonts".to_string())); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index 2eaa2a9..e1ab6b3 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,3 +3,4 @@ pub(crate) use requested_package::*; mod buildpack_config; mod requested_package; +pub(crate) mod environment; diff --git a/src/debian/multiarch_name.rs b/src/debian/multiarch_name.rs index b52244c..7856a57 100644 --- a/src/debian/multiarch_name.rs +++ b/src/debian/multiarch_name.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter}; use crate::debian::ArchitectureName; +use std::str::FromStr; #[derive(Debug, PartialEq, Clone)] #[allow(non_camel_case_types)] @@ -28,9 +29,32 @@ impl Display for MultiarchName { } } +impl FromStr for MultiarchName { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "x86_64-linux-gnu" => Ok(MultiarchName::X86_64_LINUX_GNU), + "aarch64-linux-gnu" => Ok(MultiarchName::AARCH_64_LINUX_GNU), + _ => Err(()), + } + } +} + #[cfg(test)] mod tests { use super::*; + use std::str::FromStr; + + #[test] + fn test_multiarch_name_from_str() { + // Test valid strings + assert_eq!(MultiarchName::from_str("x86_64-linux-gnu").unwrap(), MultiarchName::X86_64_LINUX_GNU); + assert_eq!(MultiarchName::from_str("aarch64-linux-gnu").unwrap(), MultiarchName::AARCH_64_LINUX_GNU); + + // Test invalid string + assert!(MultiarchName::from_str("invalid-arch").is_err()); + } #[test] fn converting_architecture_name_to_multiarch_name() { diff --git a/src/install_packages.rs b/src/install_packages.rs index 70c4b0d..898ee89 100644 --- a/src/install_packages.rs +++ b/src/install_packages.rs @@ -32,6 +32,7 @@ use tokio_util::compat::FuturesAsyncReadCompatExt; use tokio_util::io::InspectReader; use walkdir::{DirEntry, WalkDir}; +use crate::config::environment::Environment; use crate::debian::{Distro, MultiarchName, RepositoryPackage}; use crate::{ is_buildpack_debug_logging_enabled, BuildpackResult, DebianPackagesBuildpack, @@ -134,6 +135,7 @@ pub(crate) async fn install_packages( } } + // Configure the environment variables for the installed layer let layer_env = configure_layer_environment( &install_layer.path(), &MultiarchName::from(&distro.architecture), @@ -323,8 +325,16 @@ fn configure_layer_environment(install_path: &Path, multiarch_name: &MultiarchNa ]; prepend_to_env_var(&mut layer_env, "PATH", &bin_paths); - // support multi-arch and legacy filesystem layouts for debian packages - // https://wiki.ubuntu.com/MultiarchSpec + // Load and apply environment variables from the project.toml file + let project_toml_path = install_path.join("project.toml"); + if project_toml_path.exists() { + let env = Environment::load_from_toml(&project_toml_path, &install_path.to_string_lossy()); + for (key, value) in env.get_variables() { + prepend_to_env_var(&mut layer_env, key, vec![value.clone()]); + } + } + + // Support multi-arch and legacy filesystem layouts for debian packages let library_paths = [ install_path.join(format!("usr/lib/{multiarch_name}")), install_path.join("usr/lib"), diff --git a/tests/fixtures/unit_tests/project.toml b/tests/fixtures/unit_tests/project.toml new file mode 100644 index 0000000..4f7c506 --- /dev/null +++ b/tests/fixtures/unit_tests/project.toml @@ -0,0 +1,16 @@ +[_] +schema-version = "0.2" + +[com.heroku.buildpacks.deb-packages] +install = [ + { name = "git", + env = { "GIT_EXEC_PATH" = "{install_dir}/usr/lib/git-core", + "GIT_TEMPLATE_DIR" = "{install_dir}/usr/share/git-core/templates" } + }, + { name = "babeld" }, + { name = "ghostscript", + skip_dependencies = true, + force = true, + env = { "GS_LIB" = "{install_dir}/var/lib/ghostscript" } + }, +] \ No newline at end of file diff --git a/tests/integration_test.rs b/tests/integration_test.rs index e16e345..e9e0f21 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -620,3 +620,47 @@ fn update_project_toml(app_dir: &Path, update_fn: impl FnOnce(&mut DocumentMut)) update_fn(&mut doc); std::fs::write(&project_toml, doc.to_string()).unwrap(); } + +#[test] +fn test_get_integration_test_builder() { + // Set environment variable + std::env::set_var("INTEGRATION_TEST_CNB_BUILDER", "heroku/builder:24"); + assert_eq!(get_integration_test_builder(), "heroku/builder:24"); + + // Unset environment variable + std::env::remove_var("INTEGRATION_TEST_CNB_BUILDER"); + assert_eq!(get_integration_test_builder(), DEFAULT_BUILDER); +} + +#[test] +fn test_get_integration_test_arch() { + // Set environment variable + std::env::set_var("INTEGRATION_TEST_CNB_ARCH", "arm64"); + assert_eq!(get_integration_test_arch(), "arm64"); + + // Unset environment variable + std::env::remove_var("INTEGRATION_TEST_CNB_ARCH"); + assert_eq!(get_integration_test_arch(), DEFAULT_ARCH); +} + +#[test] +fn test_panic_unsupported_test_configuration() { + // This test should panic + let result = std::panic::catch_unwind(|| { + panic_unsupported_test_configuration(); + }); + assert!(result.is_err()); +} + +#[test] +fn test_set_install_config() { + let temp_dir = tempfile::tempdir().unwrap(); + let app_dir = temp_dir.path(); + std::fs::write(app_dir.join("project.toml"), "[com.heroku.buildpacks.deb-packages]").unwrap(); + + set_install_config(app_dir, [requested_package_config("ffmpeg", true)]); + + let contents = std::fs::read_to_string(app_dir.join("project.toml")).unwrap(); + assert!(contents.contains("ffmpeg")); + assert!(contents.contains("skip_dependencies = true")); +} \ No newline at end of file