libcnb_data/buildpack/
mod.rs

1mod api;
2mod id;
3mod stack;
4mod target;
5mod version;
6
7use crate::generic::GenericMetadata;
8use crate::sbom::SbomFormat;
9pub use api::*;
10pub use id::*;
11use serde::Deserialize;
12pub use stack::*;
13use std::collections::HashSet;
14pub use target::*;
15pub use version::*;
16
17/// Data structures for the Buildpack descriptor (buildpack.toml).
18///
19/// For parsing of [buildpack.toml](https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml)
20/// files when support for multiple types of buildpack is required.
21///
22/// When a specific buildpack type is expected, use [`ComponentBuildpackDescriptor`] or
23/// [`CompositeBuildpackDescriptor`] directly instead, since they allow for more detailed
24/// error messages if parsing fails.
25///
26/// # Example:
27/// ```
28/// use libcnb_data::buildpack::BuildpackDescriptor;
29///
30/// let toml_str = r#"
31/// api = "0.10"
32///
33/// [buildpack]
34/// id = "foo/bar"
35/// name = "Bar Buildpack"
36/// version = "0.0.1"
37/// homepage = "https://www.foo.com/bar"
38/// clear-env = false
39/// description = "A buildpack for Foo Bar"
40/// keywords = ["foo"]
41///
42/// [[buildpack.licenses]]
43/// type = "BSD-3-Clause"
44/// "#;
45///
46/// let buildpack_descriptor =
47///     toml::from_str::<BuildpackDescriptor>(toml_str)
48///         .expect("buildpack.toml did not match a known type!");
49/// match buildpack_descriptor {
50///     BuildpackDescriptor::Component(buildpack) => {
51///         println!("Found component buildpack: {}", buildpack.buildpack.id);
52///     }
53///     BuildpackDescriptor::Composite(buildpack) => {
54///         println!("Found composite buildpack: {}", buildpack.buildpack.id);
55///     }
56/// };
57/// ```
58#[derive(Deserialize, Debug)]
59#[serde(untagged)]
60pub enum BuildpackDescriptor<BM = GenericMetadata> {
61    Component(ComponentBuildpackDescriptor<BM>),
62    Composite(CompositeBuildpackDescriptor<BM>),
63}
64
65impl<BM> BuildpackDescriptor<BM> {
66    pub fn buildpack(&self) -> &Buildpack {
67        match self {
68            BuildpackDescriptor::Component(descriptor) => &descriptor.buildpack,
69            BuildpackDescriptor::Composite(descriptor) => &descriptor.buildpack,
70        }
71    }
72}
73
74/// Data structure for the Buildpack descriptor (buildpack.toml) of a component buildpack.
75///
76/// Representation of [buildpack.toml](https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml)
77/// when the buildpack is a component buildpack - one that implements the Buildpack Interface
78/// (ie: contains `/bin/detect` and `/bin/build` executables).
79///
80/// If support for multiple buildpack types is required, use [`BuildpackDescriptor`] instead.
81///
82/// # Example:
83/// ```
84/// use libcnb_data::buildpack::{BuildpackTarget, ComponentBuildpackDescriptor};
85/// use libcnb_data::buildpack_id;
86///
87/// let toml_str = r#"
88/// api = "0.10"
89///
90/// [buildpack]
91/// id = "foo/bar"
92/// name = "Bar Buildpack"
93/// version = "0.0.1"
94/// homepage = "https://www.foo.com/bar"
95/// clear-env = false
96/// description = "A buildpack for Foo Bar"
97/// keywords = ["foo"]
98///
99/// [[buildpack.licenses]]
100/// type = "BSD-3-Clause"
101///
102/// [[targets]]
103/// os = "linux"
104/// "#;
105///
106/// let buildpack_descriptor =
107///     toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap();
108/// assert_eq!(buildpack_descriptor.buildpack.id, buildpack_id!("foo/bar"));
109/// assert_eq!(
110///     buildpack_descriptor.targets,
111///     [BuildpackTarget {
112///         os: Some(String::from("linux")),
113///         arch: None,
114///         variant: None,
115///         distros: Vec::new()
116///     }]
117/// );
118/// ```
119#[derive(Deserialize, Debug)]
120#[serde(deny_unknown_fields)]
121pub struct ComponentBuildpackDescriptor<BM = GenericMetadata> {
122    pub api: BuildpackApi,
123    pub buildpack: Buildpack,
124    #[serde(default, skip_serializing_if = "Vec::is_empty")]
125    pub stacks: Vec<Stack>,
126    #[serde(default, skip_serializing_if = "Vec::is_empty")]
127    pub targets: Vec<BuildpackTarget>,
128    pub metadata: BM,
129    // As of 2024-02-09, the CNB spec does not forbid component buildpacks
130    // to contain `order`. This is a change from buildpack API 0.9 where `order`
131    // was disallowed in component buildpacks. However, `pack` does not allow this.
132    // We believe this to be a spec error and libcnb.rs does intentionally not support this.
133}
134
135/// Data structure for the Buildpack descriptor (buildpack.toml) of a composite buildpack.
136///
137/// Representation of [buildpack.toml](https://github.com/buildpacks/spec/blob/main/buildpack.md#buildpacktoml-toml)
138/// when the buildpack is a composite buildpack - one that does not implement the Buildpack Interface
139/// itself (ie: does not contain `/bin/detect` and `/bin/build` executables) but instead references
140/// other buildpacks via an order definition.
141///
142/// If support for multiple buildpack types is required, use [`BuildpackDescriptor`] instead.
143///
144/// # Example:
145/// ```
146/// use libcnb_data::buildpack::CompositeBuildpackDescriptor;
147/// use libcnb_data::buildpack_id;
148///
149/// let toml_str = r#"
150/// api = "0.10"
151///
152/// [buildpack]
153/// id = "foo/bar"
154/// name = "Bar Buildpack"
155/// version = "0.0.1"
156/// homepage = "https://www.foo.com/bar"
157/// clear-env = false
158/// description = "A buildpack for Foo Bar"
159/// keywords = ["foo"]
160///
161/// [[buildpack.licenses]]
162/// type = "BSD-3-Clause"
163///
164/// [[order]]
165///
166/// [[order.group]]
167/// id = "foo/baz"
168/// version = "0.0.1"
169/// "#;
170///
171/// let buildpack_descriptor =
172///     toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap();
173/// assert_eq!(buildpack_descriptor.buildpack.id, buildpack_id!("foo/bar"));
174/// ```
175#[derive(Deserialize, Debug)]
176#[serde(deny_unknown_fields)]
177pub struct CompositeBuildpackDescriptor<BM = GenericMetadata> {
178    pub api: BuildpackApi,
179    pub buildpack: Buildpack,
180    pub order: Vec<Order>,
181    pub metadata: BM,
182    // As of 2024-02-09, the CNB spec does not forbid composite buildpacks
183    // to contain `targets`. This is a change from buildpack API 0.9 where `stack`
184    // was disallowed in composite buildpacks. However, `pack` does not allow this.
185    // We believe this to be a spec error and libcnb.rs does intentionally not support this.
186}
187
188#[derive(Deserialize, Debug)]
189#[serde(deny_unknown_fields)]
190pub struct Buildpack {
191    pub id: BuildpackId,
192    pub name: Option<String>,
193    pub version: BuildpackVersion,
194    pub homepage: Option<String>,
195    #[serde(default, rename = "clear-env")]
196    pub clear_env: bool,
197    pub description: Option<String>,
198    #[serde(default, skip_serializing_if = "Vec::is_empty")]
199    pub keywords: Vec<String>,
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub licenses: Vec<License>,
202    #[serde(
203        default,
204        rename = "sbom-formats",
205        skip_serializing_if = "HashSet::is_empty"
206    )]
207    pub sbom_formats: HashSet<SbomFormat>,
208}
209
210#[derive(Deserialize, Debug, Eq, PartialEq)]
211#[serde(deny_unknown_fields)]
212pub struct License {
213    pub r#type: Option<String>,
214    pub uri: Option<String>,
215}
216
217#[derive(Deserialize, Debug, Eq, PartialEq)]
218#[serde(deny_unknown_fields)]
219pub struct Order {
220    pub group: Vec<Group>,
221}
222
223#[derive(Deserialize, Debug, Eq, PartialEq)]
224#[serde(deny_unknown_fields)]
225pub struct Group {
226    pub id: BuildpackId,
227    pub version: BuildpackVersion,
228    #[serde(default)]
229    pub optional: bool,
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    #[allow(clippy::too_many_lines)]
238    fn deserialize_component_buildpack() {
239        let toml_str = r#"
240api = "0.10"
241
242[buildpack]
243id = "foo/bar"
244name = "Bar Buildpack"
245version = "0.0.1"
246homepage = "https://example.tld"
247clear-env = true
248description = "A buildpack for Foo Bar"
249keywords = ["foo", "bar"]
250# Duplication of the Syft entry is intentional!
251sbom-formats = ["application/vnd.cyclonedx+json", "application/spdx+json", "application/vnd.syft+json", "application/vnd.syft+json"]
252
253[[buildpack.licenses]]
254type = "BSD-3-Clause"
255
256[[buildpack.licenses]]
257type = "Custom license with type and URI"
258uri = "https://example.tld/my-license"
259
260[[buildpack.licenses]]
261uri = "https://example.tld/my-license"
262
263[[stacks]]
264id = "heroku-20"
265
266[[stacks]]
267id = "io.buildpacks.stacks.bionic"
268mixins = []
269
270[[stacks]]
271id = "io.buildpacks.stacks.focal"
272mixins = ["build:jq", "wget"]
273
274[[stacks]]
275id = "*"
276
277[[targets]]
278os = "linux"
279arch = "amd64"
280[[targets.distros]]
281name = "ubuntu"
282version = "18.04"
283
284[[targets]]
285os = "linux"
286arch = "arm"
287variant = "v8"
288
289[[targets]]
290os = "windows"
291arch = "amd64"
292
293[[targets]]
294
295[metadata]
296checksum = "abc123"
297        "#;
298
299        let buildpack_descriptor =
300            toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap();
301
302        assert_eq!(
303            buildpack_descriptor.api,
304            BuildpackApi {
305                major: 0,
306                minor: 10
307            }
308        );
309        assert_eq!(
310            buildpack_descriptor.buildpack.id,
311            "foo/bar".parse().unwrap()
312        );
313        assert_eq!(
314            buildpack_descriptor.buildpack.name,
315            Some(String::from("Bar Buildpack"))
316        );
317        assert_eq!(
318            buildpack_descriptor.buildpack.version,
319            BuildpackVersion::new(0, 0, 1)
320        );
321        assert_eq!(
322            buildpack_descriptor.buildpack.homepage,
323            Some(String::from("https://example.tld"))
324        );
325        assert!(buildpack_descriptor.buildpack.clear_env);
326        assert_eq!(
327            buildpack_descriptor.buildpack.description,
328            Some(String::from("A buildpack for Foo Bar"))
329        );
330        assert_eq!(
331            buildpack_descriptor.buildpack.keywords,
332            [String::from("foo"), String::from("bar")]
333        );
334        assert_eq!(
335            buildpack_descriptor.buildpack.licenses,
336            [
337                License {
338                    r#type: Some(String::from("BSD-3-Clause")),
339                    uri: None
340                },
341                License {
342                    r#type: Some(String::from("Custom license with type and URI")),
343                    uri: Some(String::from("https://example.tld/my-license"))
344                },
345                License {
346                    r#type: None,
347                    uri: Some(String::from("https://example.tld/my-license"))
348                }
349            ]
350        );
351        assert_eq!(
352            buildpack_descriptor.buildpack.sbom_formats,
353            HashSet::from([
354                SbomFormat::SyftJson,
355                SbomFormat::CycloneDxJson,
356                SbomFormat::SpdxJson
357            ])
358        );
359        assert_eq!(
360            buildpack_descriptor.stacks,
361            [
362                Stack {
363                    id: String::from("heroku-20"),
364                    mixins: Vec::new(),
365                },
366                Stack {
367                    id: String::from("io.buildpacks.stacks.bionic"),
368                    mixins: Vec::new(),
369                },
370                Stack {
371                    id: String::from("io.buildpacks.stacks.focal"),
372                    mixins: vec![String::from("build:jq"), String::from("wget")]
373                },
374                Stack {
375                    id: String::from("*"),
376                    mixins: Vec::new()
377                }
378            ]
379        );
380        assert_eq!(
381            buildpack_descriptor.targets,
382            [
383                BuildpackTarget {
384                    os: Some(String::from("linux")),
385                    arch: Some(String::from("amd64")),
386                    variant: None,
387                    distros: vec![Distro {
388                        name: String::from("ubuntu"),
389                        version: String::from("18.04"),
390                    }],
391                },
392                BuildpackTarget {
393                    os: Some(String::from("linux")),
394                    arch: Some(String::from("arm")),
395                    variant: Some(String::from("v8")),
396                    distros: Vec::new(),
397                },
398                BuildpackTarget {
399                    os: Some(String::from("windows")),
400                    arch: Some(String::from("amd64")),
401                    variant: None,
402                    distros: Vec::new(),
403                },
404                BuildpackTarget {
405                    os: None,
406                    arch: None,
407                    variant: None,
408                    distros: Vec::new()
409                }
410            ]
411        );
412        assert_eq!(
413            buildpack_descriptor.metadata.unwrap().get("checksum"),
414            Some(&toml::value::Value::try_from("abc123").unwrap())
415        );
416    }
417
418    #[test]
419    fn deserialize_composite_buildpack() {
420        let toml_str = r#"
421api = "0.10"
422
423[buildpack]
424id = "foo/bar"
425name = "Bar Buildpack"
426version = "0.0.1"
427homepage = "https://example.tld"
428clear-env = true
429description = "A buildpack for Foo Bar"
430keywords = ["foo", "bar"]
431
432[[buildpack.licenses]]
433type = "BSD-3-Clause"
434
435[[buildpack.licenses]]
436type = "Custom license with type and URI"
437uri = "https://example.tld/my-license"
438
439[[buildpack.licenses]]
440uri = "https://example.tld/my-license"
441
442[[order]]
443
444[[order.group]]
445id = "foo/bar"
446version = "0.0.1"
447
448[[order.group]]
449id = "foo/baz"
450version = "0.1.0"
451optional = true
452
453[metadata]
454checksum = "abc123"
455        "#;
456
457        let buildpack_descriptor =
458            toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap();
459
460        assert_eq!(
461            buildpack_descriptor.api,
462            BuildpackApi {
463                major: 0,
464                minor: 10
465            }
466        );
467        assert_eq!(
468            buildpack_descriptor.buildpack.id,
469            "foo/bar".parse().unwrap()
470        );
471        assert_eq!(
472            buildpack_descriptor.buildpack.name,
473            Some(String::from("Bar Buildpack"))
474        );
475        assert_eq!(
476            buildpack_descriptor.buildpack.version,
477            BuildpackVersion::new(0, 0, 1)
478        );
479        assert_eq!(
480            buildpack_descriptor.buildpack.homepage,
481            Some(String::from("https://example.tld"))
482        );
483        assert!(buildpack_descriptor.buildpack.clear_env);
484        assert_eq!(
485            buildpack_descriptor.buildpack.description,
486            Some(String::from("A buildpack for Foo Bar"))
487        );
488        assert_eq!(
489            buildpack_descriptor.buildpack.keywords,
490            [String::from("foo"), String::from("bar")]
491        );
492        assert_eq!(
493            buildpack_descriptor.buildpack.licenses,
494            [
495                License {
496                    r#type: Some(String::from("BSD-3-Clause")),
497                    uri: None
498                },
499                License {
500                    r#type: Some(String::from("Custom license with type and URI")),
501                    uri: Some(String::from("https://example.tld/my-license"))
502                },
503                License {
504                    r#type: None,
505                    uri: Some(String::from("https://example.tld/my-license"))
506                }
507            ]
508        );
509        assert_eq!(
510            buildpack_descriptor.order,
511            [Order {
512                group: vec![
513                    Group {
514                        id: "foo/bar".parse().unwrap(),
515                        version: BuildpackVersion::new(0, 0, 1),
516                        optional: false
517                    },
518                    Group {
519                        id: "foo/baz".parse().unwrap(),
520                        version: BuildpackVersion::new(0, 1, 0),
521                        optional: true
522                    }
523                ]
524            }]
525        );
526        assert_eq!(
527            buildpack_descriptor.metadata.unwrap().get("checksum"),
528            Some(&toml::value::Value::try_from("abc123").unwrap())
529        );
530    }
531
532    #[test]
533    fn deserialize_minimal_component_buildpack() {
534        let toml_str = r#"
535api = "0.10"
536
537[buildpack]
538id = "foo/bar"
539version = "0.0.1"
540        "#;
541
542        let buildpack_descriptor =
543            toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap();
544
545        assert_eq!(
546            buildpack_descriptor.api,
547            BuildpackApi {
548                major: 0,
549                minor: 10
550            }
551        );
552        assert_eq!(
553            buildpack_descriptor.buildpack.id,
554            "foo/bar".parse().unwrap()
555        );
556        assert_eq!(buildpack_descriptor.buildpack.name, None);
557        assert_eq!(
558            buildpack_descriptor.buildpack.version,
559            BuildpackVersion::new(0, 0, 1)
560        );
561        assert_eq!(buildpack_descriptor.buildpack.homepage, None);
562        assert!(!buildpack_descriptor.buildpack.clear_env);
563        assert_eq!(buildpack_descriptor.buildpack.description, None);
564        assert_eq!(
565            buildpack_descriptor.buildpack.keywords,
566            Vec::<String>::new()
567        );
568        assert_eq!(buildpack_descriptor.buildpack.licenses, Vec::new());
569        assert_eq!(buildpack_descriptor.buildpack.sbom_formats, HashSet::new());
570        assert_eq!(buildpack_descriptor.stacks, []);
571        assert_eq!(buildpack_descriptor.targets, []);
572        assert_eq!(buildpack_descriptor.metadata, None);
573    }
574
575    #[test]
576    fn deserialize_minimal_composite_buildpack() {
577        let toml_str = r#"
578api = "0.10"
579
580[buildpack]
581id = "foo/bar"
582version = "0.0.1"
583
584[[order]]
585
586[[order.group]]
587id = "foo/bar"
588version = "0.0.1"
589"#;
590
591        let buildpack_descriptor =
592            toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap();
593
594        assert_eq!(
595            buildpack_descriptor.api,
596            BuildpackApi {
597                major: 0,
598                minor: 10
599            }
600        );
601        assert_eq!(
602            buildpack_descriptor.buildpack.id,
603            "foo/bar".parse().unwrap()
604        );
605        assert_eq!(buildpack_descriptor.buildpack.name, None);
606        assert_eq!(
607            buildpack_descriptor.buildpack.version,
608            BuildpackVersion::new(0, 0, 1)
609        );
610        assert_eq!(buildpack_descriptor.buildpack.homepage, None);
611        assert!(!buildpack_descriptor.buildpack.clear_env);
612        assert_eq!(buildpack_descriptor.buildpack.description, None);
613        assert_eq!(
614            buildpack_descriptor.buildpack.keywords,
615            Vec::<String>::new()
616        );
617        assert_eq!(buildpack_descriptor.buildpack.licenses, Vec::new());
618        assert_eq!(
619            buildpack_descriptor.order,
620            [Order {
621                group: vec![Group {
622                    id: "foo/bar".parse().unwrap(),
623                    version: BuildpackVersion::new(0, 0, 1),
624                    optional: false
625                }]
626            }]
627        );
628        assert_eq!(buildpack_descriptor.metadata, None);
629    }
630
631    #[test]
632    fn deserialize_buildpackdescriptor_component() {
633        let toml_str = r#"
634api = "0.10"
635
636[buildpack]
637id = "foo/bar"
638version = "0.0.1"
639        "#;
640
641        let buildpack_descriptor = toml::from_str::<BuildpackDescriptor>(toml_str).unwrap();
642        assert!(matches!(
643            buildpack_descriptor,
644            BuildpackDescriptor::Component(_)
645        ));
646    }
647
648    #[test]
649    fn deserialize_buildpackdescriptor_composite() {
650        let toml_str = r#"
651api = "0.10"
652
653[buildpack]
654id = "foo/bar"
655version = "0.0.1"
656
657[[order]]
658
659[[order.group]]
660id = "foo/baz"
661version = "0.0.1"
662        "#;
663
664        let buildpack_descriptor = toml::from_str::<BuildpackDescriptor>(toml_str).unwrap();
665        assert!(matches!(
666            buildpack_descriptor,
667            BuildpackDescriptor::Composite(_)
668        ));
669    }
670
671    #[test]
672    fn reject_buildpack_with_both_targets_and_order() {
673        let toml_str = r#"
674api = "0.10"
675
676[buildpack]
677id = "foo/bar"
678version = "0.0.1"
679
680[[targets]]
681os = "linux"
682
683[[order]]
684
685[[order.group]]
686id = "foo/baz"
687version = "0.0.1"
688"#;
689
690        let err = toml::from_str::<BuildpackDescriptor>(toml_str).unwrap_err();
691        assert_eq!(
692            err.to_string(),
693            "data did not match any variant of untagged enum BuildpackDescriptor\n"
694        );
695
696        let err = toml::from_str::<ComponentBuildpackDescriptor>(toml_str).unwrap_err();
697        assert!(err.to_string().contains("unknown field `order`"));
698
699        let err = toml::from_str::<CompositeBuildpackDescriptor>(toml_str).unwrap_err();
700        assert!(err.to_string().contains("unknown field `targets`"));
701    }
702}