diff --git a/Cargo.lock b/Cargo.lock index fca41de..cf991f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,6 +347,7 @@ dependencies = [ "tokio", "tokio-tar", "tokio-util", + "toml 0.9.4", "toml_edit 0.23.2", "tracing", "walkdir", diff --git a/Cargo.toml b/Cargo.toml index e5b8ca5..47d8d6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ tokio = { version = "1", default-features = false, features = ["rt-multi-thread" tokio-tar = "0.3" tokio-util = { version = "0.7", default-features = false, features = ["compat", "io"] } toml_edit = "0.23" +toml = "0.9" tracing = "0.1" walkdir = "2" diff --git a/src/config/buildpack_config.rs b/src/config/buildpack_config.rs index a611cf2..6e5bde2 100644 --- a/src/config/buildpack_config.rs +++ b/src/config/buildpack_config.rs @@ -1,10 +1,12 @@ use crate::config::custom_source::{CustomSource, ParseCustomSourceError}; use crate::config::{ParseRequestedPackageError, RequestedPackage}; -use crate::DebianPackagesBuildpackError; +use crate::{DebianPackagesBuildpack, DebianPackagesBuildpackError}; use indexmap::IndexSet; +use libcnb::build::BuildContext; use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; +use toml::Table; use toml_edit::{DocumentMut, TableLike}; pub(crate) const NAMESPACED_CONFIG: &str = "com.heroku.buildpacks.deb-packages"; @@ -25,6 +27,43 @@ impl BuildpackConfig { Err(e) => Err(e), } } + + pub(crate) fn merge(configs: I) -> BuildpackConfig + where + I: IntoIterator, + { + let mut merged_config = BuildpackConfig::default(); + for config in configs { + let BuildpackConfig { install, sources } = config; + merged_config.install.extend(install); + merged_config.sources.extend(sources); + } + merged_config + } +} + +impl TryFrom<&BuildContext> for BuildpackConfig { + type Error = ConfigError; + + fn try_from(value: &BuildContext) -> Result { + let buildpack_id = &value.buildpack_descriptor.buildpack.id; + let buildpack_configs = value + .buildpack_plan + .entries + .iter() + .filter_map(|entry| { + if entry.name == buildpack_id.to_string() { + Some( + BuildpackConfig::try_from(&entry.metadata) + .map_err(ConfigError::ParseBuildplanConfig), + ) + } else { + None + } + }) + .collect::, _>>()?; + Ok(BuildpackConfig::merge(buildpack_configs)) + } } impl TryFrom for BuildpackConfig { @@ -46,6 +85,20 @@ impl FromStr for BuildpackConfig { } } +impl TryFrom<&Table> for BuildpackConfig { + type Error = ParseConfigError; + + fn try_from(value: &Table) -> Result { + let toml_contents = toml::to_string(value).expect("toml should be serializable"); + let doc = parse_config_toml(&toml_contents)?; + let table = doc + .as_item() + .as_table_like() + .expect("toml doc should be table-like"); + BuildpackConfig::try_from(table) + } +} + impl TryFrom<&dyn TableLike> for BuildpackConfig { type Error = ParseConfigError; @@ -79,9 +132,11 @@ impl TryFrom<&dyn TableLike> for BuildpackConfig { } #[derive(Debug)] +#[allow(clippy::enum_variant_names)] pub(crate) enum ConfigError { ReadConfig(PathBuf, std::io::Error), ParseConfig(PathBuf, ParseConfigError), + ParseBuildplanConfig(ParseConfigError), } #[derive(Debug)] @@ -199,7 +254,7 @@ iGa6i2oLaGzGaQZDpdqyQZiYpQEYw9xN+8g= arch: vec![AMD_64, ARM_64], signed_by: indoc! { " -----BEGIN PGP PUBLIC KEY BLOCK----- - + NxRt3Z+7w5HMIN2laKp+ItxloPWGBdcHU4o2ZnWgsVT8Y/a+RED75DDbAQ6lS3fV sSlmQLExcf75qOPy34XNv3gWP4tbfIXXt8olflF8hwHggmKZzEImnzEozPabDsN7 nkhHZEWhGcPRcuHbFOqcirV1sfsKK1gOsTbxS00iD3OivOFCQqujF196cal/utTd diff --git a/src/errors.rs b/src/errors.rs index 697449e..5a76991 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -241,6 +241,8 @@ fn on_config_error(error: ConfigError) -> ErrorMessage { } } } + + ConfigError::ParseBuildplanConfig(e) => todo!("{e:?}"), } } diff --git a/src/main.rs b/src/main.rs index 48a86e7..64e7529 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use crate::o11y::*; use bullet_stream::{global::print, style}; use indoc::formatdoc; use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; +use libcnb::data::build_plan::BuildPlanBuilder; use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; use libcnb::generic::{GenericMetadata, GenericPlatform}; use libcnb::{buildpack_main, Buildpack, Env}; @@ -49,20 +50,30 @@ impl Buildpack for DebianPackagesBuildpack { type Error = DebianPackagesBuildpackError; fn detect(&self, context: DetectContext) -> libcnb::Result { + let buildpack_id = context.buildpack_descriptor.buildpack.id; + + let buildplan = BuildPlanBuilder::new() + .provides(buildpack_id.to_string()) + .build(); + if let Some(project_toml) = get_project_toml(&context.app_dir)? { info!({ PROJECT_TOML_DETECTED } = true); - if BuildpackConfig::is_present(project_toml)? { - DetectResultBuilder::pass().build() - } else { - print::plain("project.toml found, but no [com.heroku.buildpacks.deb-packages] configuration present."); + if !BuildpackConfig::is_present(project_toml)? { info!({ PROJECT_TOML_NO_CONFIG } = true); - DetectResultBuilder::fail().build() } + DetectResultBuilder::pass().build_plan(buildplan).build() + // if BuildpackConfig::is_present(project_toml)? { + // DetectResultBuilder::pass().build() + // } else { + // print::plain("project.toml found, but no [com.heroku.buildpacks.deb-packages] configuration present."); + // info!({ PROJECT_TOML_NO_CONFIG } = true); + // DetectResultBuilder::fail().build() + // } } else if get_aptfile(&context.app_dir)?.is_some() { // NOTE: This buildpack doesn't use an Aptfile, but we'll pass detection to display a message // to users in the build step detailing how to migrate away from the Aptfile format. info!({ APTFILE_DETECTED } = true); - DetectResultBuilder::pass().build() + DetectResultBuilder::pass().build_plan(buildplan).build() } else { print::plain("No project.toml or Aptfile found."); DetectResultBuilder::fail().build() @@ -115,7 +126,15 @@ impl Buildpack for DebianPackagesBuildpack { } } - let config = BuildpackConfig::try_from(context.app_dir.join("project.toml"))?; + let buildplan_config = BuildpackConfig::try_from(context.as_ref())?; + let user_config = if context.app_dir.join("project.toml").exists() + && BuildpackConfig::is_present(context.app_dir.join("project.toml"))? + { + BuildpackConfig::try_from(context.app_dir.join("project.toml"))? + } else { + BuildpackConfig::default() + }; + let config = BuildpackConfig::merge([buildplan_config, user_config]); if config.install.is_empty() { info!({ EARLY_EXIT_REASON } = "nothing_to_install", "early exit"); diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 827ea9b..27a2717 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -5,10 +5,11 @@ #![allow(unused_crate_dependencies)] #![allow(clippy::unwrap_used)] +use indoc::indoc; +use libcnb_test::{assert_contains, assert_contains_match, assert_not_contains, BuildConfig, BuildpackReference, PackResult, TestContext, TestRunner}; +use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; - -use libcnb_test::{assert_contains, assert_contains_match, assert_not_contains, BuildConfig, BuildpackReference, PackResult, TestContext, TestRunner}; use toml_edit::{value, Array, DocumentMut, InlineTable}; #[test] @@ -581,6 +582,159 @@ fn custom_repository_for_noble_distro() { }); } +#[test] +#[ignore = "integration test"] +fn multiple_configuration_sources() { + if get_integration_test_builder().as_str() != "heroku/builder:24" { + return; + } + integration_test_with_config( + "fixtures/project_file_with_empty_config", + |config| { + config.app_dir_preprocessor(|app_dir| { + set_install_config(&app_dir, [requested_package_config("libvips-tools", false)]); + }); + + config.buildpacks(vec![ + BuildpackReference::CurrentCrate, + custom_buildpack() + .id("test/buildplan_config") + .detect(indoc! { r#" + #!/usr/bin/env bash + build_plan="$2" + + cat <"$build_plan" + [[requires]] + name = "heroku/deb-packages" + + [requires.metadata] + install = ["mongodb-org-tools", "mongodb-org-shell"] + + [[requires.metadata.sources]] + uri = "https://repo.mongodb.org/apt/ubuntu" + suites = ["noble/mongodb-org/8.0"] + components = ["multiverse"] + arch = ["amd64", "arm64"] + signed_by = """-----BEGIN PGP PUBLIC KEY BLOCK----- + + mQINBGWgEhwBEADJpjwR+5n6na3tqZ6ueHsW/U8lvcvMFZ1DYNo+/JhrNjHkZ7HR + Wbc2IzWej1zqTtctSKZvrCkPGZxiDsKB5xta/NVtnpjSuV02Gp0F6hf0gnvark04 + HnEFaV2w15Tyr8Z4KHRDbdja6h/24t4tR0KkRzxh5U7FwLL8BpK2drbTog9FBMy+ + lqYDfOLHx6JDeOMC7eSNe/jJsAiuVcP/y+vQbLuMYAaMPSvJoidRIQ88oFLoUlVZ + NxRt3Z+7w5HMIN2laKp+ItxloPWGBdcHU4o2ZnWgsVT8Y/a+RED75DDbAQ6lS3fV + sSlmQLExcf75qOPy34XNv3gWP4tbfIXXt8olflF8hwHggmKZzEImnzEozPabDsN7 + nkhHZEWhGcPRcuHbFOqcirV1sfsKK1gOsTbxS00iD3OivOFCQqujF196cal/utTd + WvyJvY2o35eE0WFcDdstU7UiP39usE+jk4jbQS5WbMYk9yyZCCbd74T7eYAfSEXg + GqrE1O6pjMmwbEjHwHDkbn/2WGvOSgWKHJVSh8V1K5ijlAd/9SCbsY0Yh5K3G16k + gnzHZ7OuQItfvMlPLQA7P2cPj/bGkO2ayyZU4+9rCsXlHw4Cee+u1APFSO2rj1TE + vX80grtqXNmj6nV21nIiXASvBKRO3kU4t8yV9i8EEREKYx/gLIl5i3PYGwARAQAB + tDdNb25nb0RCIDguMCBSZWxlYXNlIFNpZ25pbmcgS2V5IDxwYWNrYWdpbmdAbW9u + Z29kYi5jb20+iQJOBBMBCgA4FiEESwdSwbyiOMC07hTcQd4Fik59ygUFAmWgEhwC + GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQQd4Fik59ygWy4w//e+IQ5eFT + rlowx196DaInUTiv+aMkkN5hAtJDMicV9+ZDChEfqqQH1WJuUUKfX00AeEDocQnI + LgESy0+rp2FoRPG5bXaJXTv6xQkqIMQQMNMkG4Nx3AxggRVkzd2arOr9FBwcnmf0 + 7xu9EsMJndmzTsDO+ohWnNb0ILSdPVKDafpfg4ycBWDZT7ynD6TT0JpG8WWJi8F+ + 9GR4k4CpBujk49POZbjeVDOuP/o/tosmEO9jo03C/u1qNuVVXy6vvTB6WjO79QTX + OlSTLHAiu9N/VknG1B7lW15X1yl3jl3vZ33N68ncXUW2gAJi7Nh6H6RSm288IC4i + hSmSBFabffQtwOTVE0CaKge2nU4Oc3Tp2h8moEgi81vYT/CioMt6wmHTzY0grcfF + WLwtDMFJ0VQYRrUIOMmYBFyRp2jdRYYkA+vlL+6DNAAjCeuvwCs3PqUhgFvHNxVv + bumKiRMIOoNUwpLEKsEq8jBs+U+gUfa+CmBn67G9mjDRu4cXXrtItooxnbfM/m0i + hVnssTC1arrx273zFepLosPvgrT0TS7tnyXbzuq5mo0zD1fSj4kuSS9V/SSy9fWF + LAtHiNQJkjzGFxu0/9dyQyX6C523uvfdcOzpObTyjBeGKqmEEf0lF5OYLDlkk2Sm + iGa6i2oLaGzGaQZDpdqyQZiYpQEYw9xN+8g= + =J31U + -----END PGP PUBLIC KEY BLOCK----- + """ + EOF + "#}) + .call(), + ]); + }, + |ctx| { + assert_contains!(ctx.pack_stderr, "https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 [multiverse]"); + assert_contains!(ctx.pack_stderr, "Downloaded release file https://repo.mongodb.org/apt/ubuntu/dists/noble/mongodb-org/8.0/InRelease"); + assert_contains_match!(ctx.pack_stderr, r"Downloaded package index https://repo.mongodb.org/apt/ubuntu/dists/noble/mongodb-org/8.0/multiverse/binary-(amd|arm)64/Packages.gz"); + assert_contains!(ctx.pack_stderr, "Adding `mongodb-org-tools"); + assert_contains!(ctx.pack_stderr, "Adding `mongodb-org-shell"); + assert_contains!(ctx.pack_stderr, "Adding `libvips-tools"); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn only_buildplan_configuration_source() { + if get_integration_test_builder().as_str() != "heroku/builder:24" { + return; + } + integration_test_with_config( + "fixtures/project_file_with_no_config", + |config| { + config.buildpacks(vec![ + BuildpackReference::CurrentCrate, + custom_buildpack() + .id("test/buildplan_config") + .detect(indoc! { r#" + #!/usr/bin/env bash + build_plan="$2" + + cat <"$build_plan" + [[requires]] + name = "heroku/deb-packages" + + [requires.metadata] + install = ["mongodb-org-tools", "mongodb-org-shell"] + + [[requires.metadata.sources]] + uri = "https://repo.mongodb.org/apt/ubuntu" + suites = ["noble/mongodb-org/8.0"] + components = ["multiverse"] + arch = ["amd64", "arm64"] + signed_by = """-----BEGIN PGP PUBLIC KEY BLOCK----- + + mQINBGWgEhwBEADJpjwR+5n6na3tqZ6ueHsW/U8lvcvMFZ1DYNo+/JhrNjHkZ7HR + Wbc2IzWej1zqTtctSKZvrCkPGZxiDsKB5xta/NVtnpjSuV02Gp0F6hf0gnvark04 + HnEFaV2w15Tyr8Z4KHRDbdja6h/24t4tR0KkRzxh5U7FwLL8BpK2drbTog9FBMy+ + lqYDfOLHx6JDeOMC7eSNe/jJsAiuVcP/y+vQbLuMYAaMPSvJoidRIQ88oFLoUlVZ + NxRt3Z+7w5HMIN2laKp+ItxloPWGBdcHU4o2ZnWgsVT8Y/a+RED75DDbAQ6lS3fV + sSlmQLExcf75qOPy34XNv3gWP4tbfIXXt8olflF8hwHggmKZzEImnzEozPabDsN7 + nkhHZEWhGcPRcuHbFOqcirV1sfsKK1gOsTbxS00iD3OivOFCQqujF196cal/utTd + WvyJvY2o35eE0WFcDdstU7UiP39usE+jk4jbQS5WbMYk9yyZCCbd74T7eYAfSEXg + GqrE1O6pjMmwbEjHwHDkbn/2WGvOSgWKHJVSh8V1K5ijlAd/9SCbsY0Yh5K3G16k + gnzHZ7OuQItfvMlPLQA7P2cPj/bGkO2ayyZU4+9rCsXlHw4Cee+u1APFSO2rj1TE + vX80grtqXNmj6nV21nIiXASvBKRO3kU4t8yV9i8EEREKYx/gLIl5i3PYGwARAQAB + tDdNb25nb0RCIDguMCBSZWxlYXNlIFNpZ25pbmcgS2V5IDxwYWNrYWdpbmdAbW9u + Z29kYi5jb20+iQJOBBMBCgA4FiEESwdSwbyiOMC07hTcQd4Fik59ygUFAmWgEhwC + GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQQd4Fik59ygWy4w//e+IQ5eFT + rlowx196DaInUTiv+aMkkN5hAtJDMicV9+ZDChEfqqQH1WJuUUKfX00AeEDocQnI + LgESy0+rp2FoRPG5bXaJXTv6xQkqIMQQMNMkG4Nx3AxggRVkzd2arOr9FBwcnmf0 + 7xu9EsMJndmzTsDO+ohWnNb0ILSdPVKDafpfg4ycBWDZT7ynD6TT0JpG8WWJi8F+ + 9GR4k4CpBujk49POZbjeVDOuP/o/tosmEO9jo03C/u1qNuVVXy6vvTB6WjO79QTX + OlSTLHAiu9N/VknG1B7lW15X1yl3jl3vZ33N68ncXUW2gAJi7Nh6H6RSm288IC4i + hSmSBFabffQtwOTVE0CaKge2nU4Oc3Tp2h8moEgi81vYT/CioMt6wmHTzY0grcfF + WLwtDMFJ0VQYRrUIOMmYBFyRp2jdRYYkA+vlL+6DNAAjCeuvwCs3PqUhgFvHNxVv + bumKiRMIOoNUwpLEKsEq8jBs+U+gUfa+CmBn67G9mjDRu4cXXrtItooxnbfM/m0i + hVnssTC1arrx273zFepLosPvgrT0TS7tnyXbzuq5mo0zD1fSj4kuSS9V/SSy9fWF + LAtHiNQJkjzGFxu0/9dyQyX6C523uvfdcOzpObTyjBeGKqmEEf0lF5OYLDlkk2Sm + iGa6i2oLaGzGaQZDpdqyQZiYpQEYw9xN+8g= + =J31U + -----END PGP PUBLIC KEY BLOCK----- + """ + EOF + "#}) + .call(), + ]); + }, + |ctx| { + assert_contains!(ctx.pack_stderr, "https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 [multiverse]"); + assert_contains!(ctx.pack_stderr, "Downloaded release file https://repo.mongodb.org/apt/ubuntu/dists/noble/mongodb-org/8.0/InRelease"); + assert_contains_match!(ctx.pack_stderr, r"Downloaded package index https://repo.mongodb.org/apt/ubuntu/dists/noble/mongodb-org/8.0/multiverse/binary-(amd|arm)64/Packages.gz"); + assert_contains!(ctx.pack_stderr, "Adding `mongodb-org-tools"); + assert_contains!(ctx.pack_stderr, "Adding `mongodb-org-shell"); + }, + ); +} + const DEFAULT_BUILDER: &str = "heroku/builder:24"; fn get_integration_test_builder() -> String { @@ -657,5 +811,33 @@ fn update_project_toml(app_dir: &Path, update_fn: impl FnOnce(&mut DocumentMut)) let contents = std::fs::read_to_string(&project_toml).unwrap(); let mut doc = toml_edit::DocumentMut::from_str(&contents).unwrap(); update_fn(&mut doc); - std::fs::write(&project_toml, doc.to_string()).unwrap(); + fs::write(&project_toml, doc.to_string()).unwrap(); +} + +#[bon::builder(on(String, into))] +pub fn custom_buildpack(id: &str, detect: Option, build: Option) -> BuildpackReference { + let buildpack_dir = tempfile::tempdir().unwrap().keep(); + let bin_dir = buildpack_dir.join("bin"); + + fs::create_dir(&bin_dir).unwrap(); + + fs::write( + buildpack_dir.join("buildpack.toml"), + format!( + " +api = \"0.10\" + +[buildpack] +id = \"{id}\" +version = \"0.0.0\" + " + ), + ) + .unwrap(); + + fs::write(bin_dir.join("detect"), detect.unwrap_or("#!/usr/bin/env bash".to_string())).unwrap(); + + fs::write(bin_dir.join("build"), build.unwrap_or("#!/usr/bin/env bash".to_string())).unwrap(); + + BuildpackReference::Other(buildpack_dir.to_string_lossy().to_string()) }