Skip to content

Commit

Permalink
Installed packages layer
Browse files Browse the repository at this point in the history
  • Loading branch information
colincasey committed Feb 21, 2024
1 parent 6c91816 commit 4563fe7
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 4 deletions.
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 @@ -6,6 +6,7 @@ rust-version = "1.76"
[dependencies]
commons = { git = "https://github.com/heroku/buildpacks-ruby", branch = "main" }
libcnb = "=0.18.0"
serde = "1"

[dev-dependencies]
libcnb-test = "=0.18.0"
Expand Down
6 changes: 4 additions & 2 deletions src/aptfile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::str::FromStr;

#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub(crate) struct Aptfile {
packages: HashSet<DebianPackageName>,
}
Expand All @@ -24,7 +25,8 @@ impl FromStr for Aptfile {
#[derive(Debug, PartialEq)]
pub(crate) struct ParseAptfileError(ParseDebianPackageNameError);

#[derive(Debug, Eq, PartialEq, Hash)]
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub(crate) struct DebianPackageName(String);

impl FromStr for DebianPackageName {
Expand Down
198 changes: 198 additions & 0 deletions src/layers/installed_packages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use crate::aptfile::Aptfile;
use crate::AptBuildpack;
use commons::output::interface::SectionLogger;
use commons::output::section_log::log_step;
use libcnb::build::BuildContext;
use libcnb::data::layer_content_metadata::LayerTypes;
use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder};
use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope};
use libcnb::Buildpack;
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};

pub(crate) struct InstalledPackagesLayer<'a> {
pub(crate) aptfile: &'a Aptfile,
pub(crate) cache_restored: &'a AtomicBool,
pub(crate) _section_logger: &'a dyn SectionLogger,
}

impl<'a> Layer for InstalledPackagesLayer<'a> {
type Buildpack = AptBuildpack;
type Metadata = InstalledPackagesMetadata;

fn types(&self) -> LayerTypes {
LayerTypes {
build: true,
launch: true,
cache: true,
}
}

fn create(
&mut self,
context: &BuildContext<Self::Buildpack>,
layer_path: &Path,
) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
log_step("Creating cache directory");

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);

let library_paths = [
layer_path.join("usr/lib/x86_64-linux-gnu"),
layer_path.join("usr/lib/i386-linux-gnu"),
layer_path.join("usr/lib"),
layer_path.join("lib/x86_64-linux-gnu"),
layer_path.join("lib/i386-linux-gnu"),
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);

let include_paths = [
layer_path.join("usr/include/x86_64-linux-gnu"),
layer_path.join("usr/include/i386-linux-gnu"),
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);

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("usr/lib/pkgconfig"),
];
prepend_to_env_var(&mut env, "PKG_CONFIG_PATH", ":", &pkg_config_paths);

LayerResultBuilder::new(InstalledPackagesMetadata::new(
self.aptfile.clone(),
context.target.os.clone(),
context.target.arch.clone(),
))
.env(env)
.build()
}

fn existing_layer_strategy(
&mut self,
context: &BuildContext<Self::Buildpack>,
layer_data: &LayerData<Self::Metadata>,
) -> Result<ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
let old_meta = &layer_data.content_metadata.metadata;
let new_meta = &InstalledPackagesMetadata::new(
self.aptfile.clone(),
context.target.os.clone(),
context.target.arch.clone(),
);
if old_meta == new_meta {
log_step("Restoring installed packages");
self.cache_restored.store(true, Ordering::Relaxed);
Ok(ExistingLayerStrategy::Keep)
} else {
log_step(format!(
"Invalidating installed packages ({} changed)",
new_meta.changed_fields(old_meta).join(", ")
));
Ok(ExistingLayerStrategy::Recreate)
}
}
}

fn prepend_to_env_var<I, T>(env: &mut LayerEnv, name: &str, separator: &str, paths: I)
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
env.insert(Scope::All, ModificationBehavior::Delimiter, name, separator);
env.insert(
Scope::All,
ModificationBehavior::Prepend,
name,
paths
.into_iter()
.map(Into::into)
.collect::<Vec<_>>()
.join(separator.as_ref()),
);
}

#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct InstalledPackagesMetadata {
arch: String,
aptfile: Aptfile,
os: String,
}

