From ec0777ded29f5b194dddabe02f75d54e3ba56bd2 Mon Sep 17 00:00:00 2001 From: Colin Casey Date: Wed, 21 Feb 2024 15:30:01 -0400 Subject: [PATCH] Installed packages layer --- Cargo.lock | 2 + Cargo.toml | 2 + buildpack.toml | 4 + src/aptfile.rs | 68 +------------- src/debian.rs | 147 +++++++++++++++++++++++++++++ src/errors.rs | 2 + src/layers/environment.rs | 141 ++++++++++++++++++++++++++++ src/layers/installed_packages.rs | 154 +++++++++++++++++++++++++++++++ src/layers/mod.rs | 2 + src/main.rs | 42 ++++++++- tests/fixtures/basic/Aptfile | 1 + tests/integration_test.rs | 115 ++++++++++++++++++++++- 12 files changed, 611 insertions(+), 69 deletions(-) create mode 100644 src/debian.rs create mode 100644 src/layers/environment.rs create mode 100644 src/layers/installed_packages.rs create mode 100644 src/layers/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0a6dfaa..cc591cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,8 @@ dependencies = [ "indoc", "libcnb 0.19.0", "libcnb-test", + "serde", + "toml", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2a0abca..9723a5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,12 @@ rust-version = "1.76" [dependencies] commons = { git = "https://github.com/heroku/buildpacks-ruby", branch = "main" } libcnb = "=0.19.0" +serde = "1" [dev-dependencies] libcnb-test = "=0.19.0" indoc = "2" +toml = "0.8" [lints.rust] unreachable_pub = "warn" diff --git a/buildpack.toml b/buildpack.toml index 1cf0111..b6f3001 100644 --- a/buildpack.toml +++ b/buildpack.toml @@ -11,5 +11,9 @@ keywords = ["apt"] [[buildpack.licenses]] type = "Apache-2.0" +[[targets]] +os = "linux" +arch = "amd64" + [metadata.release] image = { repository = "docker.io/heroku/buildpacks-apt" } diff --git a/src/aptfile.rs b/src/aptfile.rs index b9e1ad1..c14df2a 100644 --- a/src/aptfile.rs +++ b/src/aptfile.rs @@ -1,7 +1,10 @@ +use crate::debian::{DebianPackageName, ParseDebianPackageNameError}; +use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::str::FromStr; -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct Aptfile { packages: HashSet, } @@ -24,74 +27,11 @@ impl FromStr for Aptfile { #[derive(Debug, PartialEq)] pub(crate) struct ParseAptfileError(ParseDebianPackageNameError); -#[derive(Debug, Eq, PartialEq, Hash)] -pub(crate) struct DebianPackageName(String); - -impl FromStr for DebianPackageName { - type Err = ParseDebianPackageNameError; - - fn from_str(value: &str) -> Result { - // https://www.debian.org/doc/debian-policy/ch-controlfields.html#source - // Package names (both source and binary, see Package) must consist only of - // lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, - // and periods (.). They must be at least two characters long and must - // start with an alphanumeric character. - let is_valid_package_name = value - .chars() - .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '+' | '-' | '.')) - && value.chars().count() >= 2 - && value.starts_with(|c: char| c.is_ascii_alphanumeric()); - - if is_valid_package_name { - Ok(DebianPackageName(value.to_string())) - } else { - Err(ParseDebianPackageNameError(value.to_string())) - } - } -} - -#[derive(Debug, PartialEq)] -pub(crate) struct ParseDebianPackageNameError(String); - #[cfg(test)] mod tests { use super::*; use indoc::indoc; - #[test] - fn parse_valid_debian_package_name() { - let valid_names = [ - "a0", // min length, starting with number - "0a", // min length, starting with letter - "g++", // alphanumeric to start followed by non-alphanumeric characters - "libevent-2.1-6", // just a mix of allowed characters - "a0+.-", // all the allowed characters - ]; - for valid_name in valid_names { - assert_eq!( - DebianPackageName::from_str(valid_name).unwrap(), - DebianPackageName(valid_name.to_string()) - ); - } - } - - #[test] - fn parse_invalid_debian_package_name() { - let invalid_names = [ - "a", // too short - "+a", // can't start with non-alphanumeric character - "ab_c", // can't contain invalid characters - "aBc", // uppercase is not allowed - "package=1.2.3-1", // versioning is not allowed, package name only - ]; - for invalid_name in invalid_names { - assert_eq!( - DebianPackageName::from_str(invalid_name).unwrap_err(), - ParseDebianPackageNameError(invalid_name.to_string()) - ); - } - } - #[test] fn parse_aptfile() { let aptfile = Aptfile::from_str(indoc! { " diff --git a/src/debian.rs b/src/debian.rs new file mode 100644 index 0000000..8a41684 --- /dev/null +++ b/src/debian.rs @@ -0,0 +1,147 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +// https://www.debian.org/doc/debian-policy/ch-controlfields.html#source +pub(crate) struct DebianPackageName(pub(crate) String); + +impl FromStr for DebianPackageName { + type Err = ParseDebianPackageNameError; + + fn from_str(value: &str) -> Result { + // Package names (both source and binary, see Package) must consist only of + // lower case letters (a-z), digits (0-9), plus (+) and minus (-) signs, + // and periods (.). They must be at least two characters long and must + // start with an alphanumeric character. + let is_valid_package_name = value + .chars() + .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '+' | '-' | '.')) + && value.chars().count() >= 2 + && value.starts_with(|c: char| c.is_ascii_alphanumeric()); + + if is_valid_package_name { + Ok(DebianPackageName(value.to_string())) + } else { + Err(ParseDebianPackageNameError(value.to_string())) + } + } +} + +#[derive(Debug, PartialEq)] +pub(crate) struct ParseDebianPackageNameError(pub(crate) String); + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +// https://wiki.debian.org/Multiarch/Tuples +pub(crate) enum DebianArchitectureName { + AMD_64, +} + +impl FromStr for DebianArchitectureName { + type Err = ParseDebianArchitectureNameError; + + fn from_str(value: &str) -> Result { + match value { + "amd64" => Ok(DebianArchitectureName::AMD_64), + _ => Err(ParseDebianArchitectureNameError(value.to_string())), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseDebianArchitectureNameError(String); + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +// https://wiki.debian.org/Multiarch/Tuples +pub(crate) enum DebianMultiarchName { + X86_64_LINUX_GNU, +} + +impl From<&DebianArchitectureName> for DebianMultiarchName { + fn from(value: &DebianArchitectureName) -> Self { + match value { + DebianArchitectureName::AMD_64 => DebianMultiarchName::X86_64_LINUX_GNU, + } + } +} + +impl Display for DebianMultiarchName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + DebianMultiarchName::X86_64_LINUX_GNU => write!(f, "x86_64-linux-gnu"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_valid_debian_package_name() { + let valid_names = [ + "a0", // min length, starting with number + "0a", // min length, starting with letter + "g++", // alphanumeric to start followed by non-alphanumeric characters + "libevent-2.1-6", // just a mix of allowed characters + "a0+.-", // all the allowed characters + ]; + for valid_name in valid_names { + assert_eq!( + DebianPackageName::from_str(valid_name).unwrap(), + DebianPackageName(valid_name.to_string()) + ); + } + } + + #[test] + fn parse_invalid_debian_package_name() { + let invalid_names = [ + "a", // too short + "+a", // can't start with non-alphanumeric character + "ab_c", // can't contain invalid characters + "aBc", // uppercase is not allowed + "package=1.2.3-1", // versioning is not allowed, package name only + ]; + for invalid_name in invalid_names { + assert_eq!( + DebianPackageName::from_str(invalid_name).unwrap_err(), + ParseDebianPackageNameError(invalid_name.to_string()) + ); + } + } + + #[test] + fn parse_value_debian_architecture_name() { + assert_eq!( + DebianArchitectureName::AMD_64, + DebianArchitectureName::from_str("amd64").unwrap() + ); + } + + #[test] + fn parse_invalid_debian_architecture_name() { + match DebianArchitectureName::from_str("???").unwrap_err() { + ParseDebianArchitectureNameError(value) => assert_eq!(value, "???"), + } + } + + #[test] + fn converting_debian_architecture_name_to_multiarch_name() { + assert_eq!( + DebianMultiarchName::from(&DebianArchitectureName::AMD_64), + DebianMultiarchName::X86_64_LINUX_GNU + ); + } + + #[test] + fn display_debian_to_multiarch_name() { + assert_eq!( + DebianMultiarchName::X86_64_LINUX_GNU.to_string(), + "x86_64-linux-gnu" + ); + } +} diff --git a/src/errors.rs b/src/errors.rs index 037f49d..0477960 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,4 +1,5 @@ use crate::aptfile::ParseAptfileError; +use crate::debian::ParseDebianArchitectureNameError; #[derive(Debug)] #[allow(clippy::enum_variant_names)] @@ -6,6 +7,7 @@ pub(crate) enum AptBuildpackError { DetectAptfile(std::io::Error), ReadAptfile(std::io::Error), ParseAptfile(ParseAptfileError), + ParseDebianArchitectureName(ParseDebianArchitectureNameError), } impl From for libcnb::Error { diff --git a/src/layers/environment.rs b/src/layers/environment.rs new file mode 100644 index 0000000..d53f93f --- /dev/null +++ b/src/layers/environment.rs @@ -0,0 +1,141 @@ +use crate::debian::{DebianArchitectureName, DebianMultiarchName}; +use crate::AptBuildpack; +use commons::output::build_log::SectionLogger; +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 libcnb::Buildpack; +use std::ffi::OsString; +use std::path::Path; + +pub(crate) struct EnvironmentLayer<'a> { + pub(crate) debian_architecture_name: &'a DebianArchitectureName, + pub(crate) installed_packages_dir: &'a Path, + pub(crate) _section_logger: &'a dyn SectionLogger, +} + +impl<'a> Layer for EnvironmentLayer<'a> { + type Buildpack = AptBuildpack; + type Metadata = GenericMetadata; + + fn types(&self) -> LayerTypes { + LayerTypes { + build: true, + launch: true, + cache: false, + } + } + + fn create( + &mut self, + _context: &BuildContext, + _layer_path: &Path, + ) -> Result, ::Error> { + LayerResultBuilder::new(GenericMetadata::default()) + .env(configure_environment( + self.installed_packages_dir, + &DebianMultiarchName::from(self.debian_architecture_name), + )) + .build() + } +} + +fn configure_environment( + packages_dir: &Path, + debian_multiarch_name: &DebianMultiarchName, +) -> LayerEnv { + let mut env = LayerEnv::new(); + + let bin_paths = [ + packages_dir.join("bin"), + packages_dir.join("usr/bin"), + packages_dir.join("usr/sbin"), + ]; + prepend_to_env_var(&mut env, "PATH", &bin_paths); + + // support multi-arch and legacy filesystem layouts for debian packages + // https://wiki.ubuntu.com/MultiarchSpec + let library_paths = [ + packages_dir.join(format!("usr/lib/{debian_multiarch_name}")), + packages_dir.join("usr/lib"), + packages_dir.join(format!("lib/{debian_multiarch_name}")), + packages_dir.join("lib"), + ]; + prepend_to_env_var(&mut env, "LD_LIBRARY_PATH", &library_paths); + prepend_to_env_var(&mut env, "LIBRARY_PATH", &library_paths); + + let include_paths = [ + packages_dir.join(format!("usr/include/{debian_multiarch_name}")), + packages_dir.join("usr/include"), + ]; + prepend_to_env_var(&mut env, "INCLUDE_PATH", &include_paths); + prepend_to_env_var(&mut env, "CPATH", &include_paths); + prepend_to_env_var(&mut env, "CPPPATH", &include_paths); + + let pkg_config_paths = [ + packages_dir.join(format!("usr/lib/{debian_multiarch_name}/pkgconfig")), + packages_dir.join("usr/lib/pkgconfig"), + ]; + prepend_to_env_var(&mut env, "PKG_CONFIG_PATH", &pkg_config_paths); + + env +} + +fn prepend_to_env_var(env: &mut LayerEnv, name: &str, paths: I) +where + I: IntoIterator, + T: Into, +{ + let separator = ":"; + env.insert(Scope::All, ModificationBehavior::Delimiter, name, separator); + env.insert( + Scope::All, + ModificationBehavior::Prepend, + name, + paths + .into_iter() + .map(Into::into) + .collect::>() + .join(separator.as_ref()), + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + #[test] + fn test_configure_environment() { + let debian_multiarch_name = DebianMultiarchName::X86_64_LINUX_GNU; + let layer_env = configure_environment(&PathBuf::from("/"), &debian_multiarch_name); + let env = layer_env.apply_to_empty(Scope::All); + assert_eq!(env.get("PATH").unwrap(), "/bin:/usr/bin:/usr/sbin"); + assert_eq!( + env.get("LD_LIBRARY_PATH").unwrap(), + "/usr/lib/x86_64-linux-gnu:/usr/lib:/lib/x86_64-linux-gnu:/lib" + ); + assert_eq!( + env.get("LIBRARY_PATH").unwrap(), + "/usr/lib/x86_64-linux-gnu:/usr/lib:/lib/x86_64-linux-gnu:/lib" + ); + assert_eq!( + env.get("INCLUDE_PATH").unwrap(), + "/usr/include/x86_64-linux-gnu:/usr/include" + ); + assert_eq!( + env.get("CPATH").unwrap(), + "/usr/include/x86_64-linux-gnu:/usr/include" + ); + assert_eq!( + env.get("CPPPATH").unwrap(), + "/usr/include/x86_64-linux-gnu:/usr/include" + ); + assert_eq!( + env.get("PKG_CONFIG_PATH").unwrap(), + "/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig" + ); + } +} diff --git a/src/layers/installed_packages.rs b/src/layers/installed_packages.rs new file mode 100644 index 0000000..d818607 --- /dev/null +++ b/src/layers/installed_packages.rs @@ -0,0 +1,154 @@ +use crate::aptfile::Aptfile; +use crate::AptBuildpack; +use commons::output::interface::SectionLogger; +use commons::output::section_log::log_step; +use libcnb::build::BuildContext; +use libcnb::data::layer_content_metadata::LayerTypes; +use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder}; +use libcnb::Buildpack; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +pub(crate) struct InstalledPackagesLayer<'a> { + pub(crate) aptfile: &'a Aptfile, + pub(crate) _section_logger: &'a dyn SectionLogger, +} + +impl<'a> Layer for InstalledPackagesLayer<'a> { + type Buildpack = AptBuildpack; + type Metadata = InstalledPackagesMetadata; + + fn types(&self) -> LayerTypes { + LayerTypes { + build: true, + launch: true, + cache: true, + } + } + + fn create( + &mut self, + context: &BuildContext, + _layer_path: &Path, + ) -> Result, ::Error> { + log_step("Installing packages from Aptfile"); + + LayerResultBuilder::new(InstalledPackagesMetadata::new( + self.aptfile.clone(), + context.target.os.clone(), + context.target.arch.clone(), + )) + .build() + } + + fn existing_layer_strategy( + &mut self, + context: &BuildContext, + layer_data: &LayerData, + ) -> Result::Error> { + let old_meta = &layer_data.content_metadata.metadata; + let new_meta = &InstalledPackagesMetadata::new( + self.aptfile.clone(), + context.target.os.clone(), + context.target.arch.clone(), + ); + if old_meta == new_meta { + log_step("Skipping installation, packages already in cache"); + Ok(ExistingLayerStrategy::Keep) + } else { + log_step(format!( + "Invalidating installed packages ({} changed)", + new_meta.changed_fields(old_meta).join(", ") + )); + Ok(ExistingLayerStrategy::Recreate) + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct InstalledPackagesMetadata { + arch: String, + aptfile: Aptfile, + os: String, +} + +impl InstalledPackagesMetadata { + pub(crate) fn new(aptfile: Aptfile, os: String, arch: String) -> Self { + Self { arch, aptfile, os } + } + + pub(crate) fn changed_fields(&self, other: &InstalledPackagesMetadata) -> Vec { + let mut changed_fields = vec![]; + if self.os != other.os { + changed_fields.push("os".to_string()); + } + if self.arch != other.arch { + changed_fields.push("arch".to_string()); + } + if self.aptfile != other.aptfile { + changed_fields.push("Aptfile".to_string()); + } + changed_fields.sort(); + changed_fields + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn installed_packages_metadata_with_all_changed_fields() { + assert_eq!( + InstalledPackagesMetadata::new( + Aptfile::from_str("package-1").unwrap(), + "linux".to_string(), + "amd64".to_string(), + ) + .changed_fields(&InstalledPackagesMetadata::new( + Aptfile::from_str("package-2").unwrap(), + "windows".to_string(), + "arm64".to_string(), + )), + &["Aptfile", "arch", "os"] + ); + } + + #[test] + fn installed_packages_metadata_with_no_changed_fields() { + assert!(InstalledPackagesMetadata::new( + Aptfile::from_str("package-1").unwrap(), + "linux".to_string(), + "amd64".to_string(), + ) + .changed_fields(&InstalledPackagesMetadata::new( + Aptfile::from_str("package-1").unwrap(), + "linux".to_string(), + "amd64".to_string(), + )) + .is_empty()); + } + + #[test] + fn test_metadata_guard() { + let metadata = InstalledPackagesMetadata::new( + Aptfile::from_str("package-1").unwrap(), + "linux".to_string(), + "amd64".to_string(), + ); + let actual = toml::to_string(&metadata).unwrap(); + let expected = r#" +arch = "amd64" +os = "linux" + +[aptfile] +packages = ["package-1"] +"# + .trim(); + assert_eq!(expected, actual.trim()); + let from_toml: InstalledPackagesMetadata = toml::from_str(&actual).unwrap(); + assert_eq!(metadata, from_toml); + } +} diff --git a/src/layers/mod.rs b/src/layers/mod.rs new file mode 100644 index 0000000..e23589b --- /dev/null +++ b/src/layers/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod environment; +pub(crate) mod installed_packages; diff --git a/src/main.rs b/src/main.rs index 77bd2b2..6e54cd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,29 @@ use crate::aptfile::Aptfile; +use crate::debian::DebianArchitectureName; use crate::errors::AptBuildpackError; +use crate::layers::environment::EnvironmentLayer; +use crate::layers::installed_packages::InstalledPackagesLayer; use commons::output::build_log::{BuildLog, Logger}; use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; +use libcnb::data::layer_name; use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; use libcnb::generic::{GenericMetadata, GenericPlatform}; use libcnb::{buildpack_main, Buildpack}; -use std::fs; -use std::io::stdout; - #[cfg(test)] use libcnb_test as _; +use std::fs; +use std::io::stdout; +use std::str::FromStr; mod aptfile; +mod debian; mod errors; +mod layers; buildpack_main!(AptBuildpack); +const BUILDPACK_NAME: &str = "Heroku Apt Buildpack"; + const APTFILE_PATH: &str = "Aptfile"; struct AptBuildpack; @@ -44,11 +52,37 @@ impl Buildpack for AptBuildpack { } fn build(&self, context: BuildContext) -> libcnb::Result { - let _aptfile: Aptfile = fs::read_to_string(context.app_dir.join(APTFILE_PATH)) + let logger = BuildLog::new(stdout()).buildpack_name(BUILDPACK_NAME); + + let aptfile: Aptfile = fs::read_to_string(context.app_dir.join(APTFILE_PATH)) .map_err(AptBuildpackError::ReadAptfile)? .parse() .map_err(AptBuildpackError::ParseAptfile)?; + let debian_architecture_name = DebianArchitectureName::from_str(&context.target.arch) + .map_err(AptBuildpackError::ParseDebianArchitectureName)?; + + let section = logger.section("Apt packages"); + + let installed_packages_layer_data = context.handle_layer( + layer_name!("installed_packages"), + InstalledPackagesLayer { + aptfile: &aptfile, + _section_logger: section.as_ref(), + }, + )?; + + context.handle_layer( + layer_name!("environment"), + EnvironmentLayer { + debian_architecture_name: &debian_architecture_name, + installed_packages_dir: &installed_packages_layer_data.path, + _section_logger: section.as_ref(), + }, + )?; + + section.end_section().finish_logging(); + BuildResultBuilder::new().build() } } diff --git a/tests/fixtures/basic/Aptfile b/tests/fixtures/basic/Aptfile index e69de29..d92220e 100644 --- a/tests/fixtures/basic/Aptfile +++ b/tests/fixtures/basic/Aptfile @@ -0,0 +1 @@ +xmlsec1 diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 016c8ec..2d0a2e9 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -4,7 +4,9 @@ // Required due to: https://github.com/rust-lang/rust/issues/95513 #![allow(unused_crate_dependencies)] -use libcnb_test::{assert_contains, BuildConfig, PackResult, TestRunner}; +use libcnb_test::{ + assert_contains, assert_not_contains, BuildConfig, PackResult, TestContext, TestRunner, +}; #[test] #[ignore = "integration test"] @@ -28,8 +30,119 @@ fn test_failed_detection() { ); } +#[test] +#[ignore = "integration test"] +fn test_cache_restored() { + TestRunner::default().build( + BuildConfig::new(get_integration_test_builder(), "tests/fixtures/basic"), + |ctx| { + assert_contains!(ctx.pack_stdout, "# Heroku Apt Buildpack"); + assert_contains!(ctx.pack_stdout, "- Apt packages"); + assert_contains!(ctx.pack_stdout, " - Installing packages from Aptfile"); + + let config = ctx.config.clone(); + ctx.rebuild(config, |ctx| { + assert_contains!(ctx.pack_stdout, "- Apt packages"); + assert_contains!( + ctx.pack_stdout, + " - Skipping installation, packages already in cache" + ); + + assert_not_contains!(ctx.pack_stdout, " - Installing packages from Aptfile"); + }); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn test_cache_invalidated_when_aptfile_changes() { + TestRunner::default().build( + BuildConfig::new(get_integration_test_builder(), "tests/fixtures/basic"), + |ctx| { + assert_contains!(ctx.pack_stdout, "# Heroku Apt Buildpack"); + assert_contains!(ctx.pack_stdout, "- Apt packages"); + assert_contains!(ctx.pack_stdout, " - Installing packages from Aptfile"); + + let mut config = ctx.config.clone(); + config.app_dir_preprocessor(|app_dir| { + std::fs::write(app_dir.join("Aptfile"), "# empty\n").unwrap(); + }); + ctx.rebuild(config, |ctx| { + assert_contains!(ctx.pack_stdout, "- Apt packages"); + assert_contains!( + ctx.pack_stdout, + " - Invalidating installed packages (Aptfile changed)" + ); + assert_contains!(ctx.pack_stdout, " - Installing packages from Aptfile"); + + assert_not_contains!( + ctx.pack_stdout, + " - Skipping installation, packages already in cache" + ); + }); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn test_environment_configuration() { + TestRunner::default().build( + BuildConfig::new(get_integration_test_builder(), "tests/fixtures/basic"), + |ctx| { + let layer_dir = "/layers/heroku_apt/installed_packages"; + + let path = get_env_var(&ctx, "PATH"); + assert_contains!(path, &format!("{layer_dir}/bin")); + assert_contains!(path, &format!("{layer_dir}/usr/bin")); + assert_contains!(path, &format!("{layer_dir}/usr/sbin")); + + let ld_library_path = get_env_var(&ctx, "LD_LIBRARY_PATH"); + assert_contains!( + ld_library_path, + &format!("{layer_dir}/usr/lib/x86_64-linux-gnu") + ); + assert_contains!(ld_library_path, &format!("{layer_dir}/usr/lib")); + assert_contains!( + ld_library_path, + &format!("{layer_dir}/lib/x86_64-linux-gnu") + ); + assert_contains!(ld_library_path, &format!("{layer_dir}/lib")); + + let library_path = get_env_var(&ctx, "LIBRARY_PATH"); + assert_eq!(ld_library_path, library_path); + + let include_path = get_env_var(&ctx, "INCLUDE_PATH"); + assert_contains!( + include_path, + &format!("{layer_dir}/usr/include/x86_64-linux-gnu") + ); + assert_contains!(include_path, &format!("{layer_dir}/usr/include")); + + let cpath = get_env_var(&ctx, "CPATH"); + assert_eq!(include_path, cpath); + + let cpp_path = get_env_var(&ctx, "CPPPATH"); + assert_eq!(include_path, cpp_path); + + let pkg_config_path = get_env_var(&ctx, "PKG_CONFIG_PATH"); + assert_contains!( + pkg_config_path, + &format!("{layer_dir}/usr/lib/x86_64-linux-gnu/pkgconfig") + ); + assert_contains!(pkg_config_path, &format!("{layer_dir}/usr/lib/pkgconfig")); + }, + ); +} + const DEFAULT_BUILDER: &str = "heroku/builder:22"; fn get_integration_test_builder() -> String { std::env::var("INTEGRATION_TEST_CNB_BUILDER").unwrap_or(DEFAULT_BUILDER.to_string()) } + +fn get_env_var(ctx: &TestContext, env_var_name: &str) -> String { + ctx.run_shell_command(format!("echo -n ${env_var_name}")) + .stdout +}