From a6d650ca7187c2515524ac59c4ea19c1660b1ec9 Mon Sep 17 00:00:00 2001 From: Colin Casey Date: Tue, 8 Aug 2023 17:08:54 -0300 Subject: [PATCH] Refactor libcnb-package and libcnb-cargo in preparation for test support The following changes have been made to maximize the sharing of code between `libcnb-cargo`, `libcnb-test`, and `libcnb-package`. This is to prepare for upcoming changes that will allow for testing of local and meta-buildpacks. --- CHANGELOG.md | 22 +- libcnb-cargo/src/package/command.rs | 267 ++++++++------------- libcnb-cargo/src/package/error.rs | 257 ++++++++++++-------- libcnb-cargo/tests/test.rs | 23 +- libcnb-package/src/build.rs | 20 +- libcnb-package/src/buildpack_dependency.rs | 42 ++-- libcnb-package/src/lib.rs | 205 +++++----------- libcnb-package/src/output.rs | 261 ++++++++++++++++++++ libcnb-test/Cargo.toml | 1 - libcnb-test/src/build.rs | 20 +- 10 files changed, 654 insertions(+), 464 deletions(-) create mode 100644 libcnb-package/src/output.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 63731b73..e864c7e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,11 @@ separate changelogs for each crate were used. If you need to refer to these old ### Added -- `libcnb-package`: Add cross-compilation assistance for Linux `aarch64-unknown-linux-musl`. ([#577](https://github.com/heroku/libcnb.rs/pull/577)) +- `libcnb-package` + - Add cross-compilation assistance for Linux `aarch64-unknown-linux-musl`. ([#577](https://github.com/heroku/libcnb.rs/pull/577)) + - 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 @@ -19,10 +23,24 @@ separate changelogs for each crate were used. If you need to refer to these old - `TestRunner::new` has been removed, since its only purpose was for advanced configuration that's no longer applicable. Use `TestRunner::default` instead. ([#620](https://github.com/heroku/libcnb.rs/pull/620)) - `LogOutput` no longer exposes `stdout_raw` and `stderr_raw`. ([#607](https://github.com/heroku/libcnb.rs/pull/607)) - Improved wording of panic error messages. ([#619](https://github.com/heroku/libcnb.rs/pull/619) and [#620](https://github.com/heroku/libcnb.rs/pull/620)) -- `libcnb-package`: buildpack target directory now contains the target triple. Users that implicitly rely on the output directory need to adapt. The output of `cargo libcnb package` will refer to the new locations. ([#580](https://github.com/heroku/libcnb.rs/pull/580)) +- `libcnb-package` + - buildpack target directory now contains the target triple. Users that implicitly rely on the output directory need to adapt. The output of `cargo libcnb package` will refer to the new locations. ([#580](https://github.com/heroku/libcnb.rs/pull/580)) + - 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)) - `libherokubuildpack`: Switch the `flate2` decompression backend from `miniz_oxide` to `zlib`. ([#593](https://github.com/heroku/libcnb.rs/pull/593)) - Bump minimum external dependency versions. ([#587](https://github.com/heroku/libcnb.rs/pull/587)) +### 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)) + ### Fixed - `libcnb-test`: 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..ad655574 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; @@ -139,7 +140,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 +238,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..b9e31285 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,46 @@ 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 due to: +/// - no `CARGO` environment variable with the path to the `cargo` binary +/// - executing this function with a directory that is not within a Cargo project +/// - any other file or system error that might occur +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 58e6208c..a062501c 100644 --- a/libcnb-test/Cargo.toml +++ b/libcnb-test/Cargo.toml @@ -12,7 +12,6 @@ readme = "README.md" include = ["src/**/*", "LICENSE", "README.md"] [dependencies] -cargo_metadata = "0.17.0" 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..8c29052c 100644 --- a/libcnb-test/src/build.rs +++ b/libcnb-test/src/build.rs @@ -1,7 +1,9 @@ -use cargo_metadata::MetadataCommand; use libcnb_package::build::{build_buildpack_binaries, BuildBinariesError}; use libcnb_package::cross_compile::{cross_compile_assistance, CrossCompileAssistance}; -use libcnb_package::{assemble_buildpack_directory, CargoProfile}; +use libcnb_package::output::{ + assemble_single_buildpack_directory, AssembleBuildpackDirectoryError, +}; +use libcnb_package::CargoProfile; use std::path::PathBuf; use tempfile::{tempdir, TempDir}; @@ -14,11 +16,6 @@ pub(crate) fn package_crate_buildpack( .map(PathBuf::from) .map_err(PackageCrateBuildpackError::CannotDetermineCrateDirectory)?; - let cargo_metadata = MetadataCommand::new() - .manifest_path(&cargo_manifest_dir.join("Cargo.toml")) - .exec() - .map_err(PackageCrateBuildpackError::CargoMetadataError)?; - let cargo_env = match cross_compile_assistance(target_triple.as_ref()) { CrossCompileAssistance::HelpText(help_text) => { return Err(PackageCrateBuildpackError::CrossCompileConfigurationError( @@ -34,29 +31,28 @@ pub(crate) fn package_crate_buildpack( let buildpack_binaries = build_buildpack_binaries( &cargo_manifest_dir, - &cargo_metadata, cargo_profile, &cargo_env, target_triple.as_ref(), ) .map_err(PackageCrateBuildpackError::BuildBinariesError)?; - assemble_buildpack_directory( + assemble_single_buildpack_directory( buildpack_dir.path(), cargo_manifest_dir.join("buildpack.toml"), + None, &buildpack_binaries, ) - .map_err(PackageCrateBuildpackError::CannotAssembleBuildpackDirectory)?; + .map_err(PackageCrateBuildpackError::AssembleBuildpackDirectory)?; Ok(buildpack_dir) } #[derive(Debug)] pub(crate) enum PackageCrateBuildpackError { + AssembleBuildpackDirectory(AssembleBuildpackDirectoryError), BuildBinariesError(BuildBinariesError), - CannotAssembleBuildpackDirectory(std::io::Error), CannotCreateBuildpackTempDirectory(std::io::Error), CannotDetermineCrateDirectory(std::env::VarError), - CargoMetadataError(cargo_metadata::Error), CrossCompileConfigurationError(String), }