impl InstalledPackagesMetadata {
pub(crate) fn new(aptfile: Aptfile, os: String, arch: String) -> Self {
Self { arch, aptfile, os }
}

pub(crate) fn changed_fields(&self, other: &InstalledPackagesMetadata) -> Vec<String> {
let mut changed_fields = vec![];
if self.os != other.os {
changed_fields.push("os".to_string());
}
if self.arch != other.arch {
changed_fields.push("arch".to_string());
}
if self.aptfile != other.aptfile {
changed_fields.push("Aptfile".to_string());
}
changed_fields.sort();
changed_fields
}
}

#[derive(Debug, Eq, PartialEq)]
pub(crate) enum InstalledPackagesState {
New(PathBuf),
Restored,
}

#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;

#[test]
fn installed_packages_metadata_with_all_changed_fields() {
assert_eq!(
InstalledPackagesMetadata::new(
Aptfile::from_str("package-1").unwrap(),
"linux".to_string(),
"amd64".to_string(),
)
.changed_fields(&InstalledPackagesMetadata::new(
Aptfile::from_str("package-2").unwrap(),
"windows".to_string(),
"arm64".to_string(),
)),
&["Aptfile", "arch", "os"]
);
}

#[test]
fn installed_packages_metadata_with_no_changed_fields() {
assert!(InstalledPackagesMetadata::new(
Aptfile::from_str("package-1").unwrap(),
"linux".to_string(),
"amd64".to_string(),
)
.changed_fields(&InstalledPackagesMetadata::new(
Aptfile::from_str("package-1").unwrap(),
"linux".to_string(),
"amd64".to_string(),
))
.is_empty());
}
}
1 change: 1 addition & 0 deletions src/layers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod installed_packages;
41 changes: 40 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
use crate::aptfile::Aptfile;
use crate::errors::AptBuildpackError;
use commons::output::build_log::{BuildLog, Logger};
use commons::output::section_log::log_step;
use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
use libcnb::data::layer_name;
use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder};
use libcnb::generic::{GenericMetadata, GenericPlatform};
use libcnb::{buildpack_main, Buildpack};
use std::fs;
use std::io::stdout;
use std::sync::atomic::AtomicBool;

use crate::layers::installed_packages::{InstalledPackagesLayer, InstalledPackagesState};
#[cfg(test)]
use libcnb_test as _;

mod aptfile;
mod errors;
mod layers;

buildpack_main!(AptBuildpack);

const BUILDPACK_NAME: &str = "Heroku Apt Buildpack";

const APTFILE_PATH: &str = "Aptfile";

struct AptBuildpack;
Expand Down Expand Up @@ -44,11 +51,43 @@ impl Buildpack for AptBuildpack {
}

fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
let _aptfile: Aptfile = fs::read_to_string(context.app_dir.join(APTFILE_PATH))
let mut logger = BuildLog::new(stdout()).buildpack_name(BUILDPACK_NAME);

let aptfile: Aptfile = fs::read_to_string(context.app_dir.join(APTFILE_PATH))
.map_err(AptBuildpackError::ReadAptfile)?
.parse()
.map_err(AptBuildpackError::ParseAptfile)?;

let mut section = logger.section("Apt packages cache");
let cache_restored = AtomicBool::new(false);
let installed_packages_cache_state = context
.handle_layer(
layer_name!("installed_packages"),
InstalledPackagesLayer {
aptfile: &aptfile,
cache_restored: &cache_restored,
_section_logger: section.as_ref(),
},
)
.map(|layer| {
if cache_restored.into_inner() {
InstalledPackagesState::Restored
} else {
InstalledPackagesState::New(layer.path)
}
})?;
logger = section.end_section();

section = logger.section("Installing packages from Aptfile");
if let InstalledPackagesState::New(_install_path) = installed_packages_cache_state {
// TODO: install packages
} else {
log_step("Skipping, packages already in cache");
}
logger = section.end_section();

logger.finish_logging();

BuildResultBuilder::new().build()
}
}
1 change: 1 addition & 0 deletions tests/fixtures/basic/Aptfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xmlsec1
Loading

0 comments on commit 4563fe7

Please sign in to comment.