diff --git a/Cargo.lock b/Cargo.lock index 46b1c8b..49ca990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,8 +78,10 @@ dependencies = [ name = "buildpacks-apt" version = "0.1.0" dependencies = [ + "indoc", "libcnb", "libcnb-test", + "serde", ] [[package]] @@ -395,6 +397,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + [[package]] name = "itoa" version = "1.0.10" diff --git a/Cargo.toml b/Cargo.toml index 3000ee5..0abf200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,11 @@ rust-version = "1.75" [dependencies] libcnb = { version = "=0.17.0", features = ["trace"] } +serde = "1" [dev-dependencies] libcnb-test = "=0.17.0" +indoc = "2" [profile.release] strip = true \ No newline at end of file diff --git a/src/aptfile.rs b/src/aptfile.rs new file mode 100644 index 0000000..4a22ac9 --- /dev/null +++ b/src/aptfile.rs @@ -0,0 +1,103 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; +use std::ffi::OsStr; +use std::ops::Deref; +use std::str::FromStr; + +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub(crate) struct Aptfile { + pub(crate) packages: HashSet, +} + +#[derive(Debug)] +pub(crate) struct ParseAptfileError(ParseDebianPackageError); + +impl FromStr for Aptfile { + type Err = ParseAptfileError; + + fn from_str(value: &str) -> Result { + value + .lines() + .filter_map(|mut line| { + line = line.trim(); + if line.starts_with('#') || line.is_empty() { + None + } else { + Some(line) + } + }) + .map(|line| line.parse::()) + .collect::, _>>() + .map_err(ParseAptfileError) + .map(|packages| Aptfile { packages }) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub(crate) struct DebianPackage(String); + +impl Deref for DebianPackage { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for DebianPackage { + fn as_ref(&self) -> &OsStr { + OsStr::new(&self.0) + } +} + +#[derive(Debug, PartialEq)] +pub(crate) struct ParseDebianPackageError(String); + +impl FromStr for DebianPackage { + type Err = ParseDebianPackageError; + + fn from_str(value: &str) -> Result { + if value.is_empty() { + Err(ParseDebianPackageError(value.to_string())) + } else { + Ok(DebianPackage(value.to_string())) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + + #[test] + fn parse_valid_debian_package() { + let debian_package = DebianPackage::from_str("package-name").unwrap(); + assert_eq!(*debian_package, "package-name".to_string()); + } + #[test] + fn parse_invalid_debian_package() { + let error = DebianPackage::from_str("").unwrap_err(); + assert_eq!(error, ParseDebianPackageError("".to_string())); + } + + #[test] + fn parse_aptfile() { + let aptfile = Aptfile::from_str(indoc! { " + # comment line + + package-name-1 + package-name-2 + + " }) + .unwrap(); + assert_eq!( + aptfile.packages, + HashSet::from([ + DebianPackage::from_str("package-name-1").unwrap(), + DebianPackage::from_str("package-name-2").unwrap(), + ]) + ); + } +} diff --git a/src/errors.rs b/src/errors.rs index 06ca2dc..da7b8b1 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,2 +1,13 @@ #[derive(Debug)] -pub(crate) enum AptBuildpackError {} +#[allow(clippy::enum_variant_names)] +pub(crate) enum AptBuildpackError { + DetectAptfile(std::io::Error), + ReadAptfile(std::io::Error), + ParseAptfile, +} + +impl From for libcnb::Error { + fn from(value: AptBuildpackError) -> Self { + libcnb::Error::BuildpackError(value) + } +} diff --git a/src/main.rs b/src/main.rs index 45f583d..5111664 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,18 @@ +use crate::aptfile::Aptfile; use crate::errors::AptBuildpackError; -use libcnb::build::{BuildContext, BuildResult}; -use libcnb::detect::{DetectContext, DetectResult}; +use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder}; +use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder}; use libcnb::generic::{GenericMetadata, GenericPlatform}; use libcnb::{buildpack_main, Buildpack}; +use std::fs; +mod aptfile; mod errors; buildpack_main!(AptBuildpack); +const APTFILE_PATH: &str = "Aptfile"; + struct AptBuildpack; impl Buildpack for AptBuildpack { @@ -15,11 +20,26 @@ impl Buildpack for AptBuildpack { type Metadata = GenericMetadata; type Error = AptBuildpackError; - fn detect(&self, _context: DetectContext) -> libcnb::Result { - todo!() + fn detect(&self, context: DetectContext) -> libcnb::Result { + let exists = context + .app_dir + .join(APTFILE_PATH) + .try_exists() + .map_err(AptBuildpackError::DetectAptfile)?; + + if exists { + DetectResultBuilder::pass().build() + } else { + DetectResultBuilder::fail().build() + } } - fn build(&self, _context: BuildContext) -> libcnb::Result { - todo!() + fn build(&self, context: BuildContext) -> libcnb::Result { + let _aptfile: Aptfile = fs::read_to_string(context.app_dir.join(APTFILE_PATH)) + .map_err(AptBuildpackError::ReadAptfile)? + .parse() + .map_err(|_| AptBuildpackError::ParseAptfile)?; + + BuildResultBuilder::new().build() } }