libcnb_package/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub mod build;
4pub mod buildpack_dependency_graph;
5pub mod buildpack_kind;
6pub mod cargo;
7pub mod cross_compile;
8pub mod dependency_graph;
9pub mod output;
10pub mod package;
11pub mod package_descriptor;
12pub mod util;
13
14use crate::build::BuildpackBinaries;
15use std::fs;
16use std::path::{Path, PathBuf};
17use std::process::Command;
18
19/// The profile to use when invoking Cargo.
20///
21/// <https://doc.rust-lang.org/cargo/reference/profiles.html>
22#[derive(Debug, Copy, Clone, Eq, PartialEq)]
23pub enum CargoProfile {
24    /// Provides faster compilation times at the expense of runtime performance and binary size.
25    Dev,
26    /// Produces assets with optimised runtime performance and binary size, at the expense of compilation time.
27    Release,
28}
29
30/// Creates a buildpack directory and copies all buildpack assets to it.
31///
32/// Assembly of the directory follows the constraints set by the libcnb framework. For example,
33/// the buildpack binary is only copied once and symlinks are used to refer to it when the CNB
34/// spec requires different file(name)s.
35///
36/// This function will not validate if the buildpack descriptor at the given path is valid and will
37/// use it as-is.
38///
39/// # Errors
40///
41/// Will return `Err` if the buildpack directory couldn't be assembled.
42fn assemble_buildpack_directory(
43    destination_path: impl AsRef<Path>,
44    buildpack_descriptor_path: impl AsRef<Path>,
45    buildpack_binaries: &BuildpackBinaries,
46) -> std::io::Result<()> {
47    fs::create_dir_all(destination_path.as_ref())?;
48
49    fs::copy(
50        buildpack_descriptor_path.as_ref(),
51        destination_path.as_ref().join("buildpack.toml"),
52    )?;
53
54    let bin_path = destination_path.as_ref().join("bin");
55    fs::create_dir_all(&bin_path)?;
56
57    fs::copy(
58        &buildpack_binaries.buildpack_target_binary_path,
59        bin_path.join("build"),
60    )?;
61
62    create_file_symlink("build", bin_path.join("detect"))?;
63
64    if !buildpack_binaries.additional_target_binary_paths.is_empty() {
65        let additional_binaries_dir = destination_path
66            .as_ref()
67            .join(".libcnb-cargo")
68            .join("additional-bin");
69
70        fs::create_dir_all(&additional_binaries_dir)?;
71
72        for (binary_target_name, binary_path) in &buildpack_binaries.additional_target_binary_paths
73        {
74            fs::copy(
75                binary_path,
76                additional_binaries_dir.join(binary_target_name),
77            )?;
78        }
79    }
80
81    Ok(())
82}
83
84#[cfg(target_family = "unix")]
85fn create_file_symlink<P: AsRef<Path>, Q: AsRef<Path>>(
86    original: P,
87    link: Q,
88) -> std::io::Result<()> {
89    std::os::unix::fs::symlink(original.as_ref(), link.as_ref())
90}
91
92#[cfg(target_family = "windows")]
93fn create_file_symlink<P: AsRef<Path>, Q: AsRef<Path>>(
94    original: P,
95    link: Q,
96) -> std::io::Result<()> {
97    std::os::windows::fs::symlink_file(original.as_ref(), link.as_ref())
98}
99
100/// Recursively walks the file system from the given `start_dir` to locate any folders containing a
101/// `buildpack.toml` file.
102///
103/// # Errors
104///
105/// Will return an `Err` if any I/O errors happen while walking the file system or any parsing errors
106/// from reading a gitignore file.
107pub fn find_buildpack_dirs(start_dir: &Path) -> Result<Vec<PathBuf>, ignore::Error> {
108    ignore::Walk::new(start_dir)
109        .collect::<Result<Vec<_>, _>>()
110        .map(|entries| {
111            entries
112                .iter()
113                .filter_map(|entry| {
114                    if entry.path().is_dir() && entry.path().join("buildpack.toml").exists() {
115                        Some(entry.path().to_path_buf())
116                    } else {
117                        None
118                    }
119                })
120                .collect()
121        })
122}
123
124/// Returns the path of the root workspace directory for a Rust Cargo project. This is often a useful
125/// starting point for detecting buildpacks with [`find_buildpack_dirs`].
126///
127/// # Errors
128///
129/// Will return an `Err` if the root workspace directory can't be located due to:
130/// - no `CARGO` environment variable with the path to the `cargo` binary
131/// - executing this function with a directory that is not within a Cargo project
132/// - any other file or system error that might occur
133pub fn find_cargo_workspace_root_dir(
134    dir_in_workspace: &Path,
135) -> Result<PathBuf, FindCargoWorkspaceRootError> {
136    let cargo_bin = std::env::var("CARGO")
137        .map(PathBuf::from)
138        .map_err(FindCargoWorkspaceRootError::GetCargoEnv)?;
139
140    let output = Command::new(cargo_bin)
141        .args(["locate-project", "--workspace", "--message-format", "plain"])
142        .current_dir(dir_in_workspace)
143        .output()
144        .map_err(FindCargoWorkspaceRootError::SpawnCommand)?;
145
146    let status = output.status;
147
148    output
149        .status
150        .success()
151        .then_some(output)
152        .ok_or(FindCargoWorkspaceRootError::CommandFailure(status))
153        .and_then(|output| {
154            // Cargo outputs a newline after the actual path, so we have to trim.
155            let root_cargo_toml = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
156            root_cargo_toml.parent().map(Path::to_path_buf).ok_or(
157                FindCargoWorkspaceRootError::GetParentDirectory(root_cargo_toml),
158            )
159        })
160}
161
162#[derive(thiserror::Error, Debug)]
163pub enum FindCargoWorkspaceRootError {
164    #[error("Couldn't get value of CARGO environment variable: {0}")]
165    GetCargoEnv(#[source] std::env::VarError),
166    #[error("Error while spawning Cargo process: {0}")]
167    SpawnCommand(#[source] std::io::Error),
168    #[error("Unexpected Cargo exit status ({}) while attempting to read workspace root", exit_code_or_unknown(*.0))]
169    CommandFailure(std::process::ExitStatus),
170    #[error("Couldn't locate a Cargo workspace within {0} or its parent directories")]
171    GetParentDirectory(PathBuf),
172}
173
174fn exit_code_or_unknown(exit_status: std::process::ExitStatus) -> String {
175    exit_status
176        .code()
177        .map_or_else(|| String::from("<unknown>"), |code| code.to_string())
178}