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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
use crate::build::{BuildContext, InnerBuildResult};
use crate::buildpack::Buildpack;
use crate::data::buildpack::BuildpackApi;
use crate::detect::{DetectContext, InnerDetectResult};
use crate::error::Error;
use crate::platform::Platform;
use crate::sbom::cnb_sbom_path;
use crate::target::ContextTarget;
#[cfg(feature = "trace")]
use crate::tracing::start_trace;
use crate::util::is_not_found_error_kind;
use crate::{exit_code, TomlFileError, LIBCNB_SUPPORTED_BUILDPACK_API};
use libcnb_common::toml_file::{read_toml_file, write_toml_file};
use libcnb_data::buildpack::ComponentBuildpackDescriptor;
use libcnb_data::store::Store;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::process::exit;
use std::{env, fs};

/// Main entry point for this framework.
///
/// The Buildpack API requires us to have separate entry points for each of `bin/{detect,build}`.
/// In order to save the compile time and buildpack size of having two very similar binaries, a
/// single binary is built instead, with the filename by which it is invoked being used to determine
/// the mode in which it is being run. The desired filenames are then created as symlinks or
/// hard links to this single binary.
///
/// Currently symlinks are recommended over hard hard links due to [buildpacks/pack#1286](https://github.com/buildpacks/pack/issues/1286).
///
/// Don't implement this directly and use the [`buildpack_main`] macro instead!
#[doc(hidden)]
pub fn libcnb_runtime<B: Buildpack>(buildpack: &B) {
    // Before we do anything else, we must validate that the Buildpack's API version
    // matches that supported by libcnb, to improve the UX in cases where the lifecycle
    // passes us arguments or env vars we don't expect, due to changes between API versions.
    // We use a cut-down buildpack descriptor type, to ensure we can still read the API
    // version even if the rest of buildpack.toml doesn't match the spec (or the buildpack's
    // chosen custom `metadata` type).
    match read_buildpack_descriptor::<BuildpackDescriptorApiOnly, B::Error>() {
        Ok(buildpack_descriptor) => {
            if buildpack_descriptor.api != LIBCNB_SUPPORTED_BUILDPACK_API {
                eprintln!("Error: Cloud Native Buildpack API mismatch");
                eprintln!(
                    "This buildpack uses Cloud Native Buildpacks API version {} (specified in buildpack.toml).",
                    &buildpack_descriptor.api,
                );
                eprintln!("However, the underlying libcnb.rs library only supports CNB API {LIBCNB_SUPPORTED_BUILDPACK_API}.");
                exit(exit_code::GENERIC_CNB_API_VERSION_ERROR)
            }
        }
        Err(libcnb_error) => {
            // This case will likely never occur, since Pack/lifecycle validates each buildpack's
            // `buildpack.toml` before the buildpack even runs, so the file being missing or the
            // `api` key not being set will have already resulted in an error much earlier.
            eprintln!("Error: Unable to determine Buildpack API version");
            eprintln!("Cause: {libcnb_error}");
            exit(exit_code::GENERIC_CNB_API_VERSION_ERROR);
        }
    }

    let args: Vec<String> = env::args().collect();

    // Using `std::env::args()` instead of `std::env::current_exe()` since the latter resolves
    // symlinks to their target on some platforms, whereas we need the original filename.
    let current_exe = args.first();
    let current_exe_file_name = current_exe
        .map(Path::new)
        .and_then(Path::file_name)
        .and_then(OsStr::to_str);

    let result = match current_exe_file_name {
        Some("detect") => libcnb_runtime_detect(
            buildpack,
            DetectArgs::parse(&args).unwrap_or_else(|parse_error| match parse_error {
                DetectArgsParseError::InvalidArguments => {
                    eprintln!("Usage: detect <platform_dir> <buildplan>");
                    eprintln!(
                        "https://github.com/buildpacks/spec/blob/main/buildpack.md#detection"
                    );
                    exit(exit_code::GENERIC_UNSPECIFIED_ERROR);
                }
            }),
        ),
        Some("build") => libcnb_runtime_build(
            buildpack,
            BuildArgs::parse(&args).unwrap_or_else(|parse_error| match parse_error {
                BuildArgsParseError::InvalidArguments => {
                    eprintln!("Usage: build <layers> <platform> <plan>");
                    eprintln!("https://github.com/buildpacks/spec/blob/main/buildpack.md#build");
                    exit(exit_code::GENERIC_UNSPECIFIED_ERROR);
                }
            }),
        ),
        other => {
            eprintln!(
                "Error: Expected the name of this executable to be 'detect' or 'build', but it was '{}'",
                other.unwrap_or("<unknown>")
            );
            eprintln!("The executable name is used to determine the current buildpack phase.");
            eprintln!("You might want to create 'detect' and 'build' links to this executable and run those instead.");
            exit(exit_code::GENERIC_UNEXPECTED_EXECUTABLE_NAME_ERROR)
        }
    };

    match result {
        Ok(code) => exit(code),
        Err(libcnb_error) => {
            buildpack.on_error(libcnb_error);
            exit(exit_code::GENERIC_UNSPECIFIED_ERROR);
        }
    }
}

