diff --git a/CHANGELOG.md b/CHANGELOG.md index 7649c2de..a5631e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,36 @@ separate changelogs for each crate were used. If you need to refer to these old ## [Unreleased] +The main feature added here is support in `libcnb-test` for expanded testing on local buildpacks. Previously it +was possible to test a single (local) buildpack with `BuildpackReference::Crate` for development testing as well as +external (remote) buildpacks with `BuildpackReference::Other(String)`. This support has been expanded to allow +testing of any local buildpack (including meta-buildpacks) with `BuildpackReference::Local(PathBuf)`. This should make +developing and testing changes across multiple buildpacks easier on the buildpack author. + +### Added + +- `libcnb-package` + - Added `find_cargo_workspace` which provides a convenient starting point for locating buildpacks for packaging and testing purposes. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - Added the `BuildpackOutputDirectoryLocator` which contains information on how compiled buildpack directories are structured and provides a `.get(buildpack_id)` method which produces the output path for a buildpack. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - Added `output::assemble_single_buildpack_directory` and `output::assemble_meta_buildpack_directory` which construct buildpack output directories with all their required files during packaging. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + +### Changed + +- `libcnb-package` + - Changed the `ReadBuildpackDataError` and `ReadBuildpackageDataError` enums from struct to tuple format to be consistent with other error enums in the package. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - Changed `build::build_buildpack_binaries` to drop the `cargo_metadata` argument since it can read that directly from the given `project_path`. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - Changed `build::BuildBinariesError` to include the error variant `ReadCargoMetadata(PathBuf, cargo_metadata::Error)`. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - Changed `buildpack_dependency::rewrite_buildpackage_local_dependencies` to accept a `&BuildpackOutputDirectoryLocator` instead of `&HashMap<&BuildpackId, PathBuf>`. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - Moved `default_buildpack_directory_name` to `output::default_buildpack_directory_name`. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) +- `libcnb-test` + - Change `BuildpackReference` to include the `Local(PathBuf)` variant for referencing buildpacks on the local file-system. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + +### Removed + +- `libcnb-package` + - `get_buildpack_target_dir` has been removed in favor of `BuildpackOutputDirectoryLocator` for building output paths to compiled buildpacks. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + - `assemble_buildpack_directory` has been removed in favor of `output::assemble_single_buildpack_directory` and `output::assemble_meta_buildpack_directory`. ([#590](https://github.com/heroku/libcnb.rs/pull/590)) + ### Added - `libcnb-package`: Add cross-compilation assistance for Linux `aarch64-unknown-linux-musl`. ([#577](https://github.com/heroku/libcnb.rs/pull/577)) diff --git a/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/one/buildpack.toml b/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/one/buildpack.toml index ede40ad4..a820d1cc 100644 --- a/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/one/buildpack.toml +++ b/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/one/buildpack.toml @@ -5,4 +5,4 @@ id = "multiple-buildpacks/one" version = "0.0.0" [[stacks]] -id = "some-stack" +id = "*" diff --git a/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/two/buildpack.toml b/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/two/buildpack.toml index e9c05496..7f29a11d 100644 --- a/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/two/buildpack.toml +++ b/libcnb-cargo/fixtures/multiple_buildpacks/buildpacks/two/buildpack.toml @@ -5,4 +5,4 @@ id = "multiple-buildpacks/two" version = "0.0.0" [[stacks]] -id = "some-stack" +id = "*" diff --git a/libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two/buildpack.toml b/libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two/buildpack.toml new file mode 100644 index 00000000..5c19285b --- /dev/null +++ b/libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two/buildpack.toml @@ -0,0 +1,31 @@ +api = "0.8" + +[buildpack] +id = "multiple-buildpacks/meta-two" +name = "Meta-buildpack Test" +version = "0.0.0" +homepage = "https://example.com" +description = "Official test example" +keywords = ["test"] + +[[buildpack.licenses]] +type = "BSD-3-Clause" + +[[order]] + +[[order.group]] +id = "multiple-buildpacks/one" +version = "0.0.0" + +[[order.group]] +id = "multiple-buildpacks/two" +version = "0.0.0" + +[[order.group]] +id = "heroku/procfile" +version = "2.0.0" +optional = true + +[metadata] +[metadata.extra] +some_key = "some_value" diff --git a/libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two/package.toml b/libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two/package.toml new file mode 100644 index 00000000..3f1c53fb --- /dev/null +++ b/libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two/package.toml @@ -0,0 +1,11 @@ +[buildpack] +uri = "." + +[[dependencies]] +uri = "libcnb:multiple-buildpacks/one" + +[[dependencies]] +uri = "libcnb:multiple-buildpacks/two" + +[[dependencies]] +uri = "docker://docker.io/heroku/procfile-cnb:2.0.0" diff --git a/libcnb-cargo/fixtures/single_buildpack/buildpack.toml b/libcnb-cargo/fixtures/single_buildpack/buildpack.toml index 5a46317c..92c67953 100644 --- a/libcnb-cargo/fixtures/single_buildpack/buildpack.toml +++ b/libcnb-cargo/fixtures/single_buildpack/buildpack.toml @@ -5,4 +5,4 @@ id = "single-buildpack" version = "0.0.0" [[stacks]] -id = "some-stack" +id = "*" diff --git a/libcnb-cargo/src/package/command.rs b/libcnb-cargo/src/package/command.rs index 8cfc8523..829dc2ed 100644 --- a/libcnb-cargo/src/package/command.rs +++ b/libcnb-cargo/src/package/command.rs @@ -1,66 +1,51 @@ use crate::cli::PackageArgs; use crate::package::error::Error; use cargo_metadata::MetadataCommand; -use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId}; -use libcnb_data::buildpackage::Buildpackage; +use libcnb_data::buildpack::BuildpackDescriptor; use libcnb_package::build::build_buildpack_binaries; -use libcnb_package::buildpack_dependency::{ - rewrite_buildpackage_local_dependencies, - rewrite_buildpackage_relative_path_dependencies_to_absolute, -}; use libcnb_package::buildpack_package::{read_buildpack_package, BuildpackPackage}; use libcnb_package::cross_compile::{cross_compile_assistance, CrossCompileAssistance}; -use libcnb_package::dependency_graph::{create_dependency_graph, get_dependencies}; -use libcnb_package::{ - assemble_buildpack_directory, find_buildpack_dirs, get_buildpack_target_dir, CargoProfile, +use libcnb_package::dependency_graph::{create_dependency_graph, get_dependencies, DependencyNode}; +use libcnb_package::output::{ + assemble_meta_buildpack_directory, assemble_single_buildpack_directory, + BuildpackOutputDirectoryLocator, }; -use std::collections::HashMap; +use libcnb_package::{find_buildpack_dirs, find_cargo_workspace, CargoProfile}; +use std::ffi::OsString; use std::path::{Path, PathBuf}; -use std::process::Command; type Result = std::result::Result; pub(crate) fn execute(args: &PackageArgs) -> Result<()> { + let target_triple = args.target.clone(); + + let cargo_build_env = get_cargo_build_env(&target_triple, args.no_cross_compile_assistance)?; + + let cargo_profile = if args.release { + CargoProfile::Release + } else { + CargoProfile::Dev + }; + eprintln!("šŸ” Locating buildpacks..."); let current_dir = std::env::current_dir().map_err(Error::GetCurrentDir)?; - let workspace = get_cargo_workspace_root(¤t_dir)?; + let workspace_dir = find_cargo_workspace(¤t_dir)?; - let workspace_target_dir = MetadataCommand::new() - .manifest_path(&workspace.join("Cargo.toml")) - .exec() - .map(|metadata| metadata.target_directory.into_std_path_buf()) - .map_err(|e| Error::ReadCargoMetadata { - path: workspace.clone(), - source: e, - })?; - - let buildpack_packages = create_dependency_graph( - find_buildpack_dirs(&workspace, &[workspace_target_dir.clone()]) - .map_err(|e| Error::FindBuildpackDirs { - path: workspace_target_dir.clone(), - source: e, - })? - .into_iter() - .map(|dir| read_buildpack_package(dir).map_err(std::convert::Into::into)) - .collect::>>()?, - )?; + let output_dir = get_buildpack_output_dir(&workspace_dir)?; - let target_directories_index = buildpack_packages - .node_weights() - .map(|buildpack_package| { - let id = buildpack_package.buildpack_id(); - let target_dir = if contains_buildpack_binaries(&buildpack_package.path) { - buildpack_package.path.clone() - } else { - get_buildpack_target_dir(id, &workspace_target_dir, args.release, &args.target) - }; - (id, target_dir) - }) - .collect::>(); + let buildpack_dirs = find_buildpack_dirs(&workspace_dir, &[output_dir.clone()]) + .map_err(|e| Error::FindBuildpackDirs(workspace_dir, e))?; - let buildpack_packages_requested = buildpack_packages + let buildpack_packages = buildpack_dirs + .into_iter() + .map(read_buildpack_package) + .collect::, _>>()?; + + let buildpack_packages_graph = create_dependency_graph(buildpack_packages)?; + + let buildpack_packages_requested = buildpack_packages_graph .node_weights() .filter(|buildpack_package| { // If we're in a directory with a buildpack.toml file, we only want to build the @@ -77,15 +62,17 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { Err(Error::NoBuildpacksFound)?; } - let build_order = get_dependencies(&buildpack_packages, &buildpack_packages_requested)?; + let build_order = get_dependencies(&buildpack_packages_graph, &buildpack_packages_requested)?; + + let buildpack_output_directory_locator = + BuildpackOutputDirectoryLocator::new(output_dir, cargo_profile, target_triple.clone()); let lookup_target_dir = |buildpack_package: &BuildpackPackage| { - target_directories_index - .get(&buildpack_package.buildpack_id()) - .ok_or(Error::TargetDirectoryLookup { - buildpack_id: buildpack_package.buildpack_id().clone(), - }) - .map(std::clone::Clone::clone) + if contains_buildpack_binaries(&buildpack_package.path) { + buildpack_package.path.clone() + } else { + buildpack_output_directory_locator.get(&buildpack_package.id()) + } }; let mut current_count = 1; @@ -95,17 +82,27 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { "šŸ“¦ [{current_count}/{total_count}] Building {}", buildpack_package.buildpack_id() ); - let target_dir = lookup_target_dir(buildpack_package)?; + let target_dir = lookup_target_dir(buildpack_package); match buildpack_package.buildpack_data.buildpack_descriptor { BuildpackDescriptor::Single(_) => { if contains_buildpack_binaries(&buildpack_package.path) { eprintln!("Not a libcnb.rs buildpack, nothing to compile..."); } else { - package_single_buildpack(buildpack_package, &target_dir, args)?; + package_single_buildpack( + buildpack_package, + &target_dir, + cargo_profile, + &cargo_build_env, + &target_triple, + )?; } } BuildpackDescriptor::Meta(_) => { - package_meta_buildpack(buildpack_package, &target_dir, &target_directories_index)?; + package_meta_buildpack( + buildpack_package, + &target_dir, + &buildpack_output_directory_locator, + )?; } } current_count += 1; @@ -115,14 +112,14 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { build_order .into_iter() .map(lookup_target_dir) - .collect::>>()?, + .collect::>(), ); print_requested_buildpack_output_dirs( buildpack_packages_requested .into_iter() .map(lookup_target_dir) - .collect::>>()?, + .collect::>(), ); Ok(()) @@ -131,116 +128,75 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> { fn package_single_buildpack( buildpack_package: &BuildpackPackage, target_dir: &Path, - args: &PackageArgs, + cargo_profile: CargoProfile, + cargo_build_env: &[(OsString, OsString)], + target_triple: &str, ) -> Result<()> { - let cargo_profile = if args.release { - CargoProfile::Release - } else { - CargoProfile::Dev - }; - - let target_triple = &args.target; - - let cargo_metadata = MetadataCommand::new() - .manifest_path(&buildpack_package.path.join("Cargo.toml")) - .exec() - .map_err(|e| Error::ReadCargoMetadata { - path: buildpack_package.path.clone(), - source: e, - })?; - - let cargo_build_env = if args.no_cross_compile_assistance { - vec![] - } else { - eprintln!("Determining automatic cross-compile settings..."); - match cross_compile_assistance(target_triple) { - CrossCompileAssistance::Configuration { cargo_env } => cargo_env, - - CrossCompileAssistance::NoAssistance => { - eprintln!("Could not determine automatic cross-compile settings for target triple {target_triple}."); - eprintln!("This is not an error, but without proper cross-compile settings in your Cargo manifest and locally installed toolchains, compilation might fail."); - eprintln!("To disable this warning, pass --no-cross-compile-assistance."); - vec![] - } - - CrossCompileAssistance::HelpText(help_text) => { - Err(Error::CrossCompilationHelp { message: help_text })? - } - } - }; - eprintln!("Building binaries ({target_triple})..."); - let buildpack_binaries = build_buildpack_binaries( &buildpack_package.path, - &cargo_metadata, cargo_profile, - &cargo_build_env, + cargo_build_env, target_triple, )?; eprintln!("Writing buildpack directory..."); - clean_target_directory(target_dir)?; - - assemble_buildpack_directory( + assemble_single_buildpack_directory( target_dir, &buildpack_package.buildpack_data.buildpack_descriptor_path, + buildpack_package + .buildpackage_data + .as_ref() + .map(|data| &data.buildpackage_descriptor), &buildpack_binaries, - ) - .map_err(|e| Error::AssembleBuildpackDirectory(target_dir.to_path_buf(), e))?; - - let buildpackage_content = - toml::to_string(&Buildpackage::default()).map_err(Error::SerializeBuildpackage)?; - - std::fs::write(target_dir.join("package.toml"), buildpackage_content) - .map_err(|e| Error::WriteBuildpackage(target_dir.to_path_buf(), e))?; - + )?; eprint_compiled_buildpack_success(&buildpack_package.path, target_dir) } fn package_meta_buildpack( buildpack_package: &BuildpackPackage, target_dir: &Path, - target_dirs_by_buildpack_id: &HashMap<&BuildpackId, PathBuf>, + buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, ) -> Result<()> { eprintln!("Writing buildpack directory..."); - clean_target_directory(target_dir)?; - - std::fs::create_dir_all(target_dir) - .map_err(|e| Error::CreateBuildpackTargetDirectory(target_dir.to_path_buf(), e))?; - - std::fs::copy( + assemble_meta_buildpack_directory( + target_dir, + &buildpack_package.path, &buildpack_package.buildpack_data.buildpack_descriptor_path, - target_dir.join("buildpack.toml"), - ) - .map_err(|e| Error::WriteBuildpack(target_dir.to_path_buf(), e))?; - - let buildpackage_content = &buildpack_package - .buildpackage_data - .as_ref() - .map(|buildpackage_data| &buildpackage_data.buildpackage_descriptor) - .ok_or(Error::MissingBuildpackageData) - .and_then(|buildpackage| { - rewrite_buildpackage_local_dependencies(buildpackage, target_dirs_by_buildpack_id) - .map_err(std::convert::Into::into) - }) - .and_then(|buildpackage| { - rewrite_buildpackage_relative_path_dependencies_to_absolute( - &buildpackage, - &buildpack_package.path, - ) - .map_err(std::convert::Into::into) - }) - .and_then(|buildpackage| { - toml::to_string(&buildpackage).map_err(Error::SerializeBuildpackage) - })?; + buildpack_package + .buildpackage_data + .as_ref() + .map(|data| &data.buildpackage_descriptor), + buildpack_output_directory_locator, + )?; + eprint_compiled_buildpack_success(&buildpack_package.path, target_dir) +} - std::fs::write(target_dir.join("package.toml"), buildpackage_content) - .map_err(|e| Error::WriteBuildpackage(target_dir.to_path_buf(), e))?; +fn get_cargo_build_env( + target_triple: &str, + no_cross_compile_assistance: bool, +) -> Result> { + if no_cross_compile_assistance { + Ok(vec![]) + } else { + eprintln!("Determining automatic cross-compile settings..."); + match cross_compile_assistance(target_triple) { + CrossCompileAssistance::Configuration { cargo_env } => Ok(cargo_env), - eprint_compiled_buildpack_success(&buildpack_package.path, target_dir) + CrossCompileAssistance::NoAssistance => { + eprintln!("Could not determine automatic cross-compile settings for target triple {target_triple}."); + eprintln!("This is not an error, but without proper cross-compile settings in your Cargo manifest and locally installed toolchains, compilation might fail."); + eprintln!("To disable this warning, pass --no-cross-compile-assistance."); + Ok(vec![]) + } + + CrossCompileAssistance::HelpText(help_text) => { + Err(Error::CrossCompilationHelp(help_text))? + } + } + } } fn eprint_pack_command_hint(pack_directories: Vec) { @@ -266,27 +222,6 @@ fn print_requested_buildpack_output_dirs(output_directories: Vec) { } } -fn get_cargo_workspace_root(dir: &Path) -> Result { - let cargo_bin = std::env::var("CARGO").map(PathBuf::from)?; - - Command::new(cargo_bin) - .args(["locate-project", "--workspace", "--message-format", "plain"]) - .current_dir(dir) - .output() - .map_err(|e| Error::GetWorkspaceCommand { - path: dir.to_path_buf(), - source: e, - }) - .map(|output| { - let stdout = String::from_utf8_lossy(&output.stdout); - PathBuf::from(stdout.trim()).parent().map(Path::to_path_buf) - }) - .transpose() - .ok_or(Error::GetWorkspaceDirectory { - path: dir.to_path_buf(), - })? -} - fn clean_target_directory(dir: &Path) -> Result<()> { if dir.exists() { std::fs::remove_dir_all(dir) @@ -343,3 +278,11 @@ fn contains_buildpack_binaries(dir: &Path) -> bool { .map(|path| dir.join(path)) .all(|path| path.is_file()) } + +fn get_buildpack_output_dir(workspace_dir: &Path) -> Result { + MetadataCommand::new() + .manifest_path(&workspace_dir.join("Cargo.toml")) + .exec() + .map(|metadata| metadata.target_directory.into_std_path_buf()) + .map_err(|e| Error::GetBuildpackOutputDir(workspace_dir.to_path_buf(), e)) +} diff --git a/libcnb-cargo/src/package/error.rs b/libcnb-cargo/src/package/error.rs index 07939f8e..f93eba51 100644 --- a/libcnb-cargo/src/package/error.rs +++ b/libcnb-cargo/src/package/error.rs @@ -4,88 +4,59 @@ use libcnb_package::buildpack_dependency::{ RewriteBuildpackageLocalDependenciesError, RewriteBuildpackageRelativePathDependenciesToAbsoluteError, }; +use libcnb_package::buildpack_package::ReadBuildpackPackageError; use libcnb_package::dependency_graph::{CreateDependencyGraphError, GetDependenciesError}; +use libcnb_package::output::AssembleBuildpackDirectoryError; +use libcnb_package::{FindCargoWorkspaceError, ReadBuildpackDataError, ReadBuildpackageDataError}; use std::path::PathBuf; +use std::process::ExitStatus; #[derive(Debug, thiserror::Error)] pub(crate) enum Error { #[error("Failed to get current dir\nError: {0}")] GetCurrentDir(std::io::Error), - #[error("Could not locate a Cargo workspace within `{path}` or it's parent directories")] - GetWorkspaceDirectory { path: PathBuf }, + #[error("Could not locate a Cargo workspace within `{0}` or it's parent directories")] + GetWorkspaceDirectory(PathBuf), - #[error("Could not execute `cargo locate-project --workspace --message-format plain in {path}\nError: {source}")] - GetWorkspaceCommand { - path: PathBuf, - source: std::io::Error, - }, + #[error("Could not read Cargo.toml metadata in `{0}`\nError: {1}")] + ReadCargoMetadata(PathBuf, cargo_metadata::Error), - #[error("Could not read Cargo.toml metadata in `{path}`\nError: {source}")] - ReadCargoMetadata { - path: PathBuf, - source: cargo_metadata::Error, - }, - - #[error("Could not determine a target directory for buildpack with id `{buildpack_id}`")] - TargetDirectoryLookup { buildpack_id: BuildpackId }, - - #[error("{message}")] - CrossCompilationHelp { message: String }, + #[error("{0}")] + CrossCompilationHelp(String), #[error("No environment variable named `CARGO` is set")] - GetCargoBin(#[from] std::env::VarError), - - #[error("Meta-buildpack is missing expected package.toml file")] - MissingBuildpackageData, + GetCargoBin(std::env::VarError), #[error("Failed to serialize package.toml\nError: {0}")] SerializeBuildpackage(toml::ser::Error), - #[error("Error while finding buildpack directories\nLocation: {path}\nError: {source}")] - FindBuildpackDirs { - path: PathBuf, - source: std::io::Error, - }, + #[error("Error while finding buildpack directories\nLocation: {0}\nError: {1}")] + FindBuildpackDirs(PathBuf, std::io::Error), #[error("There was a problem with the build configuration")] BinaryConfig, - #[error("I/O error while executing Cargo for target {target}\nError: {source}")] - BinaryBuildExecution { - target: String, - source: std::io::Error, - }, - - #[error("Unexpected Cargo exit status for target {target}\nExit Status: {code}\nExamine Cargo output for details and potential compilation errors.")] - BinaryBuildExitStatus { target: String, code: String }, - - #[error("Configured buildpack target name {target} could not be found!")] - BinaryBuildMissingTarget { target: String }, - - #[error("Failed to read buildpack data\nLocation: {path}\nError: {source}")] - ReadBuildpackData { - path: PathBuf, - source: std::io::Error, - }, - - #[error("Failed to parse buildpack data\nLocation: {path}\nError: {source}")] - ParseBuildpackData { - path: PathBuf, - source: toml::de::Error, - }, - - #[error("Failed to read buildpackage data\nLocation: {path}\nError: {source}")] - ReadBuildpackageData { - path: PathBuf, - source: std::io::Error, - }, - - #[error("Failed to parse buildpackage data\nLocation: {path}\nError: {source}")] - ParseBuildpackageData { - path: PathBuf, - source: toml::de::Error, - }, + #[error("I/O error while executing Cargo for target {0}\nError: {1}")] + BinaryBuildExecution(String, std::io::Error), + + #[error("Unexpected Cargo exit status for target {0}\nExit Status: {1}\nExamine Cargo output for details and potential compilation errors.")] + BinaryBuildExitStatus(String, String), + + #[error("Configured buildpack target name {0} could not be found!")] + BinaryBuildMissingTarget(String), + + #[error("Failed to read buildpack data\nLocation: {0}\nError: {1}")] + ReadBuildpackData(PathBuf, std::io::Error), + + #[error("Failed to parse buildpack data\nLocation: {0}\nError: {1}")] + ParseBuildpackData(PathBuf, toml::de::Error), + + #[error("Failed to read buildpackage data\nLocation: {0}\nError: {1}")] + ReadBuildpackageData(PathBuf, std::io::Error), + + #[error("Failed to parse buildpackage data\nLocation: {0}\nError: {1}")] + ParseBuildpackageData(PathBuf, toml::de::Error), #[error("Failed to lookup buildpack dependency with id `{0}`")] BuildpackDependencyLookup(BuildpackId), @@ -108,17 +79,11 @@ pub(crate) enum Error { #[error("No buildpacks found!")] NoBuildpacksFound, - #[error("Could not assemble buildpack directory\nPath: {0}\nError: {1}")] - AssembleBuildpackDirectory(PathBuf, std::io::Error), - #[error( "Failed to write package.toml to the target buildpack directory\nPath: {0}\nError: {1}" )] WriteBuildpackage(PathBuf, std::io::Error), - #[error("I/O error while creating target buildpack directory\nPath: {0}\nError: {1}")] - CreateBuildpackTargetDirectory(PathBuf, std::io::Error), - #[error( "Failed to write buildpack.toml to the target buildpack directory\nPath: {0}\nError: {1}" )] @@ -129,6 +94,33 @@ pub(crate) enum Error { #[error("I/O error while calculating directory size\nPath: {0}\nError: {1}")] CalculateDirectorySize(PathBuf, std::io::Error), + + #[error("Could not read Cargo.toml metadata from workspace\nPath: {0}\nError: {1}")] + GetBuildpackOutputDir(PathBuf, cargo_metadata::Error), + + #[error("Failed to spawn Cargo command\nError: {0}")] + SpawnCargoCommand(std::io::Error), + + #[error("Unexpected Cargo exit status while attempting to read workspace root\nExit Status: {0}\nExamine Cargo output for details and potential compilation errors.")] + CargoCommandFailure(String), + + #[error("Could not create buildpack directory\nPath: {0}\nError: {1}")] + CreateBuildpackDestinationDirectory(PathBuf, std::io::Error), + + #[error("Could not create buildpack bin directory\nPath: {0}\nError: {1}")] + CreateBinDirectory(PathBuf, std::io::Error), + + #[error("Could not write `build` binary to destination\nPath: {0}\nError: {1}")] + WriteBuildBinary(PathBuf, std::io::Error), + + #[error("Could not write `detect` binary to destination\nPath: {0}\nError: {1}")] + WriteDetectBinary(PathBuf, std::io::Error), + + #[error("Could not create buildpack additional binary directory\nPath: {0}\nError: {1}")] + CreateAdditionalBinariesDirectory(PathBuf, std::io::Error), + + #[error("Could not write additional binary to destination\nPath: {0}\nError: {1}")] + WriteAdditionalBinary(PathBuf, std::io::Error), } impl From for Error { @@ -137,49 +129,20 @@ impl From for Error { BuildBinariesError::ConfigError(_) => Error::BinaryConfig, BuildBinariesError::BuildError(target, BuildError::IoError(source)) => { - Error::BinaryBuildExecution { target, source } + Error::BinaryBuildExecution(target, source) } BuildBinariesError::BuildError( target, BuildError::UnexpectedCargoExitStatus(exit_status), - ) => Error::BinaryBuildExitStatus { - target, - code: exit_status - .code() - .map_or_else(|| String::from(""), |code| code.to_string()), - }, + ) => Error::BinaryBuildExitStatus(target, exit_status_or_unknown(exit_status)), BuildBinariesError::MissingBuildpackTarget(target) => { - Error::BinaryBuildMissingTarget { target } + Error::BinaryBuildMissingTarget(target) } - } - } -} -impl From for Error { - fn from(value: libcnb_package::buildpack_package::ReadBuildpackPackageError) -> Self { - match value { - libcnb_package::buildpack_package::ReadBuildpackPackageError::ReadBuildpackDataError(error) => match error - { - libcnb_package::ReadBuildpackDataError::ReadingBuildpack { path, source } => { - Error::ReadBuildpackData { path, source } - } - libcnb_package::ReadBuildpackDataError::ParsingBuildpack { path, source } => { - Error::ParseBuildpackData { path, source } - } - }, - libcnb_package::buildpack_package::ReadBuildpackPackageError::ReadBuildpackageDataError(error) => { - match error { - libcnb_package::ReadBuildpackageDataError::ReadingBuildpackage { - path, - source, - } => Error::ReadBuildpackageData { path, source }, - libcnb_package::ReadBuildpackageDataError::ParsingBuildpackage { - path, - source, - } => Error::ParseBuildpackageData { path, source }, - } + BuildBinariesError::ReadCargoMetadata(path, error) => { + Error::ReadCargoMetadata(path, error) } } } @@ -228,3 +191,93 @@ impl From for Error } } } + +impl From for Error { + fn from(value: ReadBuildpackPackageError) -> Self { + match value { + ReadBuildpackPackageError::ReadBuildpackDataError(error) => error.into(), + ReadBuildpackPackageError::ReadBuildpackageDataError(error) => error.into(), + } + } +} + +impl From for Error { + fn from(value: ReadBuildpackDataError) -> Self { + match value { + ReadBuildpackDataError::ReadingBuildpack(path, source) => { + Error::ReadBuildpackData(path, source) + } + ReadBuildpackDataError::ParsingBuildpack(path, source) => { + Error::ParseBuildpackData(path, source) + } + } + } +} + +impl From for Error { + fn from(value: ReadBuildpackageDataError) -> Self { + match value { + ReadBuildpackageDataError::ReadingBuildpackage(path, source) => { + Error::ReadBuildpackageData(path, source) + } + ReadBuildpackageDataError::ParsingBuildpackage(path, source) => { + Error::ParseBuildpackageData(path, source) + } + } + } +} + +impl From for Error { + fn from(value: FindCargoWorkspaceError) -> Self { + match value { + FindCargoWorkspaceError::GetCargoEnv(error) => Error::GetCargoBin(error), + FindCargoWorkspaceError::SpawnCommand(error) => Error::SpawnCargoCommand(error), + FindCargoWorkspaceError::CommandFailure(exit_status) => { + Error::CargoCommandFailure(exit_status_or_unknown(exit_status)) + } + FindCargoWorkspaceError::GetParentDirectory(path) => Error::GetWorkspaceDirectory(path), + } + } +} + +impl From for Error { + fn from(value: AssembleBuildpackDirectoryError) -> Self { + match value { + AssembleBuildpackDirectoryError::CreateBuildpackDestinationDirectory(path, error) => { + Error::CreateBuildpackDestinationDirectory(path, error) + } + AssembleBuildpackDirectoryError::WriteBuildpack(path, error) => { + Error::WriteBuildpack(path, error) + } + AssembleBuildpackDirectoryError::SerializeBuildpackage(error) => { + Error::SerializeBuildpackage(error) + } + AssembleBuildpackDirectoryError::WriteBuildpackage(path, error) => { + Error::WriteBuildpackage(path, error) + } + AssembleBuildpackDirectoryError::CreateBinDirectory(path, error) => { + Error::CreateBinDirectory(path, error) + } + AssembleBuildpackDirectoryError::WriteBuildBinary(path, error) => { + Error::WriteBuildBinary(path, error) + } + AssembleBuildpackDirectoryError::WriteDetectBinary(path, error) => { + Error::WriteDetectBinary(path, error) + } + AssembleBuildpackDirectoryError::CreateAdditionalBinariesDirectory(path, error) => { + Error::CreateAdditionalBinariesDirectory(path, error) + } + AssembleBuildpackDirectoryError::WriteAdditionalBinary(path, error) => { + Error::WriteAdditionalBinary(path, error) + } + AssembleBuildpackDirectoryError::RewriteLocalDependencies(error) => error.into(), + AssembleBuildpackDirectoryError::RewriteRelativePathDependencies(error) => error.into(), + } + } +} + +fn exit_status_or_unknown(exit_status: ExitStatus) -> String { + exit_status + .code() + .map_or_else(|| String::from(""), |code| code.to_string()) +} diff --git a/libcnb-cargo/tests/test.rs b/libcnb-cargo/tests/test.rs index 378f3ce6..464eace1 100644 --- a/libcnb-cargo/tests/test.rs +++ b/libcnb-cargo/tests/test.rs @@ -1,7 +1,8 @@ use fs_extra::dir::{copy, CopyOptions}; use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId}; use libcnb_data::buildpack_id; -use libcnb_package::{get_buildpack_target_dir, read_buildpack_data, read_buildpackage_data}; +use libcnb_package::output::BuildpackOutputDirectoryLocator; +use libcnb_package::{read_buildpack_data, read_buildpackage_data, CargoProfile}; use std::env; use std::io::Read; use std::path::PathBuf; @@ -82,6 +83,7 @@ fn package_all_buildpacks_in_monorepo_buildpack_project() { .to_string_lossy() .to_string(), packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/meta-one")), + packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/meta-two")), packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/one")), packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/two")), ] @@ -103,6 +105,15 @@ fn package_all_buildpacks_in_monorepo_buildpack_project() { String::from("docker://docker.io/heroku/procfile-cnb:2.0.0"), ], ); + assert_compiled_meta_buildpack( + &packaging_test, + buildpack_id!("multiple-buildpacks/meta-two"), + vec![ + packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/one")), + packaging_test.target_dir_name(buildpack_id!("multiple-buildpacks/two")), + String::from("docker://docker.io/heroku/procfile-cnb:2.0.0"), + ], + ); } #[test] @@ -139,7 +150,7 @@ fn package_command_error_when_run_in_project_with_no_buildpacks() { assert_ne!(output.code, Some(0)); assert_eq!( output.stderr, - "šŸ” Locating buildpacks...\nāŒ No buildpacks found!\n" + "Determining automatic cross-compile settings...\nšŸ” Locating buildpacks...\nāŒ No buildpacks found!\n" ); } @@ -237,12 +248,18 @@ impl BuildpackPackagingTest { } fn target_dir(&self, buildpack_id: BuildpackId) -> PathBuf { - get_buildpack_target_dir( - &buildpack_id, - &self.dir().join("target"), - self.release_build, - &self.target_triple, - ) + let root_dir = self.dir().join("target"); + let cargo_profile = if self.release_build { + CargoProfile::Release + } else { + CargoProfile::Dev + }; + let locator = BuildpackOutputDirectoryLocator::new( + root_dir, + cargo_profile, + self.target_triple.clone(), + ); + locator.get(&buildpack_id) } fn run_libcnb_package(&self) -> TestOutput { diff --git a/libcnb-package/src/build.rs b/libcnb-package/src/build.rs index 530582a2..3c77ef9a 100644 --- a/libcnb-package/src/build.rs +++ b/libcnb-package/src/build.rs @@ -1,6 +1,6 @@ use crate::config::{config_from_metadata, ConfigError}; use crate::CargoProfile; -use cargo_metadata::Metadata; +use cargo_metadata::{Metadata, MetadataCommand}; use std::collections::HashMap; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -19,18 +19,25 @@ use std::process::{Command, ExitStatus}; /// read or the configured main buildpack binary does not exist. pub fn build_buildpack_binaries( project_path: impl AsRef, - cargo_metadata: &Metadata, cargo_profile: CargoProfile, cargo_env: &[(OsString, OsString)], target_triple: impl AsRef, ) -> Result { - let binary_target_names = binary_target_names(cargo_metadata); - let config = config_from_metadata(cargo_metadata).map_err(BuildBinariesError::ConfigError)?; + let cargo_path = project_path.as_ref().join("Cargo.toml"); + + let cargo_metadata = MetadataCommand::new() + .manifest_path(cargo_path.clone()) + .exec() + .map_err(|e| BuildBinariesError::ReadCargoMetadata(cargo_path, e))?; + + let binary_target_names = binary_target_names(&cargo_metadata); + + let config = config_from_metadata(&cargo_metadata).map_err(BuildBinariesError::ConfigError)?; let buildpack_target_binary_path = if binary_target_names.contains(&config.buildpack_target) { build_binary( project_path.as_ref(), - cargo_metadata, + &cargo_metadata, cargo_profile, cargo_env.to_owned(), target_triple.as_ref(), @@ -52,7 +59,7 @@ pub fn build_buildpack_binaries( additional_binary_target_name.clone(), build_binary( project_path.as_ref(), - cargo_metadata, + &cargo_metadata, cargo_profile, cargo_env.to_owned(), target_triple.as_ref(), @@ -169,6 +176,7 @@ pub enum BuildError { #[derive(Debug)] pub enum BuildBinariesError { + ReadCargoMetadata(PathBuf, cargo_metadata::Error), ConfigError(ConfigError), BuildError(String, BuildError), MissingBuildpackTarget(String), diff --git a/libcnb-package/src/buildpack_dependency.rs b/libcnb-package/src/buildpack_dependency.rs index bd85aa8c..fe40581b 100644 --- a/libcnb-package/src/buildpack_dependency.rs +++ b/libcnb-package/src/buildpack_dependency.rs @@ -1,7 +1,6 @@ +use crate::output::BuildpackOutputDirectoryLocator; use libcnb_data::buildpack::{BuildpackId, BuildpackIdError}; use libcnb_data::buildpackage::{Buildpackage, BuildpackageDependency}; -use std::collections::HashMap; -use std::hash::BuildHasher; use std::path::{Path, PathBuf}; /// Buildpack dependency type @@ -80,9 +79,9 @@ pub fn get_local_buildpackage_dependencies( /// * the given `buildpackage` contains a local dependency with an invalid [`BuildpackId`] /// * there is no entry found in `buildpack_ids_to_target_dir` for a local dependency's [`BuildpackId`] /// * the target path for a local dependency is an invalid URI -pub fn rewrite_buildpackage_local_dependencies( +pub fn rewrite_buildpackage_local_dependencies( buildpackage: &Buildpackage, - buildpack_ids_to_target_dir: &HashMap<&BuildpackId, PathBuf, S>, + buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, ) -> Result { let local_dependency_to_target_dir = |target_dir: &PathBuf| { BuildpackageDependency::try_from(target_dir.clone()).map_err(|_| { @@ -99,14 +98,10 @@ pub fn rewrite_buildpackage_local_dependencies( BuildpackDependency::External(buildpackage_dependency) => { Ok(buildpackage_dependency) } - BuildpackDependency::Local(buildpack_id, _) => buildpack_ids_to_target_dir - .get(&buildpack_id) - .ok_or( - RewriteBuildpackageLocalDependenciesError::TargetDirectoryLookup( - buildpack_id, - ), - ) - .and_then(local_dependency_to_target_dir), + BuildpackDependency::Local(buildpack_id, _) => { + let output_dir = buildpack_output_directory_locator.get(&buildpack_id); + local_dependency_to_target_dir(&output_dir) + } }) .collect() }) @@ -186,11 +181,12 @@ mod tests { get_local_buildpackage_dependencies, rewrite_buildpackage_local_dependencies, rewrite_buildpackage_relative_path_dependencies_to_absolute, }; + use crate::output::BuildpackOutputDirectoryLocator; + use crate::CargoProfile; use libcnb_data::buildpack_id; use libcnb_data::buildpackage::{ Buildpackage, BuildpackageBuildpackReference, BuildpackageDependency, Platform, }; - use std::collections::HashMap; use std::path::PathBuf; #[test] @@ -209,17 +205,19 @@ mod tests { #[test] fn test_rewrite_buildpackage_local_dependencies() { let buildpackage = create_buildpackage(); - let buildpack_id = buildpack_id!("buildpack-id"); - let buildpack_ids_to_target_dir = HashMap::from([( - &buildpack_id, - PathBuf::from("/path/to/target/buildpacks/buildpack-id"), - )]); - let new_buildpackage = - rewrite_buildpackage_local_dependencies(&buildpackage, &buildpack_ids_to_target_dir) - .unwrap(); + let buildpack_output_directory_locator = BuildpackOutputDirectoryLocator::new( + PathBuf::from("/path/to/target"), + CargoProfile::Dev, + "arch".to_string(), + ); + let new_buildpackage = rewrite_buildpackage_local_dependencies( + &buildpackage, + &buildpack_output_directory_locator, + ) + .unwrap(); assert_eq!( new_buildpackage.dependencies[0].uri.to_string(), - "/path/to/target/buildpacks/buildpack-id" + "/path/to/target/buildpack/arch/debug/buildpack-id" ); } diff --git a/libcnb-package/src/lib.rs b/libcnb-package/src/lib.rs index aafbe88b..ca0c471b 100644 --- a/libcnb-package/src/lib.rs +++ b/libcnb-package/src/lib.rs @@ -10,12 +10,13 @@ pub mod buildpack_package; pub mod config; pub mod cross_compile; pub mod dependency_graph; +pub mod output; -use crate::build::BuildpackBinaries; -use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId}; +use libcnb_data::buildpack::BuildpackDescriptor; use libcnb_data::buildpackage::Buildpackage; use std::fs; use std::path::{Path, PathBuf}; +use std::process::Command; use toml::Table; /// The profile to use when invoking Cargo. @@ -50,14 +51,10 @@ pub fn read_buildpack_data( let dir = project_path.as_ref(); let buildpack_descriptor_path = dir.join("buildpack.toml"); fs::read_to_string(&buildpack_descriptor_path) - .map_err(|e| ReadBuildpackDataError::ReadingBuildpack { - path: buildpack_descriptor_path.clone(), - source: e, - }) + .map_err(|e| ReadBuildpackDataError::ReadingBuildpack(buildpack_descriptor_path.clone(), e)) .and_then(|file_contents| { - toml::from_str(&file_contents).map_err(|e| ReadBuildpackDataError::ParsingBuildpack { - path: buildpack_descriptor_path.clone(), - source: e, + toml::from_str(&file_contents).map_err(|e| { + ReadBuildpackDataError::ParsingBuildpack(buildpack_descriptor_path.clone(), e) }) }) .map(|buildpack_descriptor| BuildpackData { @@ -69,14 +66,8 @@ pub fn read_buildpack_data( /// An error from [`read_buildpack_data`] #[derive(Debug)] pub enum ReadBuildpackDataError { - ReadingBuildpack { - path: PathBuf, - source: std::io::Error, - }, - ParsingBuildpack { - path: PathBuf, - source: toml::de::Error, - }, + ReadingBuildpack(PathBuf, std::io::Error), + ParsingBuildpack(PathBuf, toml::de::Error), } /// A parsed buildpackage descriptor and it's path. @@ -101,16 +92,15 @@ pub fn read_buildpackage_data( } fs::read_to_string(&buildpackage_descriptor_path) - .map_err(|e| ReadBuildpackageDataError::ReadingBuildpackage { - path: buildpackage_descriptor_path.clone(), - source: e, + .map_err(|e| { + ReadBuildpackageDataError::ReadingBuildpackage(buildpackage_descriptor_path.clone(), e) }) .and_then(|file_contents| { toml::from_str(&file_contents).map_err(|e| { - ReadBuildpackageDataError::ParsingBuildpackage { - path: buildpackage_descriptor_path.clone(), - source: e, - } + ReadBuildpackageDataError::ParsingBuildpackage( + buildpackage_descriptor_path.clone(), + e, + ) }) }) .map(|buildpackage_descriptor| { @@ -124,93 +114,8 @@ pub fn read_buildpackage_data( /// An error from [`read_buildpackage_data`] #[derive(Debug)] pub enum ReadBuildpackageDataError { - ReadingBuildpackage { - path: PathBuf, - source: std::io::Error, - }, - ParsingBuildpackage { - path: PathBuf, - source: toml::de::Error, - }, -} - -/// Creates a buildpack directory and copies all buildpack assets to it. -/// -/// Assembly of the directory follows the constraints set by the libcnb framework. For example, -/// the buildpack binary is only copied once and symlinks are used to refer to it when the CNB -/// spec requires different file(name)s. -/// -/// This function will not validate if the buildpack descriptor at the given path is valid and will -/// use it as-is. -/// -/// # Errors -/// -/// Will return `Err` if the buildpack directory could not be assembled. -pub fn assemble_buildpack_directory( - destination_path: impl AsRef, - buildpack_descriptor_path: impl AsRef, - buildpack_binaries: &BuildpackBinaries, -) -> std::io::Result<()> { - fs::create_dir_all(destination_path.as_ref())?; - - fs::copy( - buildpack_descriptor_path.as_ref(), - destination_path.as_ref().join("buildpack.toml"), - )?; - - let bin_path = destination_path.as_ref().join("bin"); - fs::create_dir_all(&bin_path)?; - - fs::copy( - &buildpack_binaries.buildpack_target_binary_path, - bin_path.join("build"), - )?; - - create_file_symlink("build", bin_path.join("detect"))?; - - if !buildpack_binaries.additional_target_binary_paths.is_empty() { - let additional_binaries_dir = destination_path - .as_ref() - .join(".libcnb-cargo") - .join("additional-bin"); - - fs::create_dir_all(&additional_binaries_dir)?; - - for (binary_target_name, binary_path) in &buildpack_binaries.additional_target_binary_paths - { - fs::copy( - binary_path, - additional_binaries_dir.join(binary_target_name), - )?; - } - } - - Ok(()) -} - -#[cfg(target_family = "unix")] -fn create_file_symlink, Q: AsRef>( - original: P, - link: Q, -) -> std::io::Result<()> { - std::os::unix::fs::symlink(original.as_ref(), link.as_ref()) -} - -#[cfg(target_family = "windows")] -fn create_file_symlink, Q: AsRef>( - original: P, - link: Q, -) -> std::io::Result<()> { - std::os::windows::fs::symlink_file(original.as_ref(), link.as_ref()) -} - -/// Construct a good default filename for a buildpack directory. -/// -/// This function ensures the resulting name is valid and does not contain problematic characters -/// such as `/`. -#[must_use] -pub fn default_buildpack_directory_name(buildpack_id: &BuildpackId) -> String { - buildpack_id.replace('/', "_") + ReadingBuildpackage(PathBuf, std::io::Error), + ParsingBuildpackage(PathBuf, toml::de::Error), } /// Recursively walks the file system from the given `start_dir` to locate any folders containing a @@ -253,44 +158,43 @@ pub fn find_buildpack_dirs(start_dir: &Path, ignore: &[PathBuf]) -> std::io::Res Ok(buildpack_dirs) } -/// Provides a standard path to use for storing a compiled buildpack's artifacts. -#[must_use] -pub fn get_buildpack_target_dir( - buildpack_id: &BuildpackId, - target_dir: &Path, - is_release: bool, - target_triple: &str, -) -> PathBuf { - target_dir - .join("buildpack") - .join(target_triple) - .join(if is_release { "release" } else { "debug" }) - .join(default_buildpack_directory_name(buildpack_id)) +/// Returns the path of the root workspace directory for a Rust Cargo project. This is often a useful +/// starting point for detecting buildpacks with [`find_buildpack_dirs`]. +/// +/// ## Errors +/// +/// Will return an `Err` if the root workspace directory cannot be located. +pub fn find_cargo_workspace(dir_in_workspace: &Path) -> Result { + let cargo_bin = std::env::var("CARGO") + .map(PathBuf::from) + .map_err(FindCargoWorkspaceError::GetCargoEnv)?; + + let output = Command::new(cargo_bin) + .args(["locate-project", "--workspace", "--message-format", "plain"]) + .current_dir(dir_in_workspace) + .output() + .map_err(FindCargoWorkspaceError::SpawnCommand)?; + + let status = output.status; + + output + .status + .success() + .then_some(output) + .ok_or(FindCargoWorkspaceError::CommandFailure(status)) + .and_then(|output| { + let root_cargo_toml = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()); + root_cargo_toml + .parent() + .map(Path::to_path_buf) + .ok_or(FindCargoWorkspaceError::GetParentDirectory(root_cargo_toml)) + }) } -#[cfg(test)] -mod tests { - use crate::get_buildpack_target_dir; - use libcnb_data::buildpack_id; - use std::path::PathBuf; - - #[test] - fn test_get_buildpack_target_dir() { - let buildpack_id = buildpack_id!("some-org/with-buildpack"); - let target_dir = PathBuf::from("/target"); - let target_triple = "x86_64-unknown-linux-musl"; - - assert_eq!( - get_buildpack_target_dir(&buildpack_id, &target_dir, false, target_triple), - PathBuf::from( - "/target/buildpack/x86_64-unknown-linux-musl/debug/some-org_with-buildpack" - ) - ); - assert_eq!( - get_buildpack_target_dir(&buildpack_id, &target_dir, true, target_triple), - PathBuf::from( - "/target/buildpack/x86_64-unknown-linux-musl/release/some-org_with-buildpack" - ) - ); - } +#[derive(Debug)] +pub enum FindCargoWorkspaceError { + GetCargoEnv(std::env::VarError), + SpawnCommand(std::io::Error), + CommandFailure(std::process::ExitStatus), + GetParentDirectory(PathBuf), } diff --git a/libcnb-package/src/output.rs b/libcnb-package/src/output.rs new file mode 100644 index 00000000..321b478a --- /dev/null +++ b/libcnb-package/src/output.rs @@ -0,0 +1,261 @@ +use crate::build::BuildpackBinaries; +use crate::buildpack_dependency::{ + rewrite_buildpackage_local_dependencies, + rewrite_buildpackage_relative_path_dependencies_to_absolute, + RewriteBuildpackageLocalDependenciesError, + RewriteBuildpackageRelativePathDependenciesToAbsoluteError, +}; +use crate::CargoProfile; +use libcnb_data::buildpack::BuildpackId; +use libcnb_data::buildpackage::Buildpackage; +use std::fs; +use std::path::{Path, PathBuf}; + +pub struct BuildpackOutputDirectoryLocator { + root_dir: PathBuf, + cargo_profile: CargoProfile, + target_triple: String, +} + +impl BuildpackOutputDirectoryLocator { + #[must_use] + pub fn new(root_dir: PathBuf, cargo_profile: CargoProfile, target_triple: String) -> Self { + Self { + root_dir, + cargo_profile, + target_triple, + } + } + + #[must_use] + pub fn get(&self, buildpack_id: &BuildpackId) -> PathBuf { + self.root_dir + .join("buildpack") + .join(&self.target_triple) + .join(match self.cargo_profile { + CargoProfile::Dev => "debug", + CargoProfile::Release => "release", + }) + .join(default_buildpack_directory_name(buildpack_id)) + } +} + +/// Construct a good default filename for a buildpack directory. +/// +/// This function ensures the resulting name is valid and does not contain problematic characters +/// such as `/`. +#[must_use] +pub fn default_buildpack_directory_name(buildpack_id: &BuildpackId) -> String { + buildpack_id.replace('/', "_") +} + +#[derive(Debug)] +pub enum AssembleBuildpackDirectoryError { + CreateBuildpackDestinationDirectory(PathBuf, std::io::Error), + WriteBuildpack(PathBuf, std::io::Error), + SerializeBuildpackage(toml::ser::Error), + WriteBuildpackage(PathBuf, std::io::Error), + CreateBinDirectory(PathBuf, std::io::Error), + WriteBuildBinary(PathBuf, std::io::Error), + WriteDetectBinary(PathBuf, std::io::Error), + CreateAdditionalBinariesDirectory(PathBuf, std::io::Error), + WriteAdditionalBinary(PathBuf, std::io::Error), + RewriteLocalDependencies(RewriteBuildpackageLocalDependenciesError), + RewriteRelativePathDependencies(RewriteBuildpackageRelativePathDependenciesToAbsoluteError), +} + +/// Creates a buildpack directory and copies all buildpack assets to it. +/// +/// Assembly of the directory follows the constraints set by the libcnb framework. For example, +/// the buildpack binary is only copied once and symlinks are used to refer to it when the CNB +/// spec requires different file(name)s. +/// +/// This function will not validate if the buildpack descriptor at the given path is valid and will +/// use it as-is. +/// +/// # Errors +/// +/// Will return `Err` if the buildpack directory could not be assembled. +pub fn assemble_single_buildpack_directory( + destination_path: impl AsRef, + buildpack_path: impl AsRef, + buildpackage: Option<&Buildpackage>, + buildpack_binaries: &BuildpackBinaries, +) -> Result<(), AssembleBuildpackDirectoryError> { + fs::create_dir_all(destination_path.as_ref()).map_err(|e| { + AssembleBuildpackDirectoryError::CreateBuildpackDestinationDirectory( + destination_path.as_ref().to_path_buf(), + e, + ) + })?; + + let destination_path = destination_path.as_ref(); + + fs::copy( + buildpack_path.as_ref(), + destination_path.join("buildpack.toml"), + ) + .map_err(|e| { + AssembleBuildpackDirectoryError::WriteBuildpack(destination_path.join("buildpack.toml"), e) + })?; + + let default_buildpackage = Buildpackage::default(); + let buildpackage_content = toml::to_string(buildpackage.unwrap_or(&default_buildpackage)) + .map_err(AssembleBuildpackDirectoryError::SerializeBuildpackage)?; + + fs::write(destination_path.join("package.toml"), buildpackage_content).map_err(|e| { + AssembleBuildpackDirectoryError::WriteBuildpackage(destination_path.join("package.toml"), e) + })?; + + let bin_path = destination_path.join("bin"); + fs::create_dir_all(&bin_path) + .map_err(|e| AssembleBuildpackDirectoryError::CreateBinDirectory(bin_path.clone(), e))?; + + fs::copy( + &buildpack_binaries.buildpack_target_binary_path, + bin_path.join("build"), + ) + .map_err(|e| AssembleBuildpackDirectoryError::WriteBuildBinary(bin_path.join("build"), e))?; + + create_file_symlink("build", bin_path.join("detect")).map_err(|e| { + AssembleBuildpackDirectoryError::WriteDetectBinary(bin_path.join("detect"), e) + })?; + + if !buildpack_binaries.additional_target_binary_paths.is_empty() { + let additional_binaries_dir = destination_path + .join(".libcnb-cargo") + .join("additional-bin"); + + fs::create_dir_all(&additional_binaries_dir).map_err(|e| { + AssembleBuildpackDirectoryError::CreateAdditionalBinariesDirectory( + additional_binaries_dir.clone(), + e, + ) + })?; + + for (binary_target_name, binary_path) in &buildpack_binaries.additional_target_binary_paths + { + fs::copy( + binary_path, + additional_binaries_dir.join(binary_target_name), + ) + .map_err(|e| { + AssembleBuildpackDirectoryError::WriteAdditionalBinary( + additional_binaries_dir.join(binary_target_name), + e, + ) + })?; + } + } + + Ok(()) +} + +/// Creates a meta-buildpack directory and copies all required meta-buildpack assets to it. +/// +/// This function will not validate if the buildpack descriptor at the given path is valid and will +/// use it as-is. +/// +/// It will also rewrite all package.toml references that use the `libcnb:{buildpack_id}` format as +/// well as relative file references to use absolute paths. +/// +/// # Errors +/// +/// Will return `Err` if the meta-buildpack directory could not be assembled. +pub fn assemble_meta_buildpack_directory( + destination_path: impl AsRef, + buildpack_source_dir: impl AsRef, + buildpack_path: impl AsRef, + buildpackage: Option<&Buildpackage>, + buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, +) -> Result<(), AssembleBuildpackDirectoryError> { + let default_buildpackage = Buildpackage::default(); + let buildpackage = rewrite_buildpackage_local_dependencies( + buildpackage.unwrap_or(&default_buildpackage), + buildpack_output_directory_locator, + ) + .map_err(AssembleBuildpackDirectoryError::RewriteLocalDependencies) + .and_then(|buildpackage| { + rewrite_buildpackage_relative_path_dependencies_to_absolute( + &buildpackage, + buildpack_source_dir.as_ref(), + ) + .map_err(AssembleBuildpackDirectoryError::RewriteRelativePathDependencies) + })?; + + fs::create_dir_all(destination_path.as_ref()).map_err(|e| { + AssembleBuildpackDirectoryError::CreateBuildpackDestinationDirectory( + destination_path.as_ref().to_path_buf(), + e, + ) + })?; + + let destination_path = destination_path.as_ref(); + + fs::copy( + buildpack_path.as_ref(), + destination_path.join("buildpack.toml"), + ) + .map_err(|e| { + AssembleBuildpackDirectoryError::WriteBuildpack(destination_path.join("buildpack.toml"), e) + })?; + + let buildpackage_content = toml::to_string(&buildpackage) + .map_err(AssembleBuildpackDirectoryError::SerializeBuildpackage)?; + + fs::write(destination_path.join("package.toml"), buildpackage_content).map_err(|e| { + AssembleBuildpackDirectoryError::WriteBuildpackage(destination_path.join("package.toml"), e) + }) +} + +#[cfg(target_family = "unix")] +fn create_file_symlink, Q: AsRef>( + original: P, + link: Q, +) -> std::io::Result<()> { + std::os::unix::fs::symlink(original.as_ref(), link.as_ref()) +} + +#[cfg(target_family = "windows")] +fn create_file_symlink, Q: AsRef>( + original: P, + link: Q, +) -> std::io::Result<()> { + std::os::windows::fs::symlink_file(original.as_ref(), link.as_ref()) +} + +#[cfg(test)] +mod tests { + use crate::output::BuildpackOutputDirectoryLocator; + use crate::CargoProfile; + use libcnb_data::buildpack_id; + use std::path::PathBuf; + + #[test] + fn test_get_buildpack_output_directory_locator() { + let buildpack_id = buildpack_id!("some-org/with-buildpack"); + + assert_eq!( + BuildpackOutputDirectoryLocator { + cargo_profile: CargoProfile::Dev, + target_triple: "x86_64-unknown-linux-musl".to_string(), + root_dir: PathBuf::from("/target") + } + .get(&buildpack_id), + PathBuf::from( + "/target/buildpack/x86_64-unknown-linux-musl/debug/some-org_with-buildpack" + ) + ); + assert_eq!( + BuildpackOutputDirectoryLocator { + cargo_profile: CargoProfile::Release, + target_triple: "x86_64-unknown-linux-musl".to_string(), + root_dir: PathBuf::from("/target") + } + .get(&buildpack_id), + PathBuf::from( + "/target/buildpack/x86_64-unknown-linux-musl/release/some-org_with-buildpack" + ) + ); + } +} diff --git a/libcnb-test/Cargo.toml b/libcnb-test/Cargo.toml index b0e19a03..b4cd6b72 100644 --- a/libcnb-test/Cargo.toml +++ b/libcnb-test/Cargo.toml @@ -13,7 +13,6 @@ include = ["src/**/*", "LICENSE", "README.md"] [dependencies] bollard = "0.14.0" -cargo_metadata = "0.15.4" fastrand = "2.0.0" fs_extra = "1.3.0" libcnb-data.workspace = true diff --git a/libcnb-test/src/build.rs b/libcnb-test/src/build.rs index bfde88c9..f2ee2515 100644 --- a/libcnb-test/src/build.rs +++ b/libcnb-test/src/build.rs @@ -1,27 +1,56 @@ -use cargo_metadata::MetadataCommand; +use crate::pack; +use crate::pack::PackBuildpackPackageCommand; +use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId, BuildpackIdError}; use libcnb_package::build::{build_buildpack_binaries, BuildBinariesError}; +use libcnb_package::buildpack_package::{ + read_buildpack_package, BuildpackPackage, ReadBuildpackPackageError, +}; use libcnb_package::cross_compile::{cross_compile_assistance, CrossCompileAssistance}; -use libcnb_package::{assemble_buildpack_directory, CargoProfile}; -use std::path::PathBuf; -use tempfile::{tempdir, TempDir}; +use libcnb_package::dependency_graph::{ + create_dependency_graph, get_dependencies, CreateDependencyGraphError, DependencyNode, + GetDependenciesError, +}; +use libcnb_package::output::{ + assemble_meta_buildpack_directory, assemble_single_buildpack_directory, + AssembleBuildpackDirectoryError, BuildpackOutputDirectoryLocator, +}; +use libcnb_package::{ + find_buildpack_dirs, find_cargo_workspace, CargoProfile, FindCargoWorkspaceError, + ReadBuildpackDataError, ReadBuildpackageDataError, +}; +use std::env::current_dir; +use std::ffi::OsString; +use std::iter::repeat_with; +use std::path::{Path, PathBuf}; +use tempfile::tempdir; /// Packages the current crate as a buildpack into a temporary directory. pub(crate) fn package_crate_buildpack( cargo_profile: CargoProfile, target_triple: impl AsRef, -) -> Result { +) -> Result { let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR") .map(PathBuf::from) - .map_err(PackageCrateBuildpackError::CannotDetermineCrateDirectory)?; + .map_err(PackageBuildpackError::CannotDetermineCrateDirectory)?; + package_buildpack(&cargo_manifest_dir, cargo_profile, target_triple) +} - let cargo_metadata = MetadataCommand::new() - .manifest_path(&cargo_manifest_dir.join("Cargo.toml")) - .exec() - .map_err(PackageCrateBuildpackError::CargoMetadataError)?; +pub(crate) fn package_buildpack( + buildpack_dir: &Path, + cargo_profile: CargoProfile, + target_triple: impl AsRef, +) -> Result { + let buildpack_dir = if buildpack_dir.is_relative() { + current_dir() + .and_then(|current_dir| current_dir.join(buildpack_dir).canonicalize()) + .map_err(PackageBuildpackError::CannotGetCurrentDirectory)? + } else { + buildpack_dir.to_path_buf() + }; - let cargo_env = match cross_compile_assistance(target_triple.as_ref()) { + let cargo_build_env = match cross_compile_assistance(target_triple.as_ref()) { CrossCompileAssistance::HelpText(help_text) => { - return Err(PackageCrateBuildpackError::CrossCompileConfigurationError( + return Err(PackageBuildpackError::CrossCompileConfigurationError( help_text, )); } @@ -29,34 +58,153 @@ pub(crate) fn package_crate_buildpack( CrossCompileAssistance::Configuration { cargo_env } => cargo_env, }; - let buildpack_dir = - tempdir().map_err(PackageCrateBuildpackError::CannotCreateBuildpackTempDirectory)?; + let workspace_dir = + find_cargo_workspace(&buildpack_dir).map_err(PackageBuildpackError::FindCargoWorkspace)?; + + let buildpack_dirs = find_buildpack_dirs(&workspace_dir, &[workspace_dir.join("target")]) + .map_err(PackageBuildpackError::FindBuildpackDirs)?; + + let buildpack_packages = buildpack_dirs + .into_iter() + .map(read_buildpack_package) + .collect::, _>>() + .map_err(|error| match error { + ReadBuildpackPackageError::ReadBuildpackDataError(e) => { + PackageBuildpackError::ReadBuildpackData(e) + } + ReadBuildpackPackageError::ReadBuildpackageDataError(e) => { + PackageBuildpackError::ReadBuildpackageData(e) + } + })?; + + let buildpack_packages_graph = create_dependency_graph(buildpack_packages) + .map_err(PackageBuildpackError::CreateDependencyGraph)?; + + let buildpack_packages_requested = buildpack_packages_graph + .node_weights() + .filter(|buildpack_package| buildpack_package.path == buildpack_dir) + .collect::>(); + + assert!( + !buildpack_packages_requested.is_empty(), + "Could not package directory as buildpack: {}", + buildpack_dir.display() + ); + + let build_order = get_dependencies(&buildpack_packages_graph, &buildpack_packages_requested) + .map_err(PackageBuildpackError::GetDependencies)?; + + let root_output_dir = + tempdir().map_err(PackageBuildpackError::CannotCreateBuildpackTempDirectory)?; + + let buildpack_output_directory_locator = BuildpackOutputDirectoryLocator::new( + root_output_dir.path().to_path_buf(), + cargo_profile, + target_triple.as_ref().to_string(), + ); + for buildpack_package in &build_order { + let target_dir = buildpack_output_directory_locator.get(&buildpack_package.id()); + match buildpack_package.buildpack_data.buildpack_descriptor { + BuildpackDescriptor::Single(_) => { + package_single_buildpack( + buildpack_package, + &target_dir, + cargo_profile, + &cargo_build_env, + target_triple.as_ref(), + )?; + } + BuildpackDescriptor::Meta(_) => { + package_meta_buildpack( + buildpack_package, + &target_dir, + &buildpack_output_directory_locator, + )?; + } + } + } + + let target_buildpack_id = buildpack_packages_requested + .iter() + .map(|buildpack_package| buildpack_package.buildpack_id()) + .next() + .expect("The list of requested buildpacks should only contain a single item (i.e.; the buildpack being tested)"); + + let output_dir = buildpack_output_directory_locator.get(target_buildpack_id); + + let buildpack_image = format!( + "{target_buildpack_id}_{}", + repeat_with(fastrand::lowercase) + .take(30) + .collect::() + ); + + let buildpack_package_command = + PackBuildpackPackageCommand::new(&buildpack_image, output_dir.join("package.toml")); + + pack::run_buildpack_package_command(buildpack_package_command); + + Ok(buildpack_image) +} + +fn package_single_buildpack( + buildpack_package: &BuildpackPackage, + target_dir: &Path, + cargo_profile: CargoProfile, + cargo_build_env: &[(OsString, OsString)], + target_triple: &str, +) -> Result<(), PackageBuildpackError> { let buildpack_binaries = build_buildpack_binaries( - &cargo_manifest_dir, - &cargo_metadata, + &buildpack_package.path, cargo_profile, - &cargo_env, - target_triple.as_ref(), + cargo_build_env, + target_triple, ) - .map_err(PackageCrateBuildpackError::BuildBinariesError)?; + .map_err(PackageBuildpackError::BuildBuildpackBinaries)?; - assemble_buildpack_directory( - buildpack_dir.path(), - cargo_manifest_dir.join("buildpack.toml"), + assemble_single_buildpack_directory( + target_dir, + &buildpack_package.buildpack_data.buildpack_descriptor_path, + buildpack_package + .buildpackage_data + .as_ref() + .map(|data| &data.buildpackage_descriptor), &buildpack_binaries, ) - .map_err(PackageCrateBuildpackError::CannotAssembleBuildpackDirectory)?; + .map_err(PackageBuildpackError::AssembleBuildpackDirectory) +} - Ok(buildpack_dir) +fn package_meta_buildpack( + buildpack_package: &BuildpackPackage, + target_dir: &Path, + buildpack_output_directory_locator: &BuildpackOutputDirectoryLocator, +) -> Result<(), PackageBuildpackError> { + assemble_meta_buildpack_directory( + target_dir, + &buildpack_package.path, + &buildpack_package.buildpack_data.buildpack_descriptor_path, + buildpack_package + .buildpackage_data + .as_ref() + .map(|data| &data.buildpackage_descriptor), + buildpack_output_directory_locator, + ) + .map_err(PackageBuildpackError::AssembleBuildpackDirectory) } #[derive(Debug)] -pub(crate) enum PackageCrateBuildpackError { - BuildBinariesError(BuildBinariesError), - CannotAssembleBuildpackDirectory(std::io::Error), +pub(crate) enum PackageBuildpackError { CannotCreateBuildpackTempDirectory(std::io::Error), CannotDetermineCrateDirectory(std::env::VarError), - CargoMetadataError(cargo_metadata::Error), CrossCompileConfigurationError(String), + CannotGetCurrentDirectory(std::io::Error), + FindCargoWorkspace(FindCargoWorkspaceError), + FindBuildpackDirs(std::io::Error), + ReadBuildpackData(ReadBuildpackDataError), + ReadBuildpackageData(ReadBuildpackageDataError), + CreateDependencyGraph(CreateDependencyGraphError), + GetDependencies(GetDependenciesError), + BuildBuildpackBinaries(BuildBinariesError), + AssembleBuildpackDirectory(AssembleBuildpackDirectoryError), } diff --git a/libcnb-test/src/build_config.rs b/libcnb-test/src/build_config.rs index 644aa4f5..765e54ba 100644 --- a/libcnb-test/src/build_config.rs +++ b/libcnb-test/src/build_config.rs @@ -251,6 +251,8 @@ pub enum BuildpackReference { Crate, /// References another buildpack by id, local directory or tarball. Other(String), + + Local(PathBuf), } /// Result of a pack execution. diff --git a/libcnb-test/src/pack.rs b/libcnb-test/src/pack.rs index 9dd6b800..9a0b3846 100644 --- a/libcnb-test/src/pack.rs +++ b/libcnb-test/src/pack.rs @@ -213,6 +213,64 @@ pub(crate) fn run_pack_command>( output } +#[derive(Clone, Debug)] +pub(crate) enum BuildpackPackageFormat { + Image, +} + +#[derive(Clone, Debug)] +pub(crate) struct PackBuildpackPackageCommand { + name: String, + config: PathBuf, + format: BuildpackPackageFormat, +} + +impl PackBuildpackPackageCommand { + pub(crate) fn new(name: impl Into, config: impl Into) -> Self { + Self { + name: name.into(), + config: config.into(), + format: BuildpackPackageFormat::Image, + } + } +} + +impl From for Command { + fn from(value: PackBuildpackPackageCommand) -> Self { + let mut command = Self::new("pack"); + + let args = vec![ + String::from("buildpack"), + String::from("package"), + value.name, + String::from("--config"), + value.config.to_string_lossy().to_string(), + format!( + "--format={}", + match value.format { + BuildpackPackageFormat::Image => String::from("image"), + } + ), + ]; + + command.args(args); + + command + } +} + +pub(crate) fn run_buildpack_package_command>(command: C) { + command.into() + .output() + .unwrap_or_else(|io_error| { + if io_error.kind() == std::io::ErrorKind::NotFound { + panic!("External `pack` command not found. Install Pack CLI and ensure it is on PATH: https://buildpacks.io/docs/install-pack"); + } else { + panic!("Could not spawn external `pack` process: {io_error}"); + }; + }); +} + #[cfg(test)] mod tests { use super::*; diff --git a/libcnb-test/src/test_context.rs b/libcnb-test/src/test_context.rs index f179de21..bf655db9 100644 --- a/libcnb-test/src/test_context.rs +++ b/libcnb-test/src/test_context.rs @@ -23,6 +23,7 @@ pub struct TestContext<'a> { pub(crate) image_name: String, pub(crate) runner: &'a TestRunner, + pub(crate) buildpack_images: Vec, } impl<'a> TestContext<'a> { @@ -247,19 +248,23 @@ impl<'a> TestContext<'a> { impl<'a> Drop for TestContext<'a> { fn drop(&mut self) { - // We do not care if image removal succeeded or not. Panicking here would result in - // SIGILL since this function might be called in a Tokio runtime. - let _image_delete_result = - self.runner + let mut images = vec![&self.image_name]; + images.extend(&self.buildpack_images); + for image in images { + // We do not care if image removal succeeded or not. Panicking here would result in + // SIGILL since this function might be called in a Tokio runtime. + let _ = self + .runner .tokio_runtime .block_on(self.runner.docker.remove_image( - &self.image_name, + image, Some(RemoveImageOptions { force: true, ..RemoveImageOptions::default() }), None, )); + } } } diff --git a/libcnb-test/src/test_runner.rs b/libcnb-test/src/test_runner.rs index 2c96cfd8..156b0f47 100644 --- a/libcnb-test/src/test_runner.rs +++ b/libcnb-test/src/test_runner.rs @@ -101,6 +101,15 @@ impl TestRunner { ) { let config = config.borrow(); + let mut test_context = TestContext { + pack_stdout: String::new(), + pack_stderr: String::new(), + image_name, + config: config.clone(), + runner: self, + buildpack_images: vec![], + }; + let app_dir = { let normalized_app_dir_path = if config.app_dir.is_relative() { env::var("CARGO_MANIFEST_DIR") @@ -131,16 +140,8 @@ impl TestRunner { } }; - let temp_crate_buildpack_dir = - config - .buildpacks - .contains(&BuildpackReference::Crate) - .then(|| { - build::package_crate_buildpack(config.cargo_profile, &config.target_triple) - .expect("Could not package current crate as buildpack") - }); - - let mut pack_command = PackBuildCommand::new(&config.builder_name, &app_dir, &image_name); + let mut pack_command = + PackBuildCommand::new(&config.builder_name, &app_dir, &test_context.image_name); config.env.iter().for_each(|(key, value)| { pack_command.env(key, value); @@ -149,22 +150,29 @@ impl TestRunner { for buildpack in &config.buildpacks { match buildpack { BuildpackReference::Crate => { - pack_command.buildpack(temp_crate_buildpack_dir.as_ref() - .expect("Test references crate buildpack, but crate wasn't packaged as a buildpack. This is an internal libcnb-test error, please report any occurrences.")) + let buildpack_image = + build::package_crate_buildpack(config.cargo_profile, &config.target_triple) + .expect("Test references crate buildpack, but crate wasn't packaged as a buildpack. This is an internal libcnb-test error, please report any occurrences"); + pack_command.buildpack(buildpack_image.clone()); + test_context.buildpack_images.push(buildpack_image); + } + BuildpackReference::Local(path) => { + let buildpack_image = + build::package_buildpack(path, config.cargo_profile, &config.target_triple) + .unwrap_or_else(|_| panic!("Test references buildpack at {}, but this directory wasn't packaged as a buildpack. This is an internal libcnb-test error, please report any occurrences", path.display())); + pack_command.buildpack(buildpack_image.clone()); + test_context.buildpack_images.push(buildpack_image); + } + BuildpackReference::Other(id) => { + pack_command.buildpack(id.clone()); } - BuildpackReference::Other(id) => pack_command.buildpack(id.clone()), }; } let output = run_pack_command(pack_command, &config.expected_pack_result); - let test_context = TestContext { - pack_stdout: String::from_utf8_lossy(&output.stdout).into_owned(), - pack_stderr: String::from_utf8_lossy(&output.stderr).into_owned(), - image_name, - config: config.clone(), - runner: self, - }; + test_context.pack_stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + test_context.pack_stderr = String::from_utf8_lossy(&output.stderr).into_owned(); f(test_context); } diff --git a/libcnb-test/tests/integration_test.rs b/libcnb-test/tests/integration_test.rs index beffc9c7..6ad66ffe 100644 --- a/libcnb-test/tests/integration_test.rs +++ b/libcnb-test/tests/integration_test.rs @@ -12,6 +12,7 @@ use libcnb_test::{ assert_contains, assert_empty, assert_not_contains, BuildConfig, BuildpackReference, ContainerConfig, PackResult, TestRunner, }; +use std::ops::Deref; use std::path::PathBuf; use std::time::Duration; use std::{env, fs, thread}; @@ -132,14 +133,29 @@ fn starting_containers() { #[test] #[ignore = "integration test"] -#[should_panic( - expected = "Could not package current crate as buildpack: BuildBinariesError(ConfigError(NoBinTargetsFound))" -)] fn buildpack_packaging_failure() { - TestRunner::default().build( - BuildConfig::new("libcnb/invalid-builder", "test-fixtures/empty"), - |_| {}, - ); + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let result = std::panic::catch_unwind(|| { + TestRunner::default().build( + BuildConfig::new("libcnb/invalid-builder", "test-fixtures/empty"), + |_| {}, + ); + }); + match result { + Ok(_) => panic!("expected a failure"), + Err(error) => { + let message = error + .downcast_ref::() + .map(String::as_str) + .or_else(|| error.downcast_ref::<&'static str>().map(Deref::deref)) + .unwrap() + .to_string(); + assert_eq!( + message, + format!("Could not package directory as buildpack: {crate_dir}") + ); + } + } } #[test] @@ -190,19 +206,6 @@ fn expected_pack_failure() { ); } -#[test] -#[ignore = "integration test"] -#[should_panic( - expected = "Could not package current crate as buildpack: BuildBinariesError(ConfigError(NoBinTargetsFound))" -)] -fn expected_pack_failure_still_panics_for_non_pack_failure() { - TestRunner::default().build( - BuildConfig::new("libcnb/invalid-builder", "test-fixtures/empty") - .expected_pack_result(PackResult::Failure), - |_| {}, - ); -} - #[test] #[ignore = "integration test"] fn app_dir_preprocessor() { @@ -303,6 +306,47 @@ fn app_dir_invalid_path_checked_before_applying_preprocessor() { ); } +#[test] +#[ignore = "integration test"] +fn basic_build_with_local_reference_to_single_buildpack() { + TestRunner::default().build( + BuildConfig::new("heroku/builder:22", "test-fixtures/procfile").buildpacks(vec![ + BuildpackReference::Local(PathBuf::from("../libcnb-cargo/fixtures/single_buildpack")), + ]), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + Single buildpack + "} + ); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn basic_build_with_local_reference_to_meta_buildpack() { + TestRunner::default().build( + BuildConfig::new("heroku/builder:22", "test-fixtures/procfile").buildpacks(vec![ + BuildpackReference::Local(PathBuf::from( + "../libcnb-cargo/fixtures/multiple_buildpacks/meta-buildpacks/meta-two", + )), + ]), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + One buildpack + Two buildpack + "} + ); + }, + ); +} + // We're referencing the procfile buildpack via Docker URL to pin the version for the tests. This also // prevents issues when the builder contains multiple heroku/procfile versions. We don't use CNB // registry URLs since, as of August 2022, pack fails when another pack instance is resolving such