-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Apt - Minimum Viable Buildpack - 02 - Aptfile Parsing (#2)
The `Aptfile` is how an application can specify custom packages to install. Unlike the version from our [classic apt buildpack](https://github.com/heroku/heroku-buildpack-apt), this parser **does not support**: - [Custom debian packages](https://github.com/heroku/heroku-buildpack-apt/blob/master/bin/compile#L84-L89) - [Custom repositories](https://github.com/heroku/heroku-buildpack-apt/blob/master/bin/compile#L71-L72) Only package names, comment lines, and blank lines are supported in this version. Package names are parsed according to the restrictions defined in the [Debian Manual](https://www.debian.org/doc/debian-policy/ch-controlfields.html#source).
- Loading branch information
1 parent
89e9e34
commit 6c91816
Showing
8 changed files
with
879 additions
and
32 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
use std::collections::HashSet; | ||
use std::str::FromStr; | ||
|
||
#[derive(Debug, Eq, PartialEq)] | ||
pub(crate) struct Aptfile { | ||
packages: HashSet<DebianPackageName>, | ||
} | ||
|
||
impl FromStr for Aptfile { | ||
type Err = ParseAptfileError; | ||
|
||
fn from_str(value: &str) -> Result<Self, Self::Err> { | ||
value | ||
.lines() | ||
.map(str::trim) | ||
.filter(|line| !line.starts_with('#') && !line.is_empty()) | ||
.map(DebianPackageName::from_str) | ||
.collect::<Result<HashSet<_>, _>>() | ||
.map_err(ParseAptfileError) | ||
.map(|packages| Aptfile { packages }) | ||
} | ||
} | ||
|
||
#[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<Self, Self::Err> { | ||
// 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! { " | ||
# comment line | ||
# comment line with leading whitespace | ||
package-name-1 | ||
package-name-2 | ||
# Package name has leading and trailing whitespace | ||
package-name-3 \t | ||
# Duplicates are allowed (at least for now) | ||
package-name-1 | ||
" }) | ||
.unwrap(); | ||
assert_eq!( | ||
aptfile, | ||
Aptfile { | ||
packages: HashSet::from([ | ||
DebianPackageName("package-name-1".to_string()), | ||
DebianPackageName("package-name-2".to_string()), | ||
DebianPackageName("package-name-3".to_string()), | ||
]) | ||
} | ||
); | ||
} | ||
|
||
#[test] | ||
fn parse_invalid_aptfile() { | ||
let error = Aptfile::from_str(indoc! { " | ||
invalid package name! | ||
" }) | ||
.unwrap_err(); | ||
assert_eq!( | ||
error, | ||
ParseAptfileError(ParseDebianPackageNameError( | ||
"invalid package name!".to_string() | ||
)) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,15 @@ | ||
use crate::aptfile::ParseAptfileError; | ||
|
||
#[derive(Debug)] | ||
pub(crate) enum AptBuildpackError {} | ||
#[allow(clippy::enum_variant_names)] | ||
pub(crate) enum AptBuildpackError { | ||
DetectAptfile(std::io::Error), | ||
ReadAptfile(std::io::Error), | ||
ParseAptfile(ParseAptfileError), | ||
} | ||
|
||
impl From<AptBuildpackError> for libcnb::Error<AptBuildpackError> { | ||
fn from(value: AptBuildpackError) -> Self { | ||
Self::BuildpackError(value) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,54 @@ | ||
use crate::aptfile::Aptfile; | ||
use crate::errors::AptBuildpackError; | ||
use libcnb::build::{BuildContext, BuildResult}; | ||
use libcnb::detect::{DetectContext, DetectResult}; | ||
use commons::output::build_log::{BuildLog, Logger}; | ||
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; | ||
use std::io::stdout; | ||
|
||
#[cfg(test)] | ||
use libcnb_test as _; | ||
|
||
mod aptfile; | ||
mod errors; | ||
|
||
buildpack_main!(AptBuildpack); | ||
|
||
const APTFILE_PATH: &str = "Aptfile"; | ||
|
||
struct AptBuildpack; | ||
|
||
impl Buildpack for AptBuildpack { | ||
type Platform = GenericPlatform; | ||
type Metadata = GenericMetadata; | ||
type Error = AptBuildpackError; | ||
|
||
fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> { | ||
todo!() | ||
fn detect(&self, context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> { | ||
let aptfile_exists = context | ||
.app_dir | ||
.join(APTFILE_PATH) | ||
.try_exists() | ||
.map_err(AptBuildpackError::DetectAptfile)?; | ||
|
||
if aptfile_exists { | ||
DetectResultBuilder::pass().build() | ||
} else { | ||
BuildLog::new(stdout()) | ||
.without_buildpack_name() | ||
.announce() | ||
.warning("No Aptfile found."); | ||
DetectResultBuilder::fail().build() | ||
} | ||
} | ||
|
||
fn build(&self, _context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> { | ||
todo!() | ||
fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> { | ||
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() | ||
} | ||
} |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters