From 8d67b0302edbe4dd818d3603d0430ca33b2f648e Mon Sep 17 00:00:00 2001 From: Mark Dittmer Date: Wed, 13 May 2026 19:51:50 +0000 Subject: [PATCH] [WIP] Experiment rewriting setup in toolchain-config directory External-to-crate configuration strategy is trait-with-consts + build script helper for generating trait impls gherrit-pr-id: Gsl2hdook2fozrononrkhrxyog4hookj7 --- anneal/Cargo.lock | 25 + anneal/Cargo.toml | 2 +- anneal/v2/Cargo.toml | 10 + anneal/v2/toolchain-config/Cargo.toml | 15 + anneal/v2/toolchain-config/src/build.rs | 118 +++++ anneal/v2/toolchain-config/src/lib.rs | 455 ++++++++++++++++++ .../toolchain-config/testdata/archive.tar.zst | Bin 0 -> 148 bytes .../v2/toolchain-config/tests/build_tests.rs | 4 + 8 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 anneal/v2/Cargo.toml create mode 100644 anneal/v2/toolchain-config/Cargo.toml create mode 100644 anneal/v2/toolchain-config/src/build.rs create mode 100644 anneal/v2/toolchain-config/src/lib.rs create mode 100644 anneal/v2/toolchain-config/testdata/archive.tar.zst create mode 100644 anneal/v2/toolchain-config/tests/build_tests.rs diff --git a/anneal/Cargo.lock b/anneal/Cargo.lock index e36a991600..440edb9e16 100644 --- a/anneal/Cargo.lock +++ b/anneal/Cargo.lock @@ -2509,6 +2509,21 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toolchain-config" +version = "0.1.0" +dependencies = [ + "anyhow", + "env_logger", + "log", + "reqwest", + "sha2", + "tar", + "tempfile", + "toml", + "zstd", +] + [[package]] name = "tower" version = "0.5.3" @@ -2735,6 +2750,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "v2" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "setup", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/anneal/Cargo.toml b/anneal/Cargo.toml index 065c7eb10d..21d7fed037 100644 --- a/anneal/Cargo.toml +++ b/anneal/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "tools/doc_gen", "v2/setup"] +members = [".", "tools/doc_gen", "v2", "v2/setup", "v2/toolchain-config"] [package] name = "cargo-anneal" diff --git a/anneal/v2/Cargo.toml b/anneal/v2/Cargo.toml new file mode 100644 index 0000000000..4db3c088e2 --- /dev/null +++ b/anneal/v2/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "v2" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.102" +clap = "4.6.0" +env_logger = "0.11.10" +setup = { path = "./setup" } diff --git a/anneal/v2/toolchain-config/Cargo.toml b/anneal/v2/toolchain-config/Cargo.toml new file mode 100644 index 0000000000..4acbf586d7 --- /dev/null +++ b/anneal/v2/toolchain-config/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "toolchain-config" +version = "0.1.0" +edition = "2024" + +[dependencies] +log = "0.4" +reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] } +sha2 = "0.10" +tempfile = "3.27.0" +tar = "0.4" +zstd = "0.13" +toml = "0.8.23" +env_logger = "0.11.10" +anyhow = "1.0.102" diff --git a/anneal/v2/toolchain-config/src/build.rs b/anneal/v2/toolchain-config/src/build.rs new file mode 100644 index 0000000000..6b7e6df4d5 --- /dev/null +++ b/anneal/v2/toolchain-config/src/build.rs @@ -0,0 +1,118 @@ +use anyhow::Context as _; + +pub fn write_toolchain_definition( + toolchain_config_toml: impl std::io::Read, + toolchain_config_rs: impl std::io::Write, +) -> Result<(), anyhow::Error> { + write_toolchain_definition_custom( + std::env::consts::OS, + std::env::consts::ARCH, + toolchain_config_toml, + toolchain_config_rs, + ) +} + +pub fn write_toolchain_definition_custom( + os: &str, + arch: &str, + mut toolchain_config_toml_read: impl std::io::Read, + mut toolchain_config_rs: impl std::io::Write, +) -> Result<(), anyhow::Error> { + let mut toolchain_config_toml_string = String::new(); + toolchain_config_toml_read + .read_to_string(&mut toolchain_config_toml_string) + .context("failed to read toolchain config toml")?; + let toolchain_config_toml: toml::Value = toml::from_str(&toolchain_config_toml_string) + .context("failed to parse toml string from toolchain config")?; + + let this_toolchain_config_toml = toolchain_config_toml + .get("toolchain") + .context("toolchain config toml missing 'toolchain' key")? + .get(os) + .with_context(|| format!("toolchain config toml missing 'toolchain.{os}' key"))? + .get(arch) + .with_context(|| format!("toolchain config toml missing 'toolchain.{os}.{arch}' key"))?; + + let Some(toml::Value::String(archive_sha256_string)) = + this_toolchain_config_toml.get("archive-sha256") + else { + anyhow::bail!("toolchain config toml missing 'toolchain.{os}.{arch}.archive-sha256' key"); + }; + let archive_sha256 = decode_hex_256(archive_sha256_string) + .with_context(|| format!("toolchain config toml missing 'toolchain.{os}.{arch}.archive-sha256' value not a 256-bit hex value; found: {:?}", archive_sha256_string))?; + let Some(toml::Value::String(archive_url)) = this_toolchain_config_toml.get("archive-url") + else { + anyhow::bail!("toolchain config toml missing 'toolchain.{os}.{arch}.archive-url' key"); + }; + + let output = format!( + r#" + use toolchain_config::Config as ToolchainConfigTrait; + + struct ToolchainConfig; + + impl ToolchainConfigTrait for ToolchainConfig {{ + const OS: &str = {:?}; + const ARCH: &str = {:?}; + const ARCHIVE_SHA256: [u8; 32] = {:?}; + const ARCHIVE_URL: &'static str = {:?}; + }} + "#, + os, arch, archive_sha256, archive_url, + ); + Ok(toolchain_config_rs + .write_all(output.as_bytes()) + .context("failed to write to toolchain config rust source file")?) +} + +const fn decode_hex_256(s: &str) -> Option<[u8; 32]> { + let bytes = s.as_bytes(); + if bytes.len() != 64 { + return None; + } + let mut res = [0u8; 32]; + let mut i = 0; + while i < 32 { + let (h, l) = (bytes[i * 2], bytes[i * 2 + 1]); + let h_nib = match decode_nibble(h) { + Some(n) => n, + None => return None, + }; + let l_nib = match decode_nibble(l) { + Some(n) => n, + None => return None, + }; + res[i] = (h_nib << 4) | l_nib; + i += 1; + } + Some(res) +} + +const fn decode_nibble(c: u8) -> Option { + match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + b'A'..=b'F' => Some(c - b'A' + 10), + _ => None, + } +} + +mod tests { + use super::*; + + #[test] + fn test_toml_succeed_self_platform() { + let toolchain_config_toml_string = format!( + r#" + [toolchain.{}.{}] + archive-sha256 = "0000000000000000000000000000000000000000000000000000000000000000" + archive-url = "http://example.com/archive.tar.zst" + "#, + std::env::consts::OS, + std::env::consts::ARCH + ); + + write_toolchain_definition(toolchain_config_toml_string.as_bytes(), &mut std::io::sink()) + .expect("failed to write toolchain definition from valid toolchain config toml"); + } +} diff --git a/anneal/v2/toolchain-config/src/lib.rs b/anneal/v2/toolchain-config/src/lib.rs new file mode 100644 index 0000000000..3c21778ef6 --- /dev/null +++ b/anneal/v2/toolchain-config/src/lib.rs @@ -0,0 +1,455 @@ +pub mod build; + +/// An archive format that can be extracted using a particular [`Extractor`] implementation. +pub enum ArchiveFormat { + TarZst, +} + +impl ArchiveFormat { + fn new_extractor(&self) -> impl Extractor { + use ArchiveFormat::*; + match self { + TarZst => TarZstLibraryExtractor, + } + } +} + +/// The source for an archive of dependencies that `setup` must put in place. +pub enum Source { + /// A fully assembled local directory tree containing uncompressed toolchain components. + LocalDirectory(std::path::PathBuf), + /// A local archive awaiting extraction. + LocalArchive(std::path::PathBuf, ArchiveFormat), + /// The archive format to expect from the statically configured remote URL source. + Remote(ArchiveFormat), +} + +/// An abstract extraction factory instantiating operational output streams targeting designated +/// filesystem paths. +/// +/// Consuming software can utilize this trait interface to decouple core processing workflows +/// from concrete decompressor tools, facilitating modular injection of specialized archiving logic +/// or isolated testing frameworks. +pub trait Extractor { + /// Unpacks stream bytes directly into the specified target directory synchronously on the calling thread. + fn extract(&self, src: &mut dyn std::io::Read, dst: &std::path::Path) -> std::io::Result<()>; +} + +/// A concrete [`Extractor`] implementation delegating archive extraction duties entirely in-process +/// via synchronous streaming pipelines using the `tar` and `zstd` library crates. +pub struct TarZstLibraryExtractor; + +impl Extractor for TarZstLibraryExtractor { + fn extract(&self, src: &mut dyn std::io::Read, dst: &std::path::Path) -> std::io::Result<()> { + let decoder = zstd::Decoder::new(src)?; + let mut archive = tar::Archive::new(decoder); + archive.unpack(dst) + } +} + +fn encode_hex_256(bytes: &[u8; 32]) -> String { + use std::fmt::Write; + let mut s = String::with_capacity(64); + for &b in bytes { + write!(&mut s, "{:02x}", b).unwrap(); + } + s +} + +struct HashReader { + inner: R, + hasher: sha2::Sha256, +} + +impl HashReader { + fn new(inner: R) -> Self { + use sha2::Digest; + Self { inner, hasher: sha2::Sha256::new() } + } + + fn finalize(self) -> [u8; 32] { + use sha2::Digest; + self.hasher.finalize().into() + } +} + +impl std::io::Read for HashReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + use sha2::Digest; + let n = self.inner.read(buf)?; + self.hasher.update(&buf[..n]); + Ok(n) + } +} + +pub trait Config { + const OS: &'static str; + const ARCH: &'static str; + const ARCHIVE_SHA256: [u8; 32]; + const ARCHIVE_URL: &'static str; +} + +/// Coordinates the provisioning and verification of the active toolchain dependency environment. +/// +/// This function processes the incoming dependency source and installs it into a toolchain +/// directory named according to the source SHA256 hash. +/// +/// # Caveats +/// +/// - When `src` is a `Source::Local*` variant, the toolchain will be installed in a directory +/// named after the _expected_ SHA256 hash associated with the detected platform. This will not +/// necessarily match the behaviour of the platform's archive and should only be used for local +/// development of the managed toolchain itself. +pub fn setup(src: Source, toolchain_dir: std::path::PathBuf) -> Result<(), String> { + setup_inner::(src, toolchain_dir, |url| { + let response = + reqwest::blocking::get(url).map_err(|e| format!("Failed to download archive: {e}"))?; + let response = response + .error_for_status() + .map_err(|e| format!("HTTP error downloading archive: {e}"))?; + Ok(Box::new(response)) + }) +} + +fn setup_from_archive( + src: impl std::io::Read, + dst: &std::path::Path, + extractor: &dyn Extractor, +) -> Result<[u8; 32], std::io::Error> { + let parent = dst.parent().expect("toolchains directory has parent"); + let temp_dir = tempfile::Builder::new().prefix("setup-").tempdir_in(parent)?; + + let mut hash_reader = HashReader::new(src); + extractor.extract(&mut hash_reader, temp_dir.path())?; + + // Handle atomic overwrite if dst already occupies the target path + let old_dir = if dst.symlink_metadata().is_ok() { + let old = tempfile::Builder::new().prefix("setup-old-").tempdir_in(parent)?; + let target_old = old.path().join("old"); + std::fs::rename(dst, &target_old)?; + Some(old) + } else { + None + }; + + // Retain (renamed) extraction directory. + let temp_dir_path = temp_dir.keep(); + std::fs::rename(&temp_dir_path, dst)?; + + // Drop/delete (renamed) old directory (if it exists). + drop(old_dir); + + Ok(hash_reader.finalize()) +} + +#[cfg(not(unix))] +fn copy_dir_all(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> { + std::fs::create_dir_all(dst)?; + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let ft = entry.file_type()?; + let target = dst.join(entry.file_name()); + if ft.is_dir() { + copy_dir_all(&entry.path(), &target)?; + } else { + std::fs::copy(&entry.path(), &target)?; + } + } + Ok(()) +} + +fn link_or_copy_dir(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> { + #[cfg(unix)] + { + std::os::unix::fs::symlink(src, dst) + } + #[cfg(not(unix))] + { + #[cfg(windows)] + { + if std::os::windows::fs::symlink_dir(src, dst).is_ok() { + return Ok(()); + } + log::warn!( + "Native directory symlink creation failed. Falling back to recursive directory copy." + ); + } + #[cfg(not(windows))] + { + log::warn!( + "Symbolic linking not natively configured for this platform. Falling back to recursive directory copy." + ); + } + + copy_dir_all(src, dst) + } +} + +fn setup_from_directory(src: &std::path::Path, dst: &std::path::Path) -> Result<(), String> { + let parent = dst.parent().expect("toolchains directory has parent"); + let old_dir = if dst.symlink_metadata().is_ok() { + let old = tempfile::Builder::new() + .prefix("setup-old-") + .tempdir_in(parent) + .map_err(|e| format!("Failed to create temp dir for old target: {e}"))?; + let target_old = old.path().join("old"); + std::fs::rename(dst, &target_old) + .map_err(|e| format!("Failed to rename existing target: {e}"))?; + Some(old) + } else { + None + }; + + link_or_copy_dir(src, dst) + .map_err(|e| format!("Failed to link or copy local directory: {e}"))?; + + drop(old_dir); + Ok(()) +} + +fn setup_inner( + src: Source, + toolchain_dir: std::path::PathBuf, + fetcher: impl FnOnce(&str) -> Result, String>, +) -> Result<(), String> { + let expected_hex = encode_hex_256(&C::ARCHIVE_SHA256); + let target_dir = toolchain_dir.join(setup_dir_name::()); + + use Source::*; + match src { + LocalArchive(path, format) => { + log::warn!( + "Toolchain contents from local archive may not match expected toolchain hash/version number." + ); + let extractor = format.new_extractor(); + let file = std::fs::File::open(path) + .map_err(|e| format!("Failed to open local archive: {e}"))?; + setup_from_archive(file, &target_dir, &extractor) + .map_err(|e| format!("Failed to extract archive: {e}"))?; + } + LocalDirectory(path) => { + log::warn!( + "Toolchain contents from local directory may not match expected toolchain hash/version number." + ); + setup_from_directory(&path, &target_dir)?; + } + Remote(format) => { + let extractor = format.new_extractor(); + let response = fetcher(C::ARCHIVE_URL)?; + + let actual_hash: [u8; 32] = setup_from_archive(response, &target_dir, &extractor) + .map_err(|e| format!("Failed to extract downloaded archive: {e}"))?; + + if actual_hash != C::ARCHIVE_SHA256 { + let _ = std::fs::remove_dir_all(&target_dir); + return Err(format!( + "Checksum mismatch for downloaded archive. Expected {}, got {}", + expected_hex, + encode_hex_256(&actual_hash) + )); + } + } + } + + Ok(()) +} + +fn setup_dir_name() -> String { + let archive_sha256_hex = encode_hex_256(&C::ARCHIVE_SHA256); + format!("{}-{}-{}", C::OS, C::ARCH, &archive_sha256_hex[..12]) +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_ARCHIVE: &[u8] = include_bytes!("../testdata/archive.tar.zst"); + + fn create_test_archive(src_dir: &std::path::Path, archive_path: &std::path::Path) { + let file = std::fs::File::create(archive_path).unwrap(); + let encoder = zstd::Encoder::new(file, 0).unwrap(); + let mut builder = tar::Builder::new(encoder); + builder.append_dir_all(".", src_dir).unwrap(); + let encoder = builder.into_inner().unwrap(); + encoder.finish().unwrap(); + } + + fn compute_sha256(path: &std::path::Path) -> [u8; 32] { + use sha2::Digest; + let mut file = std::fs::File::open(path).unwrap(); + let mut hasher = sha2::Sha256::new(); + std::io::copy(&mut file, &mut hasher).unwrap(); + hasher.finalize().into() + } + + #[test] + fn test_setup_from_directory() { + let temp = tempfile::tempdir().unwrap(); + let src = temp.path().join("src"); + std::fs::create_dir(&src).unwrap(); + std::fs::write(src.join("file.txt"), "hello").unwrap(); + + let dst = temp.path().join("dst"); + setup_from_directory(&src, &dst).unwrap(); + + assert!(dst.join("file.txt").exists()); + assert_eq!(std::fs::read_to_string(dst.join("file.txt")).unwrap(), "hello"); + + // Test atomic replacement by running it again with updated contents + let src2 = temp.path().join("src2"); + std::fs::create_dir(&src2).unwrap(); + std::fs::write(src2.join("file.txt"), "world").unwrap(); + + setup_from_directory(&src2, &dst).unwrap(); + assert_eq!(std::fs::read_to_string(dst.join("file.txt")).unwrap(), "world"); + } + + #[test] + fn test_setup_from_archive() { + let temp = tempfile::tempdir().unwrap(); + let src = temp.path().join("src"); + std::fs::create_dir(&src).unwrap(); + std::fs::write(src.join("data.txt"), "archive_content").unwrap(); + + let archive_path = temp.path().join("test.tar.zst"); + create_test_archive(&src, &archive_path); + + let dst = temp.path().join("dst"); + let file = std::fs::File::open(&archive_path).unwrap(); + let hash = setup_from_archive(file, &dst, &TarZstLibraryExtractor).unwrap(); + + assert_eq!(hash, compute_sha256(&archive_path)); + assert_eq!(std::fs::read_to_string(dst.join("data.txt")).unwrap(), "archive_content"); + } + + #[test] + fn test_setup_inner_local_directory() { + let temp = tempfile::tempdir().unwrap(); + let src = temp.path().join("src"); + std::fs::create_dir(&src).unwrap(); + std::fs::write(src.join("test.txt"), "local_dir").unwrap(); + + struct TestConfig; + + impl Config for TestConfig { + const OS: &'static str = std::env::consts::OS; + const ARCH: &'static str = std::env::consts::ARCH; + const ARCHIVE_SHA256: [u8; 32] = [1u8; 32]; + const ARCHIVE_URL: &'static str = "http://example.com/archive.tar.zst"; + } + + setup_inner::( + Source::LocalDirectory(src), + temp.path().to_path_buf(), + |_| unreachable!(), + ) + .unwrap(); + + let target_dir = temp.path().join(setup_dir_name::()); + assert_eq!(std::fs::read_to_string(target_dir.join("test.txt")).unwrap(), "local_dir"); + } + + #[test] + fn test_setup_inner_local_archive() { + let temp = tempfile::tempdir().unwrap(); + let src = temp.path().join("src"); + std::fs::create_dir(&src).unwrap(); + std::fs::write(src.join("test.txt"), "local_archive").unwrap(); + + let archive_path = temp.path().join("archive.tar.zst"); + create_test_archive(&src, &archive_path); + + struct TestConfig; + + impl Config for TestConfig { + const OS: &'static str = std::env::consts::OS; + const ARCH: &'static str = std::env::consts::ARCH; + const ARCHIVE_SHA256: [u8; 32] = [2u8; 32]; + const ARCHIVE_URL: &'static str = "http://example.com/archive.tar.zst"; + } + + setup_inner::( + Source::LocalArchive(archive_path, ArchiveFormat::TarZst), + temp.path().to_path_buf(), + |_| unreachable!(), + ) + .unwrap(); + + let target_dir = temp.path().join(setup_dir_name::()); + assert_eq!(std::fs::read_to_string(target_dir.join("test.txt")).unwrap(), "local_archive"); + } + + #[test] + fn test_setup_inner_remote_success() { + let temp = tempfile::tempdir().unwrap(); + + // To (re)compute test archive hash: + // use sha2::Digest; + // let mut hasher = sha2::Sha256::new(); + // std::io::copy(&mut TEST_ARCHIVE, &mut hasher).unwrap(); + // let actual_hash = hasher.finalize().into(); + // let expected_hex = encode_hex_256(&actual_hash); + // panic!("ACTUAL HASH: {:?}\nACTUAL HASH: {:?}", actual_hash, expected_hex); + + struct TestConfig; + + impl Config for TestConfig { + const OS: &'static str = std::env::consts::OS; + const ARCH: &'static str = std::env::consts::ARCH; + // Manually observed with commented-out code above. + const ARCHIVE_SHA256: [u8; 32] = [ + 78, 126, 202, 220, 170, 27, 159, 91, 201, 226, 162, 235, 212, 250, 206, 165, 178, + 77, 19, 69, 48, 181, 169, 14, 87, 54, 8, 222, 13, 75, 192, 188, + ]; + const ARCHIVE_URL: &'static str = "http://example.com/archive.tar.zst"; + } + + setup_inner::( + Source::Remote(ArchiveFormat::TarZst), + temp.path().to_path_buf(), + move |_url| Ok(Box::new(TEST_ARCHIVE)), + ) + .expect("setup_inner failed"); + } + + #[test] + fn test_setup_inner_remote_checksum_mismatch() { + let temp = tempfile::tempdir().unwrap(); + let src = temp.path().join("src"); + std::fs::create_dir(&src).unwrap(); + std::fs::write(src.join("test.txt"), "bad_remote").unwrap(); + + let archive_path = temp.path().join("bad_remote.tar.zst"); + create_test_archive(&src, &archive_path); + + // To (re)compute mutated hash: + // use sha2::Digest; + // let mut hasher = sha2::Sha256::new(); + // std::io::copy(&mut TEST_ARCHIVE, &mut hasher).unwrap(); + // let actual_hash: [u8; 32] = hasher.finalize().into(); + // let mut expected_hash = actual_hash.clone(); + // expected_hash[0] ^= 1; + // let expected_hex = encode_hex_256(&expected_hash); + // panic!("MUTATED HASH: {:?}\nMUTATED HASH: {:?}", expected_hash, expected_hex); + + struct TestConfig; + + impl Config for TestConfig { + const OS: &'static str = std::env::consts::OS; + const ARCH: &'static str = std::env::consts::ARCH; + // Manually observed with commented-out code above. + const ARCHIVE_SHA256: [u8; 32] = [ + 79, 126, 202, 220, 170, 27, 159, 91, 201, 226, 162, 235, 212, 250, 206, 165, 178, + 77, 19, 69, 48, 181, 169, 14, 87, 54, 8, 222, 13, 75, 192, 188, + ]; + const ARCHIVE_URL: &'static str = "http://example.com/archive.tar.zst"; + } + + setup_inner::( + Source::Remote(ArchiveFormat::TarZst), + temp.path().to_path_buf(), + move |_url| Ok(Box::new(TEST_ARCHIVE)), + ) + .expect_err("setup_inner succeeded, but should have failed"); + } +} diff --git a/anneal/v2/toolchain-config/testdata/archive.tar.zst b/anneal/v2/toolchain-config/testdata/archive.tar.zst new file mode 100644 index 0000000000000000000000000000000000000000..ebfa28daddd06d418122dfd622082db919034e0d GIT binary patch literal 148 zcmV;F0Bip!wJ-eySUm&)Qb!jVfX6uqWU{1ogLh;#5i?Mmv`{0G;1)D@q^qn|76kC) z;~nbKv4$mbGcnUF8aQY+ri^v9U6u?qo6pf)LSsBuDy6%R_jGrJX9J-^bRQz)(tT9_ z?_w300MRUn4gi}Y#{&S91ISqb1F8WX1|c3Wd4Sl0BcMJ&vjC(94%{g;n}8SnTiN}B C?mTG# literal 0 HcmV?d00001 diff --git a/anneal/v2/toolchain-config/tests/build_tests.rs b/anneal/v2/toolchain-config/tests/build_tests.rs new file mode 100644 index 0000000000..063a95ed65 --- /dev/null +++ b/anneal/v2/toolchain-config/tests/build_tests.rs @@ -0,0 +1,4 @@ +#[test] +fn test_hello() { + // panic!("Hello!"); +}