Skip to content

Commit

Permalink
Add target support
Browse files Browse the repository at this point in the history
  • Loading branch information
colincasey committed Feb 26, 2024
1 parent 3dad333 commit 8fb2955
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 93 deletions.
4 changes: 4 additions & 0 deletions buildpack.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
65 changes: 1 addition & 64 deletions src/aptfile.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::debian::{DebianPackageName, ParseDebianPackageNameError};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::str::FromStr;
Expand Down Expand Up @@ -25,75 +26,11 @@ impl FromStr for Aptfile {
#[derive(Debug, PartialEq)]
pub(crate) struct ParseAptfileError(ParseDebianPackageNameError);

#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
#[serde(transparent)]
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! { "
Expand Down
147 changes: 147 additions & 0 deletions src/debian.rs
Original file line number Diff line number Diff line change
@@ -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(transparent)]
// 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<Self, Self::Err> {
// 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<Self, Self::Err> {
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"
);
}
}
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use crate::aptfile::ParseAptfileError;
use crate::debian::ParseDebianArchitectureNameError;

#[derive(Debug)]
#[allow(clippy::enum_variant_names)]
pub(crate) enum AptBuildpackError {
DetectAptfile(std::io::Error),
ReadAptfile(std::io::Error),
ParseAptfile(ParseAptfileError),
ParseDebianArchitectureName(ParseDebianArchitectureNameError),
}

impl From<AptBuildpackError> for libcnb::Error<AptBuildpackError> {
Expand Down
39 changes: 23 additions & 16 deletions src/layers/installed_packages.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::aptfile::Aptfile;
use crate::debian::{DebianArchitectureName, DebianMultiarchName};
use crate::errors::AptBuildpackError;
use crate::AptBuildpack;
use commons::output::interface::SectionLogger;
use commons::output::section_log::log_step;
Expand All @@ -10,6 +12,7 @@ use libcnb::Buildpack;
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};

pub(crate) struct InstalledPackagesLayer<'a> {
Expand Down Expand Up @@ -37,41 +40,44 @@ impl<'a> Layer for InstalledPackagesLayer<'a> {
) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
log_step("Creating cache directory");

let debian_architecture = DebianArchitectureName::from_str(&context.target.arch)
.map_err(AptBuildpackError::ParseDebianArchitectureName)?;

let debian_multiarch_name = DebianMultiarchName::from(debian_architecture);

let mut env = LayerEnv::new();

let bin_paths = [
layer_path.join("bin"),
layer_path.join("usr/bin"),
layer_path.join("usr/sbin"),
];
prepend_to_env_var(&mut env, "PATH", ":", &bin_paths);
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 = [
layer_path.join("usr/lib/x86_64-linux-gnu"),
layer_path.join("usr/lib/i386-linux-gnu"),
layer_path.join(format!("usr/lib/{debian_multiarch_name}")),
layer_path.join("usr/lib"),
layer_path.join("lib/x86_64-linux-gnu"),
layer_path.join("lib/i386-linux-gnu"),
layer_path.join(format!("lib/{debian_multiarch_name}")),
layer_path.join("lib"),
];
prepend_to_env_var(&mut env, "LD_LIBRARY_PATH", ":", &library_paths);
prepend_to_env_var(&mut env, "LIBRARY_PATH", ":", &library_paths);
prepend_to_env_var(&mut env, "LD_LIBRARY_PATH", &library_paths);
prepend_to_env_var(&mut env, "LIBRARY_PATH", &library_paths);

let include_paths = [
layer_path.join("usr/include/x86_64-linux-gnu"),
layer_path.join("usr/include/i386-linux-gnu"),
layer_path.join(format!("usr/include/{debian_multiarch_name}")),
layer_path.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);
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 = [
layer_path.join("usr/lib/x86_64-linux-gnu/pkgconfig"),
layer_path.join("usr/lib/i386-linux-gnu/pkgconfig"),
layer_path.join(format!("usr/lib/{debian_multiarch_name}/pkgconfig")),
layer_path.join("usr/lib/pkgconfig"),
];
prepend_to_env_var(&mut env, "PKG_CONFIG_PATH", ":", &pkg_config_paths);
prepend_to_env_var(&mut env, "PKG_CONFIG_PATH", &pkg_config_paths);

LayerResultBuilder::new(InstalledPackagesMetadata::new(
self.aptfile.clone(),
Expand Down Expand Up @@ -107,11 +113,12 @@ impl<'a> Layer for InstalledPackagesLayer<'a> {
}
}

fn prepend_to_env_var<I, T>(env: &mut LayerEnv, name: &str, separator: &str, paths: I)
fn prepend_to_env_var<I, T>(env: &mut LayerEnv, name: &str, paths: I)
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let separator = ":";
env.insert(Scope::All, ModificationBehavior::Delimiter, name, separator);
env.insert(
Scope::All,
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::layers::installed_packages::{InstalledPackagesLayer, InstalledPackage
use libcnb_test as _;

mod aptfile;
mod debian;
mod errors;
mod layers;

Expand Down
13 changes: 0 additions & 13 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,11 @@ fn test_environment_configuration() {
ld_library_path,
&format!("{layer_dir}/usr/lib/x86_64-linux-gnu")
);
assert_contains!(
ld_library_path,
&format!("{layer_dir}/usr/lib/i386-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/i386-linux-gnu"));
assert_contains!(ld_library_path, &format!("{layer_dir}/lib"));

let library_path = get_env_var(&ctx, "LIBRARY_PATH");
Expand All @@ -112,10 +107,6 @@ fn test_environment_configuration() {
include_path,
&format!("{layer_dir}/usr/include/x86_64-linux-gnu")
);
assert_contains!(
include_path,
&format!("{layer_dir}/usr/include/i386-linux-gnu")
);
assert_contains!(include_path, &format!("{layer_dir}/usr/include"));

let cpath = get_env_var(&ctx, "CPATH");
Expand All @@ -129,10 +120,6 @@ fn test_environment_configuration() {
pkg_config_path,
&format!("{layer_dir}/usr/lib/x86_64-linux-gnu/pkgconfig")
);
assert_contains!(
pkg_config_path,
&format!("{layer_dir}/usr/lib/i386-linux-gnu/pkgconfig")
);
assert_contains!(pkg_config_path, &format!("{layer_dir}/usr/lib/pkgconfig"));
},
);
Expand Down

0 comments on commit 8fb2955

Please sign in to comment.