Skip to content

Commit

Permalink
Package all binary targets (#314)
Browse files Browse the repository at this point in the history
* Package all binary targets into buildpack

* Add docs about configuring main buildpack binary

* Make Config create private

* Fix typo

Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com>

* Fix typo

Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com>

* Move packaging configuration docs to libcnb-cargo, add referece to libcnb-test

* Remove explict configuration of main buildpack binary

Co-authored-by: Ed Morley <501702+edmorley@users.noreply.github.com>
  • Loading branch information
Malax and edmorley authored Feb 14, 2022
1 parent 11bdefc commit 4675daf
Show file tree
Hide file tree
Showing 8 changed files with 296 additions and 136 deletions.
1 change: 1 addition & 0 deletions libcnb-cargo/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased]

- Update cross-compile assistance on macOS to use https://github.com/messense/homebrew-macos-cross-toolchains instead of https://github.com/FiloSottile/homebrew-musl-cross. Support for the latter has not been removed, existing setups continue to work as before. ([#312](https://github.com/Malax/libcnb.rs/pull/312))
- `libcnb-cargo` now cross-compiles and packages all binary targets of the buildpack. The main buildpack binary is either the only binary target or the target with the same name as the crate. This feature allows the usage of additional binaries for i.e. execd. ([#314](https://github.com/Malax/libcnb.rs/pull/314))

## [0.2.1] 2022-01-19

Expand Down
178 changes: 178 additions & 0 deletions libcnb-cargo/src/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use crate::config::{config_from_metadata, ConfigError};
use crate::CargoProfile;
use cargo_metadata::Metadata;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};

/// Builds all buildpack binary targets using Cargo.
///
/// It uses libcnb configuration metadata in the Crate's `Cargo.toml` to determine which binary is
/// the main buildpack binary and which are additional ones.
///
/// See [`build_binary`] for details around the build process.
///
/// # Errors
///
/// Will return `Err` if any build did not finish successfully, the configuration cannot be
/// read or the configured main buildpack binary does not exist.
pub fn build_buildpack_binaries<
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr> + Clone,
V: AsRef<OsStr> + Clone,
>(
project_path: impl AsRef<Path>,
cargo_metadata: &Metadata,
cargo_profile: CargoProfile,
cargo_env: I,
target_triple: impl AsRef<str>,
) -> Result<BuildpackBinaries, BuildBinariesError> {
let binary_target_names = binary_target_names(cargo_metadata);
let config = config_from_metadata(cargo_metadata).map_err(BuildBinariesError::ConfigError)?;

let cargo_env: Vec<(K, V)> = cargo_env.into_iter().collect();

let buildpack_target_binary_path = if binary_target_names.contains(&config.buildpack_target) {
build_binary(
project_path.as_ref(),
cargo_metadata,
cargo_profile,
cargo_env.clone(),
target_triple.as_ref(),
&config.buildpack_target,
)
.map_err(|error| BuildBinariesError::BuildError(config.buildpack_target.clone(), error))
} else {
Err(BuildBinariesError::MissingBuildpackTarget(
config.buildpack_target.clone(),
))
}?;

let mut additional_target_binary_paths = HashMap::new();
for additional_binary_target_name in binary_target_names
.iter()
.filter(|name| *name != &config.buildpack_target)
{
additional_target_binary_paths.insert(
additional_binary_target_name.clone(),
build_binary(
project_path.as_ref(),
cargo_metadata,
cargo_profile,
cargo_env.clone(),
target_triple.as_ref(),
additional_binary_target_name,
)
.map_err(|error| {
BuildBinariesError::BuildError(additional_binary_target_name.clone(), error)
})?,
);
}

Ok(BuildpackBinaries {
buildpack_target_binary_path,
additional_target_binary_paths,
})
}

/// Builds a binary using Cargo.
///
/// It is designed to handle cross-compilation without requiring custom configuration in the Cargo
/// manifest of the user's buildpack. The triple for the target platform is a mandatory
/// argument of this function.
///
/// Depending on the host platform, this function will try to set the required cross compilation
/// settings automatically. Please note that only selected host platforms and targets are supported.
/// For other combinations, compilation might fail, surfacing cross-compile related errors to the
/// user.
///
/// In many cases, cross-compilation requires external tools such as compilers and linkers to be
/// installed on the user's machine. When a tool is missing, a `BuildError::CrossCompileError` is
/// returned which provides additional information. Use the `cross_compile::cross_compile_help`
/// function to obtain human-readable instructions on how to setup the required tools.
///
/// This function will write Cargo's output to stdout and stderr.
///
/// # Errors
///
/// Will return `Err` if the build did not finish successfully.
pub fn build_binary<I: IntoIterator<Item = (K, V)>, K: AsRef<OsStr>, V: AsRef<OsStr>>(
project_path: impl AsRef<Path>,
cargo_metadata: &Metadata,
cargo_profile: CargoProfile,
cargo_env: I,
target_triple: impl AsRef<str>,
target_name: impl AsRef<str>,
) -> Result<PathBuf, BuildError> {
let mut cargo_args = vec!["build", "--target", target_triple.as_ref()];
match cargo_profile {
CargoProfile::Dev => {}
CargoProfile::Release => cargo_args.push("--release"),
}

let exit_status = Command::new("cargo")
.args(cargo_args)
.envs(cargo_env)
.current_dir(&project_path)
.spawn()
.and_then(|mut child| child.wait())
.map_err(BuildError::IoError)?;

if exit_status.success() {
let binary_path = cargo_metadata
.target_directory
.join(target_triple.as_ref())
.join(match cargo_profile {
CargoProfile::Dev => "debug",
CargoProfile::Release => "release",
})
.join(target_name.as_ref())
.into_std_path_buf();

Ok(binary_path)
} else {
Err(BuildError::UnexpectedCargoExitStatus(exit_status))
}
}

#[derive(Debug)]
pub struct BuildpackBinaries {
/// The path to the main buildpack binary
pub buildpack_target_binary_path: PathBuf,
/// Paths to additional binaries from the buildpack
pub additional_target_binary_paths: HashMap<String, PathBuf>,
}

#[derive(Debug)]
pub enum BuildError {
IoError(std::io::Error),
UnexpectedCargoExitStatus(ExitStatus),
}

#[derive(Debug)]
pub enum BuildBinariesError {
ConfigError(ConfigError),
BuildError(String, BuildError),
MissingBuildpackTarget(String),
}

/// Determines the names of all binary targets from the given Cargo metadata.
fn binary_target_names(cargo_metadata: &Metadata) -> Vec<String> {
cargo_metadata
.root_package()
.map(|root_package| {
root_package
.targets
.iter()
.filter_map(|target| {
if target.kind.contains(&String::from("bin")) {
Some(target.name.clone())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
45 changes: 45 additions & 0 deletions libcnb-cargo/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use cargo_metadata::{Metadata, Target};

#[derive(Debug)]
pub(crate) struct Config {
pub buildpack_target: String,
}

pub(crate) fn config_from_metadata(cargo_metadata: &Metadata) -> Result<Config, ConfigError> {
let root_package = cargo_metadata
.root_package()
.ok_or(ConfigError::MissingRootPackage)?;

let buildpack_bin_targets: Vec<&Target> = root_package
.targets
.iter()
.filter(|target| target.kind == vec!["bin"])
.collect();

match buildpack_bin_targets.as_slice() {
[single_target] => Ok(Config {
buildpack_target: single_target.name.clone(),
}),
[] => Err(ConfigError::NoBinTargetsFound),
bin_target_names => {
let has_bin_target_with_root_package_name = bin_target_names
.iter()
.any(|target_name| target_name.name == root_package.name);

if has_bin_target_with_root_package_name {
Ok(Config {
buildpack_target: root_package.name.clone(),
})
} else {
Err(ConfigError::MultipleBinTargetsFound)
}
}
}
}

#[derive(Debug)]
pub enum ConfigError {
MissingRootPackage,
NoBinTargetsFound,
MultipleBinTargetsFound,
}
125 changes: 23 additions & 102 deletions libcnb-cargo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,111 +3,14 @@
// This lint is too noisy and enforces a style that reduces readability in many cases.
#![allow(clippy::module_name_repetitions)]

pub mod build;
pub mod config;
pub mod cross_compile;

use cargo_metadata::{MetadataCommand, Target};
use crate::build::BuildpackBinaries;
use libcnb_data::buildpack::SingleBuildpackDescriptor;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};

/// Builds a buildpack binary using Cargo.
///
/// It is designed to handle cross-compilation without requiring custom configuration in the Cargo
/// manifest of the user's buildpack. The triple for the target platform is a mandatory
/// argument of this function.
///
/// Depending on the host platform, this function will try to set the required cross compilation
/// settings automatically. Please note that only selected host platforms and targets are supported.
/// For other combinations, compilation might fail, surfacing cross-compile related errors to the
/// user.
///
/// In many cases, cross-compilation requires external tools such as compilers and linkers to be
/// installed on the user's machine. When a tool is missing, a `BuildError::CrossCompileError` is
/// returned which provides additional information. Use the `cross_compile::cross_compile_help`
/// function to obtain human-readable instructions on how to setup the required tools.
///
/// This function currently only supports projects with a single binary target. If the project
/// does not contain exactly one target, the appropriate `BuildError` is returned.
///
/// This function will write Cargo's output to stdout and stderr.
///
/// # Errors
///
/// Will return `Err` if the build did not finish successfully.
pub fn build_buildpack_binary<I, K, V>(
project_path: impl AsRef<Path>,
cargo_profile: CargoProfile,
target_triple: impl AsRef<str>,
cargo_env: I,
) -> Result<PathBuf, BuildError>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let cargo_metadata = MetadataCommand::new()
.manifest_path(project_path.as_ref().join("Cargo.toml"))
.exec()
.map_err(BuildError::MetadataError)?;

let buildpack_cargo_package = cargo_metadata
.root_package()
.ok_or(BuildError::CouldNotFindRootPackage)?;

let buildpack_bin_targets: Vec<&Target> = buildpack_cargo_package
.targets
.iter()
.filter(|target| target.kind == vec!["bin"])
.collect();

let target = match buildpack_bin_targets.as_slice() {
[] => Err(BuildError::NoBinTargetsFound),
[single_target] => Ok(single_target),
_ => Err(BuildError::MultipleBinTargetsFound),
}?;

let mut cargo_args = vec!["build", "--target", target_triple.as_ref()];
match cargo_profile {
CargoProfile::Dev => {}
CargoProfile::Release => cargo_args.push("--release"),
}

let exit_status = Command::new("cargo")
.args(cargo_args)
.envs(cargo_env)
.current_dir(&project_path)
.spawn()
.and_then(|mut child| child.wait())
.map_err(BuildError::IoError)?;

if exit_status.success() {
let binary_path = cargo_metadata
.target_directory
.join(target_triple.as_ref())
.join(match cargo_profile {
CargoProfile::Dev => "debug",
CargoProfile::Release => "release",
})
.join(&target.name)
.into_std_path_buf();

Ok(binary_path)
} else {
Err(BuildError::UnexpectedExitStatus(exit_status))
}
}

#[derive(Debug)]
pub enum BuildError {
IoError(std::io::Error),
UnexpectedExitStatus(ExitStatus),
NoBinTargetsFound,
MultipleBinTargetsFound,
MetadataError(cargo_metadata::Error),
CouldNotFindRootPackage,
}

#[derive(Copy, Clone)]
pub enum CargoProfile {
Expand Down Expand Up @@ -162,7 +65,7 @@ pub struct BuildpackData<BM> {
pub fn assemble_buildpack_directory(
destination_path: impl AsRef<Path>,
buildpack_descriptor_path: impl AsRef<Path>,
buildpack_binary_path: impl AsRef<Path>,
buildpack_binaries: &BuildpackBinaries,
) -> std::io::Result<()> {
fs::create_dir_all(destination_path.as_ref())?;

Expand All @@ -174,9 +77,27 @@ pub fn assemble_buildpack_directory(
let bin_path = destination_path.as_ref().join("bin");
fs::create_dir_all(&bin_path)?;

fs::copy(buildpack_binary_path.as_ref(), bin_path.join("build"))?;
fs::copy(
&buildpack_binaries.buildpack_target_binary_path,
bin_path.join("build"),
)?;

create_file_symlink("build", bin_path.join("detect"))?;

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(())
}

Expand Down
Loading

0 comments on commit 4675daf

Please sign in to comment.