1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use crate::build::build_buildpack_binaries;
use crate::buildpack_kind::{determine_buildpack_kind, BuildpackKind};
use crate::package_descriptor::{normalize_package_descriptor, NormalizePackageDescriptorError};
use crate::{assemble_buildpack_directory, CargoProfile};
use cargo_metadata::MetadataCommand;
use libcnb_common::toml_file::{read_toml_file, write_toml_file, TomlFileError};
use libcnb_data::buildpack::BuildpackId;
use libcnb_data::package_descriptor::PackageDescriptor;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};

/// Packages either a libcnb.rs or a composite buildpack.
///
/// # Errors
///
/// Returns `Err` if packaging failed or the given buildpack directory is unsupported.
pub fn package_buildpack(
    buildpack_directory: &Path,
    cargo_profile: CargoProfile,
    target_triple: &str,
    cargo_build_env: &[(OsString, OsString)],
    destination: &Path,
    dependencies: &BTreeMap<BuildpackId, PathBuf>,
) -> Result<(), PackageBuildpackError> {
    match determine_buildpack_kind(buildpack_directory) {
        Some(BuildpackKind::LibCnbRs) => package_libcnb_buildpack(
            buildpack_directory,
            cargo_profile,
            target_triple,
            cargo_build_env,
            destination,
        )
        .map_err(PackageBuildpackError::PackageLibcnbBuildpackError),
        Some(BuildpackKind::Composite) => {
            package_composite_buildpack(buildpack_directory, destination, dependencies)
                .map_err(PackageBuildpackError::PackageCompositeBuildpackError)
        }
        _ => Err(PackageBuildpackError::UnsupportedBuildpack),
    }
}

#[derive(thiserror::Error, Debug)]
pub enum PackageBuildpackError {
    #[error(transparent)]
    PackageCompositeBuildpackError(PackageCompositeBuildpackError),
    #[error(transparent)]
    PackageLibcnbBuildpackError(PackageLibcnbBuildpackError),
    #[error("Buildpack is not supported to be packaged")]
    UnsupportedBuildpack,
}

/// Packages a libcnb.rs buildpack after (cross-) compiling.
///
/// # Errors
///
/// Returns `Err` if compilation or packaging failed.
fn package_libcnb_buildpack(
    buildpack_directory: &Path,
    cargo_profile: CargoProfile,
    target_triple: &str,
    cargo_build_env: &[(OsString, OsString)],
    destination: &Path,
) -> Result<(), PackageLibcnbBuildpackError> {
    let cargo_metadata = MetadataCommand::new()
        .manifest_path(buildpack_directory.join("Cargo.toml"))
        .exec()
        .map_err(PackageLibcnbBuildpackError::CargoMetadataError)?;

    let buildpack_binaries = build_buildpack_binaries(
        buildpack_directory,
        &cargo_metadata,
        cargo_profile,
        cargo_build_env,
        target_triple,
    )
    .map_err(PackageLibcnbBuildpackError::BuildBinariesError)?;

    assemble_buildpack_directory(
        destination,
        buildpack_directory.join("buildpack.toml"),
        &buildpack_binaries,
    )
    .map_err(PackageLibcnbBuildpackError::AssembleBuildpackDirectory)?;

    fs::write(
        destination.join("package.toml"),
        "[buildpack]\nuri = \".\"\n",
    )
    .map_err(PackageLibcnbBuildpackError::WritePackageDescriptor)
}

#[derive(thiserror::Error, Debug)]
pub enum PackageLibcnbBuildpackError {
    #[error("Assembling buildpack directory failed: {0}")]
    AssembleBuildpackDirectory(std::io::Error),
    #[error("Couldn't write package.toml: {0}")]
    WritePackageDescriptor(std::io::Error),
    #[error("Building buildpack binaries failed: {0}")]
    BuildBinariesError(crate::build::BuildBinariesError),
    #[error("Obtaining Cargo metadata failed: {0}")]
    CargoMetadataError(cargo_metadata::Error),
}

/// Packages a composite buildpack.
///
/// Packaging consists of copying `buildpack.toml` as well as `package.toml` to the given
/// destination path.
///
/// In addition, references to libcnb.rs buildpacks in the form of `libcnb:` URIs are resolved and
/// local paths are absolutized so the `package.toml` stays correct after being moved to a
/// different location.
///
/// # Errors
///
/// Returns `Err` if a `libcnb:` URI refers to a buildpack not in `buildpack_paths` or packaging
/// otherwise failed (i.e. I/O errors).
pub fn package_composite_buildpack(
    buildpack_directory: &Path,
    destination: &Path,
    buildpack_paths: &BTreeMap<BuildpackId, PathBuf>,
) -> Result<(), PackageCompositeBuildpackError> {
    fs::copy(
        buildpack_directory.join("buildpack.toml"),
        destination.join("buildpack.toml"),
    )
    .map_err(PackageCompositeBuildpackError::CouldNotCopyBuildpackToml)?;

    let package_descriptor_path = buildpack_directory.join("package.toml");

    let normalized_package_descriptor =
        read_toml_file::<PackageDescriptor>(&package_descriptor_path)
            .map_err(PackageCompositeBuildpackError::CouldNotReadPackageDescriptor)
            .and_then(|package_descriptor| {
                normalize_package_descriptor(
                    &package_descriptor,
                    &package_descriptor_path,
                    buildpack_paths,
                )
                .map_err(PackageCompositeBuildpackError::NormalizePackageDescriptorError)
            })?;

    write_toml_file(
        &normalized_package_descriptor,
        destination.join("package.toml"),
    )
    .map_err(PackageCompositeBuildpackError::CouldNotWritePackageDescriptor)
}

#[derive(thiserror::Error, Debug)]
pub enum PackageCompositeBuildpackError {
    #[error("Couldn't copy buildpack.toml: {0}")]
    CouldNotCopyBuildpackToml(std::io::Error),
    #[error("Couldn't read package.toml: {0}")]
    CouldNotReadPackageDescriptor(TomlFileError),
    #[error("Error while normalizing package.toml: {0}")]
    NormalizePackageDescriptorError(NormalizePackageDescriptorError),
    #[error("Couldn't write package.toml: {0}")]
    CouldNotWritePackageDescriptor(TomlFileError),
}