diff --git a/.github/workflows/build_jruby.yml b/.github/workflows/build_jruby.yml index d3f7e4a..2b79100 100644 --- a/.github/workflows/build_jruby.yml +++ b/.github/workflows/build_jruby.yml @@ -51,3 +51,13 @@ jobs: - name: Upload Ruby runtime archive to S3 production if: (!inputs.dry_run) run: aws s3 sync ./output "s3://${S3_BUCKET}" + + after-build-and-upload: + needs: build-and-upload + runs-on: pub-hk-ubuntu-24.04-xlarge + steps: + - name: Update Jruby inventory file locally + uses: peter-evans/create-pull-request@v6 + with: + path: jruby_inventory.toml + title: "Add JRuby ${{inputs.jruby_version}} to inventory" diff --git a/Cargo.lock b/Cargo.lock index 858d91c..62f1b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -126,6 +141,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bullet_stream" version = "0.2.0" @@ -156,6 +180,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.5", +] + [[package]] name = "chunked_transfer" version = "1.5.0" @@ -224,6 +263,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -233,6 +281,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -325,6 +393,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fun_run" version = "0.2.0" @@ -385,6 +463,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gimli" version = "0.29.0" @@ -434,6 +522,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.1.0" @@ -536,6 +630,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -562,6 +679,18 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" +[[package]] +name = "inventory" +version = "0.1.0" +source = "git+https://github.com/Malax/inventory#e4eed7de95e49d441cdf13f9c001644ed9159393" +dependencies = [ + "hex", + "serde", + "sha2", + "thiserror", + "toml", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -596,12 +725,14 @@ name = "jruby_executable" version = "0.1.0" dependencies = [ "bullet_stream", + "chrono", "clap", "flate2", "fs-err", "fun_run", "glob", "indoc", + "inventory", "java-properties", "lazy_static", "nom", @@ -711,6 +842,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1017,18 +1157,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -1046,6 +1186,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1058,24 +1207,41 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shared" version = "0.1.0" dependencies = [ "bullet_stream", + "chrono", "clap", "flate2", "fs-err", + "fs2", "fun_run", "glob", "indoc", + "inventory", "nom", "regex", "reqwest", + "serde", + "sha2", "tar", "tempfile", "thiserror", "tiny_http", + "toml", ] [[package]] @@ -1256,6 +1422,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1308,6 +1508,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -1352,6 +1558,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.1" @@ -1443,6 +1655,37 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1582,6 +1825,15 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winnow" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 1d08439..f9a3104 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,11 @@ tar = "0.4.40" tempfile = "3.10.1" thiserror = "1.0.61" bullet_stream = "0.2.0" +sha2 = "0.10" +toml = "0.8" +chrono = {version = "0.4", features = ["serde"] } +serde = {version = "1.0", features = ["derive"] } +inventory = { git = "https://github.com/Malax/inventory", features = ["sha2"] } + +# File locking (FLOCK) +fs2 = "0.4" diff --git a/jruby_executable/Cargo.toml b/jruby_executable/Cargo.toml index c10b017..ca81df1 100644 --- a/jruby_executable/Cargo.toml +++ b/jruby_executable/Cargo.toml @@ -32,3 +32,5 @@ tar = { workspace = true} tempfile = { workspace = true} thiserror = { workspace = true} bullet_stream = { workspace = true } +inventory = { workspace = true } +chrono = { workspace = true } diff --git a/jruby_executable/src/bin/jruby_build.rs b/jruby_executable/src/bin/jruby_build.rs index 29e60d4..92e59d6 100644 --- a/jruby_executable/src/bin/jruby_build.rs +++ b/jruby_executable/src/bin/jruby_build.rs @@ -2,10 +2,18 @@ use bullet_stream::{style, Print}; use clap::Parser; use fs_err::PathExt; use indoc::formatdoc; +use inventory::artifact::Artifact; use jruby_executable::jruby_build_properties; -use shared::{download_tar, source_dir, tar_dir_to_file, untar_to_dir, BaseImage, TarDownloadPath}; +use shared::{ + append_filename_with, append_manifest, download_tar, sha256_from_path, source_dir, + tar_dir_to_file, untar_to_dir, ArtifactMetadata, BaseImage, CpuArch, ManifestVersion, + TarDownloadPath, +}; +use std::convert::From; use std::error::Error; +static S3_BASE_URL: &str = "https://heroku-buildpack-ruby.s3.us-east-1.amazonaws.com"; + #[derive(Parser, Debug)] struct Args { #[arg(long)] @@ -25,6 +33,8 @@ fn jruby_build(args: &Args) -> Result<(), Box> { let volume_cache_dir = source_dir().join("cache"); let volume_output_dir = source_dir().join("output"); + let inventory = source_dir().join("jruby_inventory.toml"); + fs_err::create_dir_all(&volume_cache_dir)?; let temp_dir = tempfile::tempdir()?; @@ -89,6 +99,10 @@ fn jruby_build(args: &Args) -> Result<(), Box> { log = { let mut bullet = log.bullet("Creating tgz archives"); + bullet = bullet.sub_bullet(format!( + "Inventory file {}", + style::value(inventory.to_string_lossy()) + )); let tar_dir = volume_output_dir.join(base_image.to_string()); fs_err::create_dir_all(&tar_dir)?; @@ -99,17 +113,45 @@ fn jruby_build(args: &Args) -> Result<(), Box> { tar_dir_to_file(&jruby_dir, &tar_file)?; bullet = timer.done(); - for arch in &["amd64", "arm64"] { - let dir = volume_output_dir.join(base_image.to_string()).join(arch); + for cpu_arch in &[CpuArch::new("amd64")?, CpuArch::new("arm64")?] { + let dir = volume_output_dir + .join(base_image.to_string()) + .join(cpu_arch.to_string()); fs_err::create_dir_all(&dir)?; let path = dir.join(&tgz_name); bullet = bullet.sub_bullet(format!("Write {}", path.display())); fs_err::copy(tar_file.path(), &path)?; + + let sha = sha256_from_path(&path)?; + let sha_seven = sha.chars().take(7).collect::(); + let sha_seven_path = append_filename_with(&path, &format!("-{sha_seven}"), ".tgz")?; + + bullet = bullet.sub_bullet(format!("Write {}", sha_seven_path.display(),)); + fs_err::copy(tar_file.path(), &sha_seven_path)?; + + append_manifest( + &inventory, + Artifact { + version: ManifestVersion::new(version), + os: inventory::artifact::Os::Linux, + arch: cpu_arch.into(), + url: format!( + "{S3_BASE_URL}/{}", + sha_seven_path.strip_prefix(&volume_output_dir)?.display() + ), + checksum: format!("sha256:{sha}").parse()?, + metadata: ArtifactMetadata { + distro_version: base_image.distro_version(), + timestamp: chrono::Utc::now(), + }, + }, + )?; } bullet.done() }; + log.done(); Ok(()) diff --git a/jruby_inventory.toml b/jruby_inventory.toml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/jruby_inventory.toml @@ -0,0 +1 @@ + diff --git a/shared/Cargo.toml b/shared/Cargo.toml index 1072bf5..09cfc82 100644 --- a/shared/Cargo.toml +++ b/shared/Cargo.toml @@ -17,6 +17,12 @@ tar = { workspace = true} tempfile = { workspace = true} thiserror = { workspace = true} bullet_stream = { workspace = true } +sha2 = { workspace = true } +chrono = { workspace = true } +serde = { workspace = true } +toml = { workspace = true } +inventory = { workspace = true } +fs2 = { workspace = true } [dev-dependencies] tiny_http = "0.12.0" diff --git a/shared/src/base_image.rs b/shared/src/base_image.rs index 4e95870..f3c0563 100644 --- a/shared/src/base_image.rs +++ b/shared/src/base_image.rs @@ -1,6 +1,8 @@ use std::fmt::Display; use std::str::FromStr; +use serde::{Deserialize, Serialize}; + static KNOWN_ARCHITECTURES: [&str; 2] = ["amd64", "arm64"]; static KNOWN_BASE_IMAGES: &[(&str, &str)] = &[ ("heroku-20", "20"), @@ -19,6 +21,9 @@ pub struct BaseImage { distro_number: String, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct DistroVersion(String); + impl BaseImage { pub fn new(s: &str) -> Result { KNOWN_BASE_IMAGES @@ -31,6 +36,10 @@ impl BaseImage { .ok_or_else(|| BaseImageError(s.to_owned())) } + pub fn distro_version(&self) -> DistroVersion { + DistroVersion(format!("{}.04", self.distro_number)) + } + pub fn is_arch_aware(&self) -> bool { MULTI_ARCH_BASE_IMAGES.contains(&self.name.as_str()) } @@ -83,6 +92,18 @@ impl FromStr for CpuArch { #[error("Invalid CPU architecture {0} must be one of {}", KNOWN_ARCHITECTURES.join(", "))] pub struct CpuArchError(String); +impl From<&CpuArch> for inventory::artifact::Arch { + fn from(value: &CpuArch) -> Self { + if &value.name == "amd64" { + inventory::artifact::Arch::Amd64 + } else if value.name == "arm64" { + inventory::artifact::Arch::Arm64 + } else { + unreachable!(); + } + } +} + impl CpuArch { pub fn new(s: &str) -> Result { KNOWN_ARCHITECTURES diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 5391370..efa5961 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,7 +1,14 @@ +use base_image::DistroVersion; use bullet_stream::state::SubBullet; use bullet_stream::Print; +use chrono::{DateTime, Utc}; +use fs2::FileExt; use fs_err::{File, PathExt}; use fun_run::CommandWithName; +use inventory::artifact::Artifact; +use inventory::inventory::Inventory; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; use std::process::Command; @@ -12,6 +19,120 @@ mod download_ruby_version; pub use base_image::{BaseImage, CpuArch, CpuArchError}; pub use download_ruby_version::RubyDownloadVersion; +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ArtifactMetadata { + pub timestamp: DateTime, + pub distro_version: DistroVersion, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ManifestVersion(pub String); + +impl ManifestVersion { + pub fn new(version: &str) -> Self { + Self(version.to_string()) + } +} + +/// Appends the given artifact to the inventory file at the given path +/// +/// If the file doesn't exist, it will be created. +/// Uses file locking to ensure atomic updating. +pub fn append_manifest( + path: &Path, + artifact: Artifact, +) -> Result<(), Error> { + fs_err::create_dir_all( + path.parent().ok_or_else(|| { + Error::Other(format!("Cannot determine parent from {}", path.display())) + })?, + ) + .map_err(Error::FsError)?; + + let mut file: std::fs::File = fs_err::OpenOptions::new() + .append(true) + .create(true) + .open(path) + .map_err(Error::FsError)? + .into(); + + file.lock_exclusive().map_err(|e| { + Error::Other(format!( + "Error {e} obtaining file lock on {}", + path.display() + )) + })?; + + let inventory_string = fs_err::read_to_string(path).map_err(Error::FsError)?; + + let mut inventory = if inventory_string.trim().is_empty() { + Inventory { + artifacts: Vec::new(), + } + } else { + inventory_string + .parse::>() + .map_err(|e| Error::Other(format!("Error {e} parsing inventory: {}", path.display())))? + }; + + inventory.push(artifact); + writeln!(file, "{inventory}").expect("Writeable file"); + + file.unlock().map_err(|e| { + Error::Other(format!( + "Error {e} releasing file lock on {}", + path.display() + )) + })?; + + Ok(()) +} + +/// Appends the given string after the filename and before the `ends_with` +/// +/// ``` +/// use std::path::Path; +/// use shared::append_filename_with; +/// +/// let path = Path::new("/tmp/file.txt"); +/// let out = append_filename_with(path, "-lol", ".txt").unwrap(); +/// assert_eq!(Path::new("/tmp/file-lol.txt"), out); +/// ``` +/// +/// Raises an error if the files doesn't exist or if the file name doesn't end with `ends_with` +pub fn append_filename_with(path: &Path, append: &str, ends_with: &str) -> Result { + let parent = path + .parent() + .ok_or_else(|| Error::Other(format!("Cannot determine parent from {}", path.display())))?; + let file_name = path + .file_name() + .ok_or_else(|| { + Error::Other(format!( + "Cannot determine file name from {}", + path.display() + )) + })? + .to_string_lossy(); + + if !file_name.ends_with(ends_with) { + Err(Error::Other(format!( + "File name {} does not end with {}", + file_name, ends_with + )))?; + } + let file_base = file_name.trim_end_matches(ends_with); + + Ok(parent.join(format!("{file_base}{append}{ends_with}"))) +} + +/// Returns the sha256 hash of the file at the given path +pub fn sha256_from_path(path: &Path) -> Result { + let mut hasher = Sha256::new(); + hasher.update(fs_err::read(path).map_err(Error::FsError)?); + + Ok(format!("{:x}", hasher.finalize())) +} + #[derive(Debug, Clone)] pub struct TarDownloadPath(pub PathBuf); @@ -67,6 +188,9 @@ pub enum Error { #[source] source: std::io::Error, }, + + #[error("Error {0}")] + Other(String), } pub fn source_dir() -> PathBuf {