libcnb_package/
build.rs

1use crate::CargoProfile;
2use crate::cargo::{
3    DetermineBuildpackCargoTargetNameError, cargo_binary_target_names,
4    determine_buildpack_cargo_target_name,
5};
6use cargo_metadata::Metadata;
7use std::collections::HashMap;
8use std::env;
9use std::ffi::OsString;
10use std::path::{Path, PathBuf};
11use std::process::{Command, ExitStatus};
12
13/// Builds all buildpack binary targets using Cargo.
14///
15/// It uses libcnb configuration metadata in the Crate's `Cargo.toml` to determine which binary is
16/// the main buildpack binary and which are additional ones.
17///
18/// See [`build_binary`] for details around the build process.
19///
20/// # Errors
21///
22/// Will return `Err` if any build did not finish successfully, the configuration can't be
23/// read or the configured main buildpack binary does not exist.
24pub(crate) fn build_buildpack_binaries(
25    project_path: impl AsRef<Path>,
26    cargo_metadata: &Metadata,
27    cargo_profile: CargoProfile,
28    cargo_env: &[(OsString, OsString)],
29    target_triple: impl AsRef<str>,
30) -> Result<BuildpackBinaries, BuildBinariesError> {
31    let binary_target_names = cargo_binary_target_names(cargo_metadata);
32    let buildpack_cargo_target = determine_buildpack_cargo_target_name(cargo_metadata)
33        .map_err(BuildBinariesError::CannotDetermineBuildpackCargoTargetName)?;
34
35    let buildpack_target_binary_path = if binary_target_names.contains(&buildpack_cargo_target) {
36        build_binary(
37            project_path.as_ref(),
38            cargo_metadata,
39            cargo_profile,
40            cargo_env.to_owned(),
41            target_triple.as_ref(),
42            &buildpack_cargo_target,
43        )
44        .map_err(|error| BuildBinariesError::BuildError(buildpack_cargo_target.clone(), error))
45    } else {
46        Err(BuildBinariesError::MissingBuildpackTarget(
47            buildpack_cargo_target.clone(),
48        ))
49    }?;
50
51    let mut additional_target_binary_paths = HashMap::new();
52    for additional_binary_target_name in binary_target_names
53        .iter()
54        .filter(|name| *name != &buildpack_cargo_target)
55    {
56        additional_target_binary_paths.insert(
57            additional_binary_target_name.clone(),
58            build_binary(
59                project_path.as_ref(),
60                cargo_metadata,
61                cargo_profile,
62                cargo_env.to_owned(),
63                target_triple.as_ref(),
64                additional_binary_target_name,
65            )
66            .map_err(|error| {
67                BuildBinariesError::BuildError(additional_binary_target_name.clone(), error)
68            })?,
69        );
70    }
71
72    Ok(BuildpackBinaries {
73        buildpack_target_binary_path,
74        additional_target_binary_paths,
75    })
76}
77
78/// Builds a binary using Cargo.
79///
80/// It is designed to handle cross-compilation without requiring custom configuration in the Cargo
81/// manifest of the user's buildpack. The triple for the target platform is a mandatory
82/// argument of this function.
83///
84/// Depending on the host platform, this function will try to set the required cross compilation
85/// settings automatically. Please note that only selected host platforms and targets are supported.
86/// For other combinations, compilation might fail, surfacing cross-compile related errors to the
87/// user.
88///
89/// In many cases, cross-compilation requires external tools such as compilers and linkers to be
90/// installed on the user's machine. When a tool is missing, a `BuildError::CrossCompileError` is
91/// returned which provides additional information. Use the `cross_compile::cross_compile_help`
92/// function to obtain human-readable instructions on how to setup the required tools.
93///
94/// This function will write Cargo's output to stdout and stderr.
95///
96/// # Errors
97///
98/// Will return `Err` if the build did not finish successfully.
99fn build_binary(
100    project_path: impl AsRef<Path>,
101    cargo_metadata: &Metadata,
102    cargo_profile: CargoProfile,
103    mut cargo_env: Vec<(OsString, OsString)>,
104    target_triple: impl AsRef<str>,
105    target_name: impl AsRef<str>,
106) -> Result<PathBuf, BuildError> {
107    let mut cargo_args = vec!["build", "--target", target_triple.as_ref()];
108
109    if env::var_os("CI").is_some() {
110        cargo_args.push("--locked");
111    }
112
113    match cargo_profile {
114        CargoProfile::Dev => {
115            // We enable stripping for dev builds too, since debug builds are extremely
116            // large and can otherwise take a long time to be Docker copied into the
117            // ephemeral builder image created by `pack build` for local development
118            // and integration testing workflows. Since we are stripping the builds,
119            // we also disable debug symbols to improve performance slightly, since
120            // they will only be stripped out at the end of the build anyway.
121            cargo_env.append(&mut vec![
122                (
123                    OsString::from("CARGO_PROFILE_DEV_DEBUG"),
124                    OsString::from("false"),
125                ),
126                (
127                    OsString::from("CARGO_PROFILE_DEV_STRIP"),
128                    OsString::from("true"),
129                ),
130            ]);
131        }
132        CargoProfile::Release => {
133            cargo_args.push("--release");
134            cargo_env.push((
135                OsString::from("CARGO_PROFILE_RELEASE_STRIP"),
136                OsString::from("true"),
137            ));
138        }
139    }
140
141    let exit_status = Command::new("cargo")
142        .args(cargo_args)
143        .envs(cargo_env)
144        .current_dir(&project_path)
145        .spawn()
146        .and_then(|mut child| child.wait())
147        .map_err(BuildError::CargoProcessIoError)?;
148
149    if exit_status.success() {
150        let binary_path = cargo_metadata
151            .target_directory
152            .join(target_triple.as_ref())
153            .join(match cargo_profile {
154                CargoProfile::Dev => "debug",
155                CargoProfile::Release => "release",
156            })
157            .join(target_name.as_ref())
158            .into_std_path_buf();
159
160        Ok(binary_path)
161    } else {
162        Err(BuildError::UnexpectedCargoExitStatus(exit_status))
163    }
164}
165
166#[derive(Debug)]
167pub(crate) struct BuildpackBinaries {
168    /// The path to the main buildpack binary
169    pub(crate) buildpack_target_binary_path: PathBuf,
170    /// Paths to additional binaries from the buildpack
171    pub(crate) additional_target_binary_paths: HashMap<String, PathBuf>,
172}
173
174#[derive(thiserror::Error, Debug)]
175pub enum BuildError {
176    #[error("I/O error while running Cargo build process: {0}")]
177    CargoProcessIoError(#[source] std::io::Error),
178    #[error("Cargo unexpectedly exited with status {0}")]
179    UnexpectedCargoExitStatus(ExitStatus),
180}
181
182#[derive(thiserror::Error, Debug)]
183pub enum BuildBinariesError {
184    #[error("Failed to determine Cargo target name for buildpack: {0}")]
185    CannotDetermineBuildpackCargoTargetName(#[source] DetermineBuildpackCargoTargetNameError),
186    #[error("Failed to build binary target {0}: {1}")]
187    BuildError(String, #[source] BuildError),
188    #[error("Binary target {0} couldn't be found")]
189    MissingBuildpackTarget(String),
190}