Skip to content

Commit

Permalink
Added a FromStr implementation to debian::MultiarchName
Browse files Browse the repository at this point in the history
Implment functionality to load the environment variables associate with packages from the project.toml file
  • Loading branch information
tlhmerry0098 committed Dec 30, 2024
1 parent 8949e5d commit 2db46d1
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 8 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
},
]
```

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -199,6 +213,7 @@ For each package added after [determining the packages to install](#step-2-deter
| `INCLUDE_PATH` | `/<layer_dir>/usr/include/<arch>` <br> `/<layer_dir>/usr/include` | header files |
| `CPATH` | Same as `INCLUDE_PATH` | header files |
| `CPPPATH` | Same as `INCLUDE_PATH` | header files |
| `PKG_CONFIG_PATH` | `/<layer_dir>/usr/lib/<arch>/pkgconfig` <br> `/<layer_dir>/usr/lib/pkgconfig` | pc files |
## Contributing
Expand Down
16 changes: 16 additions & 0 deletions project.toml
Original file line number Diff line number Diff line change
@@ -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" }
},
]
98 changes: 98 additions & 0 deletions src/config/environment.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
}

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::<DocumentMut>().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<String, String> {
&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()));
}
}
1 change: 1 addition & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub(crate) use requested_package::*;

mod buildpack_config;
mod requested_package;
pub(crate) mod environment;
24 changes: 24 additions & 0 deletions src/debian/multiarch_name.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -28,9 +29,32 @@ impl Display for MultiarchName {
}
}

impl FromStr for MultiarchName {
type Err = ();

fn from_str(s: &str) -> Result<Self, Self::Err> {
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() {
Expand Down
14 changes: 12 additions & 2 deletions src/install_packages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand Down
16 changes: 16 additions & 0 deletions tests/fixtures/unit_tests/project.toml
Original file line number Diff line number Diff line change
@@ -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" }
},
]
44 changes: 44 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}

0 comments on commit 2db46d1

Please sign in to comment.