Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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"

Expand Down
59 changes: 57 additions & 2 deletions src/config/buildpack_config.rs
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,6 +27,43 @@ impl BuildpackConfig {
Err(e) => Err(e),
}
}

pub(crate) fn merge<I>(configs: I) -> BuildpackConfig
where
I: IntoIterator<Item = BuildpackConfig>,
{
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<DebianPackagesBuildpack>> for BuildpackConfig {
type Error = ConfigError;

fn try_from(value: &BuildContext<DebianPackagesBuildpack>) -> Result<Self, Self::Error> {
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::<Result<Vec<_>, _>>()?;
Ok(BuildpackConfig::merge(buildpack_configs))
}
}

impl TryFrom<PathBuf> for BuildpackConfig {
Expand All @@ -46,6 +85,20 @@ impl FromStr for BuildpackConfig {
}
}

impl TryFrom<&Table> for BuildpackConfig {
type Error = ParseConfigError;

fn try_from(value: &Table) -> Result<Self, Self::Error> {
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;

Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ fn on_config_error(error: ConfigError) -> ErrorMessage {
}
}
}

ConfigError::ParseBuildplanConfig(e) => todo!("{e:?}"),
}
}

Expand Down
33 changes: 26 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -49,20 +50,30 @@ impl Buildpack for DebianPackagesBuildpack {
type Error = DebianPackagesBuildpackError;

fn detect(&self, context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
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()
Expand Down Expand Up @@ -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");
Expand Down
188 changes: 185 additions & 3 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 <<EOF >"$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 <<EOF >"$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 {
Expand Down Expand Up @@ -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<String>, build: Option<String>) -> 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())
}
Loading