diff --git a/Cargo.lock b/Cargo.lock index bc8cce5f..15b3a9bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "base64" @@ -59,6 +59,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -72,9 +78,14 @@ dependencies = [ name = "buildpack-heroku-dotnet" version = "0.0.0" dependencies = [ + "inventory", "libcnb", + "libherokubuildpack", + "semver", "serde", + "sha2", "thiserror", + "toml", ] [[package]] @@ -117,9 +128,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.95" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -165,6 +176,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -202,13 +219,26 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide", ] @@ -239,9 +269,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -250,9 +280,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heroku-inventory-utils" @@ -316,6 +346,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inventory" +version = "0.1.0" +source = "git+https://github.com/malax/inventory#f8c8106f4832557f026d33e2a8d098af28643c22" +dependencies = [ + "hex", + "semver", + "serde", + "sha2", + "thiserror", + "toml", +] + [[package]] name = "inventory-updater" version = "0.0.0" @@ -367,9 +410,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libcnb" @@ -418,6 +461,34 @@ dependencies = [ "syn", ] +[[package]] +name = "libherokubuildpack" +version = "0.19.0" +source = "git+https://github.com/heroku/libcnb.rs?branch=malax/layer-api#78ea79b1bfc8d759d7f54fdd4c84e9aa636f58ca" +dependencies = [ + "crossbeam-utils", + "flate2", + "libcnb", + "pathdiff", + "sha2", + "tar", + "termcolor", + "thiserror", + "toml", + "ureq", +] + +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "log" version = "0.4.21" @@ -450,9 +521,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -463,17 +534,29 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -487,6 +570,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.10.4" @@ -547,9 +639,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -564,15 +656,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -599,9 +691,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -642,15 +734,34 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.60" @@ -799,6 +910,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -874,6 +991,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -958,15 +1084,15 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] [[package]] name = "zeroize" -version = "1.8.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63381fa6624bf92130a6b87c0d07380116f80b565c42cf0d754136f0238359ef" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/buildpacks/dotnet/Cargo.toml b/buildpacks/dotnet/Cargo.toml index 4bffece3..80dfddbe 100644 --- a/buildpacks/dotnet/Cargo.toml +++ b/buildpacks/dotnet/Cargo.toml @@ -7,6 +7,11 @@ edition.workspace = true workspace = true [dependencies] +inventory = { git = "https://github.com/malax/inventory", features = ["sha2", "semver"]} libcnb = { git = "https://github.com/heroku/libcnb.rs", branch = "malax/layer-api"} -serde = "1.0.201" +libherokubuildpack = { git = "https://github.com/heroku/libcnb.rs", branch = "malax/layer-api"} +semver = "1.0" +serde = "1" +sha2 = "0.10" thiserror = "1.0.60" +toml = "0.8" diff --git a/buildpacks/dotnet/src/layers/mod.rs b/buildpacks/dotnet/src/layers/mod.rs new file mode 100644 index 00000000..dad92ce9 --- /dev/null +++ b/buildpacks/dotnet/src/layers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod sdk; diff --git a/buildpacks/dotnet/src/layers/sdk.rs b/buildpacks/dotnet/src/layers/sdk.rs new file mode 100644 index 00000000..e19fbdb3 --- /dev/null +++ b/buildpacks/dotnet/src/layers/sdk.rs @@ -0,0 +1,135 @@ +use crate::{DotnetBuildpack, DotnetBuildpackError}; +use inventory::artifact::Artifact; +use libcnb::data::layer_name; +use libcnb::layer::{ + CachedLayerDefinition, InspectExistingAction, InvalidMetadataAction, LayerContents, +}; +use libcnb::layer_env::{LayerEnv, ModificationBehavior, Scope}; +use libherokubuildpack::download::download_file; +use libherokubuildpack::log::log_info; +use libherokubuildpack::tar::decompress_tarball; +use semver::Version; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha512}; +use std::env::temp_dir; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +#[derive(Serialize, Deserialize)] +pub(crate) struct SdkLayerMetadata { + artifact: Artifact, +} + +#[derive(thiserror::Error, Debug)] +pub(crate) enum SdkLayerError { + #[error("Couldn't create tempfile for .NET SDK: {0}")] + TempFile(std::io::Error), + #[error("Couldn't download .NET SDK: {0}")] + Download(libherokubuildpack::download::DownloadError), + #[error("Couldn't decompress .NET SDK: {0}")] + Untar(std::io::Error), + #[error("Error verifying checksum")] + ChecksumVerification, + #[error("Couldn't read tempfile for .NET SDK: {0}")] + ReadTempFile(std::io::Error), +} + +pub(crate) fn handle( + artifact: &Artifact, + context: &libcnb::build::BuildContext, +) -> Result<(), DotnetBuildpackError> { + let sdk_layer = context.cached_layer( + layer_name!("sdk"), + CachedLayerDefinition { + build: true, + launch: true, + invalid_metadata: &|_| InvalidMetadataAction::DeleteLayer, + inspect_existing: &|metadata: &SdkLayerMetadata, _path| { + if &metadata.artifact == artifact { + log_info(format!( + "Reusing cached .NET SDK version: {}", + artifact.version + )); + InspectExistingAction::Keep + } else { + log_info(format!( + "Deleting cached .NET SDK version: {}", + metadata.artifact.version + )); + InspectExistingAction::Delete + } + }, + }, + )?; + + if let LayerContents::Empty { .. } = &sdk_layer.contents { + sdk_layer.replace_metadata(SdkLayerMetadata { + artifact: artifact.clone(), + })?; + + libherokubuildpack::log::log_info(format!( + "Downloading .NET SDK version {} from {}", + artifact.version, artifact.url + )); + + let path = temp_dir().as_path().join("dotnetsdk.tar.gz"); + download_file(&artifact.url, path.clone()).map_err(SdkLayerError::Download)?; + + log_info("Verifying checksum"); + let digest = sha512(path.clone()).map_err(SdkLayerError::ReadTempFile)?; + if artifact.checksum.value != digest { + Err(SdkLayerError::ChecksumVerification)?; + } + + log_info(format!( + "Extracting .NET SDK version: {}", + &artifact.version + )); + + log_info(format!("Installing .NET SDK version {}", &artifact.version)); + decompress_tarball( + &mut File::open(path.clone()).map_err(SdkLayerError::TempFile)?, + sdk_layer.path(), + ) + .map_err(SdkLayerError::Untar)?; + sdk_layer.replace_env( + &LayerEnv::new() + .chainable_insert( + libcnb::layer_env::Scope::All, + ModificationBehavior::Override, + "DOTNET_EnableWriteXorExecute", + "0", + ) + .chainable_insert( + Scope::All, + ModificationBehavior::Prepend, + "PATH", + format!( + "{}:", + sdk_layer + .path() + .as_path() + .to_str() + .expect("layer to be a str") + ), + ), + )?; + }; + + Ok(()) +} + +fn sha512(path: impl AsRef) -> Result, std::io::Error> { + let mut file = File::open(path.as_ref())?; + let mut buffer = [0x00; 10 * 1024]; + let mut digest = sha2::Sha512::default(); + + let mut read = file.read(&mut buffer)?; + while read > 0 { + Digest::update(&mut digest, &buffer[..read]); + read = file.read(&mut buffer)?; + } + + Ok(digest.finalize().to_vec()) +} diff --git a/buildpacks/dotnet/src/main.rs b/buildpacks/dotnet/src/main.rs index c0c214c4..f07b8edd 100644 --- a/buildpacks/dotnet/src/main.rs +++ b/buildpacks/dotnet/src/main.rs @@ -1,21 +1,33 @@ +mod layers; + +use std::env::consts; + +use inventory::artifact::{Arch, Artifact, Os}; +use inventory::inventory::Inventory; use libcnb::build::BuildResultBuilder; -use libcnb::data::layer_name; use libcnb::detect::DetectResultBuilder; use libcnb::generic::{GenericMetadata, GenericPlatform}; -use libcnb::layer::{CachedLayerDefinition, InspectExistingAction, InvalidMetadataAction}; use libcnb::{buildpack_main, Buildpack}; -use serde::{Deserialize, Serialize}; +use libherokubuildpack::log::{log_header, log_info}; +use semver::{Version, VersionReq}; +use sha2::Sha512; + +use crate::layers::sdk::SdkLayerError; buildpack_main! { DotnetBuildpack } struct DotnetBuildpack; #[derive(thiserror::Error, Debug)] -enum DotnetBuildpackError {} - -#[derive(Serialize, Deserialize)] -pub struct SdkLayerMetadata { - sdk_version: String, +enum DotnetBuildpackError { + #[error(transparent)] + SdkLayerError(#[from] SdkLayerError), + #[error("Couldn't parse .NET SDK inventory: {0}")] + InventoryParse(toml::de::Error), + #[error("Couldn't parse .NET SDK version: {0}")] + SemVer(#[from] semver::Error), + #[error("Couldn't resolve .NET SDK version: {0}")] + VersionResolution(semver::VersionReq), } impl Buildpack for DotnetBuildpack { @@ -36,18 +48,36 @@ impl Buildpack for DotnetBuildpack { &self, context: libcnb::build::BuildContext, ) -> libcnb::Result { - let _sdk_layer = context.cached_layer( - layer_name!("sdk"), - CachedLayerDefinition { - build: true, - launch: true, - invalid_metadata: &|_| InvalidMetadataAction::DeleteLayer, - inspect_existing: &|_metadata: &SdkLayerMetadata, _path| { - InspectExistingAction::Keep - }, - }, - )?; + log_header("Resolving .NET SDK version"); + + let artifact = resolve_sdk_artifact().map_err(libcnb::Error::BuildpackError)?; + + log_info(format!("Resolved .NET SDK version: {}", artifact.version)); + + layers::sdk::handle(&artifact, &context).map_err(libcnb::Error::BuildpackError)?; println!("Hello, World!"); BuildResultBuilder::new().build() } } + +const INVENTORY: &str = include_str!("../inventory.toml"); + +fn resolve_sdk_artifact() -> Result, DotnetBuildpackError> { + let inv: Inventory = + toml::from_str(INVENTORY).map_err(DotnetBuildpackError::InventoryParse)?; + + let requirement = VersionReq::parse("8.0")?; + let artifact = match (consts::OS.parse::(), consts::ARCH.parse::()) { + (Ok(os), Ok(arch)) => inv.resolve(os, arch, &requirement), + (_, _) => None, + } + .ok_or(DotnetBuildpackError::VersionResolution(requirement.clone()))?; + + Ok(artifact.clone()) +} + +impl From> for DotnetBuildpackError { + fn from(_value: libcnb::Error) -> Self { + todo!() + } +}