Skip to content

Commit

Permalink
Add find_cargo_workspace utility method
Browse files Browse the repository at this point in the history
In preparation for adding support for testing meta-buildpacks the `find_cargo_workspace` method is being added to `libcnb-package` so that it can be shared between `libcnb-package` and `libcnb-test`.

Both the packaging and testing processes will use this to locate buildpacks within a workspace in order to resolve project-level dependencies declared with `libcnb:{buildpack_id}`.
  • Loading branch information
colincasey committed Aug 11, 2023
1 parent 1cf9d61 commit 6609f11
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 60 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ 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))
- Added `find_cargo_workspace` which provides a convenient starting point for locating buildpacks for packaging and testing purposes.
- `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))

Expand Down
72 changes: 26 additions & 46 deletions libcnb-cargo/src/package/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ 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,
assemble_buildpack_directory, find_buildpack_dirs, find_cargo_workspace,
get_buildpack_target_dir, CargoProfile,
};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;

type Result<T> = std::result::Result<T, Error>;

Expand All @@ -25,42 +25,35 @@ pub(crate) fn execute(args: &PackageArgs) -> Result<()> {

let current_dir = std::env::current_dir().map_err(Error::GetCurrentDir)?;

let workspace = get_cargo_workspace_root(&current_dir)?;
let workspace_dir = find_cargo_workspace(&current_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 output_dir = get_buildpack_output_dir(&workspace_dir)?;

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::<Result<Vec<BuildpackPackage>>>()?,
)?;
let buildpack_dirs = find_buildpack_dirs(&workspace_dir, &[output_dir.clone()])
.map_err(|e| Error::FindBuildpackDirs(workspace_dir.clone(), e))?;

let buildpack_packages = buildpack_dirs
.into_iter()
.into_iter()
.map(read_buildpack_package)
.collect::<std::result::Result<Vec<_>, _>>()?;

let buildpack_packages_graph = create_dependency_graph(buildpack_packages)?;

let target_directories_index = buildpack_packages
let target_directories_index = buildpack_packages_graph
.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)
get_buildpack_target_dir(id, &output_dir, args.release, &args.target)
};
(id, target_dir)
})
.collect::<HashMap<_, _>>();

let buildpack_packages_requested = 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
Expand All @@ -77,7 +70,7 @@ 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 lookup_target_dir = |buildpack_package: &BuildpackPackage| {
target_directories_index
Expand Down Expand Up @@ -266,27 +259,6 @@ fn print_requested_buildpack_output_dirs(output_directories: Vec<PathBuf>) {
}
}

fn get_cargo_workspace_root(dir: &Path) -> Result<PathBuf> {
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)
Expand Down Expand Up @@ -343,3 +315,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<PathBuf> {
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))
}
47 changes: 34 additions & 13 deletions libcnb-cargo/src/package/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@ use libcnb_package::buildpack_dependency::{
RewriteBuildpackageRelativePathDependenciesToAbsoluteError,
};
use libcnb_package::dependency_graph::{CreateDependencyGraphError, GetDependenciesError};
use libcnb_package::FindCargoWorkspaceError;
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 execute `cargo locate-project --workspace --message-format plain in {path}\nError: {source}")]
GetWorkspaceCommand {
path: PathBuf,
source: std::io::Error,
},
#[error("Could not locate a Cargo workspace within `{0}` or it's parent directories")]
GetWorkspaceDirectory(PathBuf),

#[error("Could not read Cargo.toml metadata in `{path}`\nError: {source}")]
ReadCargoMetadata {
Expand All @@ -42,11 +38,8 @@ pub(crate) enum Error {
#[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,
Expand Down Expand Up @@ -129,6 +122,15 @@ pub(crate) enum Error {

#[error("I/O error while calculating directory size\nPath: {0}\nError: {1}")]
CalculateDirectorySize(PathBuf, std::io::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 read Cargo.toml metadata from workspace\nPath: {0}\nError: {1}")]
GetBuildpackOutputDir(PathBuf, cargo_metadata::Error),
}

impl From<BuildBinariesError> for Error {
Expand Down Expand Up @@ -228,3 +230,22 @@ impl From<RewriteBuildpackageRelativePathDependenciesToAbsoluteError> for Error
}
}
}

impl From<FindCargoWorkspaceError> 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),
}
}
}

fn exit_status_or_unknown(exit_status: ExitStatus) -> String {
exit_status
.code()
.map_or_else(|| String::from("<unknown>"), |code| code.to_string())
}
45 changes: 45 additions & 0 deletions libcnb-package/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId};
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.
Expand Down Expand Up @@ -268,6 +269,50 @@ pub fn get_buildpack_target_dir(
.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<PathBuf, FindCargoWorkspaceError> {
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))
})
}

#[derive(Debug)]
pub enum FindCargoWorkspaceError {
GetCargoEnv(std::env::VarError),
SpawnCommand(std::io::Error),
CommandFailure(std::process::ExitStatus),
GetParentDirectory(PathBuf),
}

#[cfg(test)]
mod tests {
use crate::get_buildpack_target_dir;
Expand Down

0 comments on commit 6609f11

Please sign in to comment.