/// Detect entry point for this framework.
///
/// Exposed only to allow for advanced use-cases where detect is programmatically invoked.
#[doc(hidden)]
pub fn libcnb_runtime_detect<B: Buildpack>(
    buildpack: &B,
    args: DetectArgs,
) -> crate::Result<i32, B::Error> {
    let app_dir = env::current_dir().map_err(Error::CannotDetermineAppDirectory)?;

    let buildpack_dir = read_buildpack_dir()?;

    let buildpack_descriptor: ComponentBuildpackDescriptor<<B as Buildpack>::Metadata> =
        read_buildpack_descriptor()?;

    #[cfg(feature = "trace")]
    let mut trace = start_trace(&buildpack_descriptor.buildpack, "detect");

    #[cfg(feature = "trace")]
    let mut trace_error = |err: &dyn std::error::Error| {
        trace.set_error(err);
    };

    #[cfg(not(feature = "trace"))]
    let mut trace_error = |_: &dyn std::error::Error| {};

    let platform = B::Platform::from_path(&args.platform_dir_path)
        .map_err(Error::CannotCreatePlatformFromPath)
        .inspect_err(|err| trace_error(err))?;

    let build_plan_path = args.build_plan_path;

    let target = context_target().inspect_err(|err| trace_error(err))?;

    let detect_context = DetectContext {
        app_dir,
        buildpack_dir,
        target,
        platform,
        buildpack_descriptor,
    };

    let detect_result = buildpack
        .detect(detect_context)
        .inspect_err(|err| trace_error(err))?;

    match detect_result.0 {
        InnerDetectResult::Fail => {
            #[cfg(feature = "trace")]
            trace.add_event("detect-failed");
            Ok(exit_code::DETECT_DETECTION_FAILED)
        }
        InnerDetectResult::Pass { build_plan } => {
            if let Some(build_plan) = build_plan {
                write_toml_file(&build_plan, build_plan_path)
                    .map_err(Error::CannotWriteBuildPlan)
                    .inspect_err(|err| trace_error(err))?;
            }
            #[cfg(feature = "trace")]
            trace.add_event("detect-passed");
            Ok(exit_code::DETECT_DETECTION_PASSED)
        }
    }
}

/// Build entry point for this framework.
///
/// Exposed only to allow for advanced use-cases where build is programmatically invoked.
#[doc(hidden)]
#[allow(clippy::too_many_lines)]
pub fn libcnb_runtime_build<B: Buildpack>(
    buildpack: &B,
    args: BuildArgs,
) -> crate::Result<i32, B::Error> {
    let layers_dir = args.layers_dir_path;

    let app_dir = env::current_dir().map_err(Error::CannotDetermineAppDirectory)?;

    let buildpack_dir = read_buildpack_dir()?;

    let buildpack_descriptor: ComponentBuildpackDescriptor<<B as Buildpack>::Metadata> =
        read_buildpack_descriptor()?;

    #[cfg(feature = "trace")]
    let mut trace = start_trace(&buildpack_descriptor.buildpack, "build");

    #[cfg(feature = "trace")]
    let mut trace_error = |err: &dyn std::error::Error| {
        trace.set_error(err);
    };

    #[cfg(not(feature = "trace"))]
    let mut trace_error = |_: &dyn std::error::Error| {};

    let platform = Platform::from_path(&args.platform_dir_path)
        .map_err(Error::CannotCreatePlatformFromPath)
        .inspect_err(|err| trace_error(err))?;

    let buildpack_plan = read_toml_file(&args.buildpack_plan_path)
        .map_err(Error::CannotReadBuildpackPlan)
        .inspect_err(|err| trace_error(err))?;

    let store = match read_toml_file::<Store>(layers_dir.join("store.toml")) {
        Err(TomlFileError::IoError(io_error)) if is_not_found_error_kind(&io_error) => Ok(None),
        other => other.map(Some),
    }
    .map_err(Error::CannotReadStore)
    .inspect_err(|err| trace_error(err))?;

    let target = context_target().inspect_err(|err| trace_error(err))?;

    let build_context = BuildContext {
        layers_dir: layers_dir.clone(),
        app_dir,
        platform,
        target,
        buildpack_plan,
        buildpack_dir,
        buildpack_descriptor,
        store,
    };

    let build_result = buildpack
        .build(build_context)
        .inspect_err(|err| trace_error(err))?;

    match build_result.0 {
        InnerBuildResult::Pass {
            launch,
            store,
            build_sboms,
            launch_sboms,
        } => {
            if let Some(launch) = launch {
                write_toml_file(&launch, layers_dir.join("launch.toml"))
                    .map_err(Error::CannotWriteLaunch)
                    .inspect_err(|err| trace_error(err))?;
            };

            if let Some(store) = store {
                write_toml_file(&store, layers_dir.join("store.toml"))
                    .map_err(Error::CannotWriteStore)
                    .inspect_err(|err| trace_error(err))?;
            };

            for build_sbom in build_sboms {
                fs::write(
                    cnb_sbom_path(&build_sbom.format, &layers_dir, "build"),
                    &build_sbom.data,
                )
                .map_err(Error::CannotWriteBuildSbom)
                .inspect_err(|err| trace_error(err))?;
            }

            for launch_sbom in launch_sboms {
                fs::write(
                    cnb_sbom_path(&launch_sbom.format, &layers_dir, "launch"),
                    &launch_sbom.data,
                )
                .map_err(Error::CannotWriteLaunchSbom)
                .inspect_err(|err| trace_error(err))?;
            }

            #[cfg(feature = "trace")]
            trace.add_event("build-success");
            Ok(exit_code::GENERIC_SUCCESS)
        }
    }
}

