use crate::build::{BuildContext, InnerBuildResult};
use crate::buildpack::Buildpack;
use crate::data::buildpack::BuildpackApi;
use crate::detect::{DetectContext, InnerDetectResult};
use crate::error::Error;
use crate::platform::Platform;
use crate::sbom::cnb_sbom_path;
#[cfg(feature = "trace")]
use crate::tracing::init_tracing;
use crate::util::is_not_found_error_kind;
use crate::{LIBCNB_SUPPORTED_BUILDPACK_API, Target, TomlFileError, exit_code};
use libcnb_common::toml_file::{read_toml_file, write_toml_file};
use libcnb_data::buildpack::ComponentBuildpackDescriptor;
use libcnb_data::store::Store;
use serde::Deserialize;
use serde::de::DeserializeOwned;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::{env, fs};
pub(crate) enum ExecutionPhase {
Detect(DetectArgs),
Build(BuildArgs),
}
#[doc(hidden)]
pub fn libcnb_runtime<B: Buildpack>(buildpack: &B) {
match read_buildpack_descriptor::<BuildpackDescriptorApiOnly, B::Error>() {
Ok(buildpack_descriptor) => {
if buildpack_descriptor.api != LIBCNB_SUPPORTED_BUILDPACK_API {
eprintln!("Error: Cloud Native Buildpack API mismatch");
eprintln!(
"This buildpack uses Cloud Native Buildpacks API version {} (specified in buildpack.toml).",
&buildpack_descriptor.api,
);
eprintln!(
"However, the underlying libcnb.rs library only supports CNB API {LIBCNB_SUPPORTED_BUILDPACK_API}."
);
exit(exit_code::GENERIC_CNB_API_VERSION_ERROR)
}
}
Err(libcnb_error) => {
eprintln!("Error: Unable to determine Buildpack API version");
eprintln!("Cause: {libcnb_error}");
exit(exit_code::GENERIC_CNB_API_VERSION_ERROR);
}
}
let args: Vec<String> = env::args().collect();
let current_exe = args.first();
let current_exe_file_name = current_exe
.map(Path::new)
.and_then(Path::file_name)
.and_then(OsStr::to_str);
let execution_phase = match current_exe_file_name {
Some("detect") => {
ExecutionPhase::Detect(DetectArgs::parse(&args).unwrap_or_else(|parse_error| {
match parse_error {
DetectArgsParseError::InvalidArguments => {
eprintln!("Usage: detect <platform_dir> <buildplan>");
eprintln!(
"https://github.com/buildpacks/spec/blob/main/buildpack.md#detection"
);
exit(exit_code::GENERIC_UNSPECIFIED_ERROR);
}
}
}))
}
Some("build") => {
ExecutionPhase::Build(BuildArgs::parse(&args).unwrap_or_else(|parse_error| {
match parse_error {
BuildArgsParseError::InvalidArguments => {
eprintln!("Usage: build <layers> <platform> <plan>");
eprintln!(
"https://github.com/buildpacks/spec/blob/main/buildpack.md#build"
);
exit(exit_code::GENERIC_UNSPECIFIED_ERROR);
}
}
}))
}
other => {
eprintln!(
"Error: Expected the name of this executable to be 'detect' or 'build', but it was '{}'",
other.unwrap_or("<unknown>")
);
eprintln!("The executable name is used to determine the current buildpack phase.");
eprintln!(
"You might want to create 'detect' and 'build' links to this executable and run those instead."
);
exit(exit_code::GENERIC_UNEXPECTED_EXECUTABLE_NAME_ERROR)
}
};
let buildpack_descriptor: ComponentBuildpackDescriptor<<B as Buildpack>::Metadata> =
match read_buildpack_descriptor() {
Ok(descriptor) => descriptor,
Err(libcnb_error) => {
buildpack.on_error(libcnb_error);
exit(exit_code::GENERIC_UNSPECIFIED_ERROR);
}
};
#[cfg(feature = "trace")]
let buildpack_trace = init_tracing(&buildpack_descriptor.buildpack, &execution_phase);
let result = match execution_phase {
ExecutionPhase::Detect(detect_args) => {
libcnb_runtime_detect(buildpack, buildpack_descriptor, detect_args)
}
ExecutionPhase::Build(build_args) => {
libcnb_runtime_build(buildpack, buildpack_descriptor, build_args)
}
};
let exit_code = match result {
Ok(code) => code,
Err(libcnb_error) => {
buildpack.on_error(libcnb_error);
exit_code::GENERIC_UNSPECIFIED_ERROR
}
};
#[cfg(feature = "trace")]
drop(buildpack_trace);
exit(exit_code)
}
#[doc(hidden)]
pub fn libcnb_runtime_detect<B: Buildpack>(
buildpack: &B,
buildpack_descriptor: ComponentBuildpackDescriptor<<B as Buildpack>::Metadata>,
args: DetectArgs,
) -> crate::Result<i32, B::Error> {
let app_dir = env::current_dir().map_err(Error::CannotDetermineAppDirectory)?;
let buildpack_dir = read_buildpack_dir()?;
#[cfg(feature = "trace")]
let detect_span = tracing::info_span!("libcnb-detect", failed = true).entered();
#[cfg(feature = "trace")]
let trace_error =
|error: &Error<<B as Buildpack>::Error>| tracing::error!(?error, "libcnb-detect-error");
#[cfg(not(feature = "trace"))]
let trace_error = |_: &Error<<B as Buildpack>::Error>| {};
let platform = B::Platform::from_path(&args.platform_dir_path)
.map_err(Error::CannotCreatePlatformFromPath)
.inspect_err(trace_error)?;
let build_plan_path = args.build_plan_path;
let target = context_target().inspect_err(trace_error)?;
let detect_context = DetectContext {
app_dir,
buildpack_dir,
target,
platform,
buildpack_descriptor,
};
let detect_result = buildpack.detect(detect_context).inspect_err(trace_error)?;
match detect_result.0 {
InnerDetectResult::Fail => {
#[cfg(feature = "trace")]
tracing::info!("libcnb-detect-failed");
Ok(exit_code::DETECT_DETECTION_FAILED)
}
InnerDetectResult::Pass { build_plan } => {
if let Some(build_plan) = build_plan {
write_toml_file(&build_plan, build_plan_path)
.map_err(Error::CannotWriteBuildPlan)
.inspect_err(trace_error)?;
}
#[cfg(feature = "trace")]
{
detect_span.record("failed", false);
tracing::info!("libcnb-detect-passed");
}
Ok(exit_code::DETECT_DETECTION_PASSED)
}
}
}
#[doc(hidden)]
pub fn libcnb_runtime_build<B: Buildpack>(
buildpack: &B,
buildpack_descriptor: ComponentBuildpackDescriptor<<B as Buildpack>::Metadata>,
args: BuildArgs,
) -> crate::Result<i32, B::Error> {
let layers_dir = args.layers_dir_path;
let app_dir = env::current_dir().map_err(Error::CannotDetermineAppDirectory)?;
let buildpack_dir = read_buildpack_dir()?;
#[cfg(feature = "trace")]
let build_span = tracing::info_span!("libcnb-build", failed = true).entered();
#[cfg(feature = "trace")]
let trace_error =
|error: &Error<<B as Buildpack>::Error>| tracing::error!(?error, "libcnb-build-error");
#[cfg(not(feature = "trace"))]
let trace_error = |_: &Error<<B as Buildpack>::Error>| {};
let platform = Platform::from_path(&args.platform_dir_path)
.map_err(Error::CannotCreatePlatformFromPath)
.inspect_err(trace_error)?;
let buildpack_plan = read_toml_file(&args.buildpack_plan_path)
.map_err(Error::CannotReadBuildpackPlan)
.inspect_err(trace_error)?;
let store = match read_toml_file::<Store>(layers_dir.join("store.toml")) {
Err(TomlFileError::IoError(io_error)) if is_not_found_error_kind(&io_error) => Ok(None),
other => other.map(Some),
}
.map_err(Error::CannotReadStore)
.inspect_err(trace_error)?;
let target = context_target().inspect_err(trace_error)?;
let build_context = BuildContext {
layers_dir: layers_dir.clone(),
app_dir,
platform,
target,
buildpack_plan,
buildpack_dir,
buildpack_descriptor,
store,
};
let build_result = buildpack.build(build_context).inspect_err(trace_error)?;
match build_result.0 {
InnerBuildResult::Pass {
launch,
store,
build_sboms,
launch_sboms,
} => {
if let Some(launch) = launch {
write_toml_file(&launch, layers_dir.join("launch.toml"))
.map_err(Error::CannotWriteLaunch)
.inspect_err(trace_error)?;
}
if let Some(store) = store {
write_toml_file(&store, layers_dir.join("store.toml"))
.map_err(Error::CannotWriteStore)
.inspect_err(trace_error)?;
}
for build_sbom in build_sboms {
fs::write(
cnb_sbom_path(&build_sbom.format, &layers_dir, "build"),
&build_sbom.data,
)
.map_err(Error::CannotWriteBuildSbom)
.inspect_err(trace_error)?;
}
for launch_sbom in launch_sboms {
fs::write(
cnb_sbom_path(&launch_sbom.format, &layers_dir, "launch"),
&launch_sbom.data,
)
.map_err(Error::CannotWriteLaunchSbom)
.inspect_err(trace_error)?;
}
#[cfg(feature = "trace")]
{
build_span.record("failed", false);
tracing::info!("libcnb-build-success");
}
Ok(exit_code::GENERIC_SUCCESS)
}
}
}
#[derive(Deserialize)]
struct BuildpackDescriptorApiOnly {
api: BuildpackApi,
}
#[doc(hidden)]
pub struct DetectArgs {
pub platform_dir_path: PathBuf,
pub build_plan_path: PathBuf,
}
impl DetectArgs {
pub fn parse(args: &[String]) -> Result<Self, DetectArgsParseError> {
if let [_, platform_dir_path, build_plan_path] = args {
Ok(Self {
platform_dir_path: PathBuf::from(platform_dir_path),
build_plan_path: PathBuf::from(build_plan_path),
})
} else {
Err(DetectArgsParseError::InvalidArguments)
}
}
}
#[derive(Debug)]
#[doc(hidden)]
pub enum DetectArgsParseError {
InvalidArguments,
}
#[doc(hidden)]
pub struct BuildArgs {
pub layers_dir_path: PathBuf,
pub platform_dir_path: PathBuf,
pub buildpack_plan_path: PathBuf,
}
impl BuildArgs {
pub fn parse(args: &[String]) -> Result<Self, BuildArgsParseError> {
if let [_, layers_dir_path, platform_dir_path, buildpack_plan_path] = args {
Ok(Self {
layers_dir_path: PathBuf::from(layers_dir_path),
platform_dir_path: PathBuf::from(platform_dir_path),
buildpack_plan_path: PathBuf::from(buildpack_plan_path),
})
} else {
Err(BuildArgsParseError::InvalidArguments)
}
}
}
#[derive(Debug)]
#[doc(hidden)]
pub enum BuildArgsParseError {
InvalidArguments,
}
fn read_buildpack_dir<E: Debug>() -> crate::Result<PathBuf, E> {
env::var("CNB_BUILDPACK_DIR")
.map_err(Error::CannotDetermineBuildpackDirectory)
.map(PathBuf::from)
}
fn read_buildpack_descriptor<BD: DeserializeOwned, E: Debug>() -> crate::Result<BD, E> {
read_buildpack_dir().and_then(|buildpack_dir| {
read_toml_file(buildpack_dir.join("buildpack.toml"))
.map_err(Error::CannotReadBuildpackDescriptor)
})
}
fn context_target<E>() -> crate::Result<Target, E>
where
E: Debug,
{
let os = env::var("CNB_TARGET_OS").map_err(Error::CannotDetermineTargetOs)?;
let arch = env::var("CNB_TARGET_ARCH").map_err(Error::CannotDetermineTargetArch)?;
let arch_variant = env::var("CNB_TARGET_ARCH_VARIANT").ok();
let distro_name =
env::var("CNB_TARGET_DISTRO_NAME").map_err(Error::CannotDetermineTargetDistroName)?;
let distro_version =
env::var("CNB_TARGET_DISTRO_VERSION").map_err(Error::CannotDetermineTargetDistroVersion)?;
Ok(Target {
os,
arch,
arch_variant,
distro_name,
distro_version,
})
}