// A partial representation of buildpack.toml that contains only the Buildpack API version,
// so that the version can still be read when the buildpack descriptor doesn't match the
// supported spec version.
#[derive(Deserialize)]
struct BuildpackDescriptorApiOnly {
    api: BuildpackApi,
}

#[doc(hidden)]
pub struct DetectArgs {
    pub platform_dir_path: PathBuf,
    pub build_plan_path: PathBuf,
}

impl DetectArgs {
    pub fn parse(args: &[String]) -> Result<Self, DetectArgsParseError> {
        if let [_, platform_dir_path, build_plan_path] = args {
            Ok(Self {
                platform_dir_path: PathBuf::from(platform_dir_path),
                build_plan_path: PathBuf::from(build_plan_path),
            })
        } else {
            Err(DetectArgsParseError::InvalidArguments)
        }
    }
}

#[derive(Debug)]
#[doc(hidden)]
pub enum DetectArgsParseError {
    InvalidArguments,
}

#[doc(hidden)]
pub struct BuildArgs {
    pub layers_dir_path: PathBuf,
    pub platform_dir_path: PathBuf,
    pub buildpack_plan_path: PathBuf,
}

impl BuildArgs {
    pub fn parse(args: &[String]) -> Result<Self, BuildArgsParseError> {
        if let [_, layers_dir_path, platform_dir_path, buildpack_plan_path] = args {
            Ok(Self {
                layers_dir_path: PathBuf::from(layers_dir_path),
                platform_dir_path: PathBuf::from(platform_dir_path),
                buildpack_plan_path: PathBuf::from(buildpack_plan_path),
            })
        } else {
            Err(BuildArgsParseError::InvalidArguments)
        }
    }
}

#[derive(Debug)]
#[doc(hidden)]
pub enum BuildArgsParseError {
    InvalidArguments,
}

fn read_buildpack_dir<E: Debug>() -> crate::Result<PathBuf, E> {
    env::var("CNB_BUILDPACK_DIR")
        .map_err(Error::CannotDetermineBuildpackDirectory)
        .map(PathBuf::from)
}

fn read_buildpack_descriptor<BD: DeserializeOwned, E: Debug>() -> crate::Result<BD, E> {
    read_buildpack_dir().and_then(|buildpack_dir| {
        read_toml_file(buildpack_dir.join("buildpack.toml"))
            .map_err(Error::CannotReadBuildpackDescriptor)
    })
}

fn context_target<E>() -> crate::Result<ContextTarget, E>
where
    E: Debug,
{
    let os = env::var("CNB_TARGET_OS").map_err(Error::CannotDetermineTargetOs)?;
    let arch = env::var("CNB_TARGET_ARCH").map_err(Error::CannotDetermineTargetArch)?;
    let arch_variant = env::var("CNB_TARGET_ARCH_VARIANT").ok();
    let distro_name = env::var("CNB_TARGET_DISTRO_NAME").ok();
    let distro_version = env::var("CNB_TARGET_DISTRO_VERSION").ok();

    Ok(ContextTarget {
        os,
        arch,
        arch_variant,
        distro_name,
        distro_version,
    })
}