diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 64bbb0daf117..144db57948c6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -372,7 +372,8 @@ jobs: submodules: true - name: Install Rust run: rustup update stable && rustup default stable - - run: cd cranelift/codegen && cargo build --features "all-arch completely-skip-isle-for-ci-deterministic-check" + # Run without `check-isle` feature (see note in `cranelift/codegen/build.rs` for why). + - run: cd cranelift/codegen && cargo build --no-default-features --features "std unwind all-arch" - run: ci/ensure_deterministic_build.sh # Perform release builds of `wasmtime` and `libwasmtime.so`. Builds on diff --git a/cranelift/codegen/Cargo.toml b/cranelift/codegen/Cargo.toml index 77d34bbfb9d5..a6abe4ef8f1d 100644 --- a/cranelift/codegen/Cargo.toml +++ b/cranelift/codegen/Cargo.toml @@ -40,7 +40,7 @@ miette = { version = "3", features = ["fancy"], optional = true } sha2 = "0.9.8" [features] -default = ["std", "unwind"] +default = ["std", "unwind", "check-isle"] # The "std" feature enables use of libstd. The "core" feature enables use # of some minimal std-like replacement libraries. At least one of these two @@ -58,6 +58,13 @@ testing_hooks = [] # This enables unwind info generation functionality. unwind = ["gimli"] +# The "check-isle" feature checks that ISLE-generated source is +# up-to-date during build, according to a manifest of file hashes +# created the last time the source was regenerated. Use the +# "rebuild-isle" feature (which depends on this feature) to +# additionally regenerate the source. +check-isle = [] + # ISA targets for which we should build. # If no ISA targets are explicitly enabled, the ISA target for the host machine is enabled. x86 = [] @@ -91,7 +98,12 @@ regalloc-snapshot = ["bincode", "regalloc/enable-serde"] souper-harvest = ["souper-ir", "souper-ir/stringify"] # Recompile ISLE DSL source files into their generated Rust code. -rebuild-isle = ["cranelift-isle", "miette", "cranelift-codegen-meta/rebuild-isle"] +rebuild-isle = [ + "check-isle", + "cranelift-isle", + "miette", + "cranelift-codegen-meta/rebuild-isle", +] # A hack to skip the ISLE-rebuild logic when testing for determinism # with the "Meta deterministic check" CI job. diff --git a/cranelift/codegen/build.rs b/cranelift/codegen/build.rs index d900b92fa449..9d8dc370ecc9 100644 --- a/cranelift/codegen/build.rs +++ b/cranelift/codegen/build.rs @@ -16,7 +16,6 @@ use cranelift_codegen_meta as meta; -use sha2::{Digest, Sha512}; use std::env; use std::io::Read; use std::process; @@ -68,16 +67,26 @@ fn main() { println!("cargo:warning=Generated files are in {}", out_dir); } - // The "Meta deterministic check" CI job runs this build script N - // times to ensure it produces the same output + // We put all ISLE-related behavior (manifest check and automatic + // rebuild of generated code) under a feature flag `check-isle`. + // This is for two reasons. + // + // First, we want to allow downstream dependents to rely on + // Cranelift without pulling in *any* extra crates, even `sha2` + // which we use for the manifest-based up-to-date check. They can + // do this by specifying `default-features = false` and then not + // enabling `check-isle`. + // + // Second, the "Meta deterministic check" CI job runs this build + // script N times to ensure it produces the same output // consistently. However, it runs the script in a fresh directory, // without any of the source tree present; this breaks our // manifest check (we need the ISLE source to be present). To keep // things simple, we just disable all ISLE-related logic for this // specific CI job. - #[cfg(not(feature = "completely-skip-isle-for-ci-deterministic-check"))] + #[cfg(feature = "check-isle")] { - maybe_rebuild_isle(crate_dir).expect("Unhandled failure in ISLE rebuild"); + isle::maybe_rebuild_isle(crate_dir).expect("Unhandled failure in ISLE rebuild"); } let pkg_version = env::var("CARGO_PKG_VERSION").unwrap(); @@ -117,342 +126,354 @@ fn main() { .unwrap(); } -/// Strip the current directory from the file paths, because `islec` -/// includes them in the generated source, and this helps us maintain -/// deterministic builds that don't include those local file paths. -fn make_isle_source_path_relative( - cur_dir: &std::path::PathBuf, - filename: std::path::PathBuf, -) -> std::path::PathBuf { - if let Ok(suffix) = filename.strip_prefix(&cur_dir) { - suffix.to_path_buf() - } else { - filename +#[cfg(feature = "check-isle")] +mod isle { + use sha2::{Digest, Sha512}; + use std::io::Read; + + /// Strip the current directory from the file paths, because `islec` + /// includes them in the generated source, and this helps us maintain + /// deterministic builds that don't include those local file paths. + fn make_isle_source_path_relative( + cur_dir: &std::path::PathBuf, + filename: std::path::PathBuf, + ) -> std::path::PathBuf { + if let Ok(suffix) = filename.strip_prefix(&cur_dir) { + suffix.to_path_buf() + } else { + filename + } } -} - -/// A list of compilations (transformations from ISLE source to -/// generated Rust source) that exist in the repository. -/// -/// This list is used either to regenerate the Rust source in-tree (if -/// the `rebuild-isle` feature is enabled), or to verify that the ISLE -/// source in-tree corresponds to the ISLE source that was last used -/// to rebuild the Rust source (if the `rebuild-isle` feature is not -/// enabled). -#[derive(Clone, Debug)] -struct IsleCompilations { - items: Vec, -} -#[derive(Clone, Debug)] -struct IsleCompilation { - output: std::path::PathBuf, - inputs: Vec, -} + /// A list of compilations (transformations from ISLE source to + /// generated Rust source) that exist in the repository. + /// + /// This list is used either to regenerate the Rust source in-tree (if + /// the `rebuild-isle` feature is enabled), or to verify that the ISLE + /// source in-tree corresponds to the ISLE source that was last used + /// to rebuild the Rust source (if the `rebuild-isle` feature is not + /// enabled). + #[derive(Clone, Debug)] + struct IsleCompilations { + items: Vec, + } -impl IsleCompilation { - /// Compute the manifest filename for the given generated Rust file. - fn manifest_filename(&self) -> std::path::PathBuf { - self.output.with_extension("manifest") + #[derive(Clone, Debug)] + struct IsleCompilation { + output: std::path::PathBuf, + inputs: Vec, } - /// Compute the content of the source manifest for all ISLE source - /// files that go into the compilation of one Rust file. - /// - /// We store this alongside the `.rs` file as - /// `.manifest` and use it to verify that a - /// rebuild was done if necessary. - fn compute_manifest(&self) -> Result> { - use std::fmt::Write; - - let mut manifest = String::new(); - - for filename in &self.inputs { - // Our source must be valid UTF-8 for this to work, else user - // will get an error on build. This is not expected to be an - // issue. - let content = std::fs::read_to_string(filename)?; - // On Windows, source is checked out with line-endings changed - // to `\r\n`; canonicalize the source that we hash to - // Unix-style (`\n`) so hashes will match. - let content = content.replace("\r\n", "\n"); - // One line in the manifest: . - let mut hasher = Sha512::default(); - hasher.update(content.as_bytes()); - let filename = format!("{}", filename.display()).replace("\\", "/"); - writeln!(&mut manifest, "{} {:x}", filename, hasher.finalize())?; + impl IsleCompilation { + /// Compute the manifest filename for the given generated Rust file. + fn manifest_filename(&self) -> std::path::PathBuf { + self.output.with_extension("manifest") } - Ok(manifest) - } -} - -/// Construct the list of compilations (transformations from ISLE -/// source to generated Rust source) that exist in the repository. -fn get_isle_compilations(crate_dir: &std::path::Path) -> Result { - let cur_dir = std::env::current_dir()?; - - let clif_isle = - make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("clif.isle")); - let prelude_isle = - make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle")); - let src_isa_x64 = - make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64")); - let src_isa_aarch64 = - make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("aarch64")); - - // This is a set of ISLE compilation units. - // - // The format of each entry is: - // - // (output Rust code file, input ISLE source files) - // - // There should be one entry for each backend that uses ISLE for lowering, - // and if/when we replace our peephole optimization passes with ISLE, there - // should be an entry for each of those as well. - Ok(IsleCompilations { - items: vec![ - // The x86-64 instruction selector. - IsleCompilation { - output: src_isa_x64 - .join("lower") - .join("isle") - .join("generated_code.rs"), - inputs: vec![ - clif_isle.clone(), - prelude_isle.clone(), - src_isa_x64.join("inst.isle"), - src_isa_x64.join("lower.isle"), - ], - }, - // The aarch64 instruction selector. - IsleCompilation { - output: src_isa_aarch64 - .join("lower") - .join("isle") - .join("generated_code.rs"), - inputs: vec![ - clif_isle.clone(), - prelude_isle.clone(), - src_isa_aarch64.join("inst.isle"), - src_isa_aarch64.join("lower.isle"), - ], - }, - ], - }) -} + /// Compute the content of the source manifest for all ISLE source + /// files that go into the compilation of one Rust file. + /// + /// We store this alongside the `.rs` file as + /// `.manifest` and use it to verify that a + /// rebuild was done if necessary. + fn compute_manifest(&self) -> Result> { + use std::fmt::Write; + + let mut manifest = String::new(); + + for filename in &self.inputs { + // Our source must be valid UTF-8 for this to work, else user + // will get an error on build. This is not expected to be an + // issue. + let content = std::fs::read_to_string(filename)?; + // On Windows, source is checked out with line-endings changed + // to `\r\n`; canonicalize the source that we hash to + // Unix-style (`\n`) so hashes will match. + let content = content.replace("\r\n", "\n"); + // One line in the manifest: . + let mut hasher = Sha512::default(); + hasher.update(content.as_bytes()); + let filename = format!("{}", filename.display()).replace("\\", "/"); + writeln!(&mut manifest, "{} {:x}", filename, hasher.finalize())?; + } -/// Check the manifest for the ISLE generated code, which documents -/// what ISLE source went into generating the Rust, and if there is a -/// mismatch, either invoke the ISLE compiler (if we have the -/// `rebuild-isle` feature) or exit with an error (if not). -/// -/// We do this by computing a hash of the ISLE source and checking it -/// against a "manifest" that is also checked into git, alongside the -/// generated Rust. -/// -/// (Why not include the `rebuild-isle` feature by default? Because -/// the build process must not modify the checked-in source by -/// default; any checked-in source is a human-managed bit of data, and -/// we can only act as an agent of the human developer when explicitly -/// requested to do so. This manifest check is a middle ground that -/// ensures this explicit control while also avoiding the easy footgun -/// of "I changed the ISLE, why isn't the compiler updated?!".) -fn maybe_rebuild_isle( - crate_dir: &std::path::Path, -) -> Result<(), Box> { - let isle_compilations = get_isle_compilations(crate_dir)?; - let mut rebuild_compilations = vec![]; - - for compilation in &isle_compilations.items { - for file in &compilation.inputs { - println!("cargo:rerun-if-changed={}", file.display()); + Ok(manifest) } + } - let manifest = - std::fs::read_to_string(compilation.manifest_filename()).unwrap_or(String::new()); - // Canonicalize Windows line-endings into Unix line-endings in - // the manifest text itself. - let manifest = manifest.replace("\r\n", "\n"); - let expected_manifest = compilation.compute_manifest()?.replace("\r\n", "\n"); - if manifest != expected_manifest { - rebuild_compilations.push((compilation, expected_manifest)); - } + /// Construct the list of compilations (transformations from ISLE + /// source to generated Rust source) that exist in the repository. + fn get_isle_compilations( + crate_dir: &std::path::Path, + ) -> Result { + let cur_dir = std::env::current_dir()?; + + let clif_isle = + make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("clif.isle")); + let prelude_isle = + make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle")); + let src_isa_x64 = + make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64")); + let src_isa_aarch64 = make_isle_source_path_relative( + &cur_dir, + crate_dir.join("src").join("isa").join("aarch64"), + ); + + // This is a set of ISLE compilation units. + // + // The format of each entry is: + // + // (output Rust code file, input ISLE source files) + // + // There should be one entry for each backend that uses ISLE for lowering, + // and if/when we replace our peephole optimization passes with ISLE, there + // should be an entry for each of those as well. + Ok(IsleCompilations { + items: vec![ + // The x86-64 instruction selector. + IsleCompilation { + output: src_isa_x64 + .join("lower") + .join("isle") + .join("generated_code.rs"), + inputs: vec![ + clif_isle.clone(), + prelude_isle.clone(), + src_isa_x64.join("inst.isle"), + src_isa_x64.join("lower.isle"), + ], + }, + // The aarch64 instruction selector. + IsleCompilation { + output: src_isa_aarch64 + .join("lower") + .join("isle") + .join("generated_code.rs"), + inputs: vec![ + clif_isle.clone(), + prelude_isle.clone(), + src_isa_aarch64.join("inst.isle"), + src_isa_aarch64.join("lower.isle"), + ], + }, + ], + }) } - #[cfg(feature = "rebuild-isle")] - { - if !rebuild_compilations.is_empty() { - set_miette_hook(); + /// Check the manifest for the ISLE generated code, which documents + /// what ISLE source went into generating the Rust, and if there is a + /// mismatch, either invoke the ISLE compiler (if we have the + /// `rebuild-isle` feature) or exit with an error (if not). + /// + /// We do this by computing a hash of the ISLE source and checking it + /// against a "manifest" that is also checked into git, alongside the + /// generated Rust. + /// + /// (Why not include the `rebuild-isle` feature by default? Because + /// the build process must not modify the checked-in source by + /// default; any checked-in source is a human-managed bit of data, and + /// we can only act as an agent of the human developer when explicitly + /// requested to do so. This manifest check is a middle ground that + /// ensures this explicit control while also avoiding the easy footgun + /// of "I changed the ISLE, why isn't the compiler updated?!".) + pub(crate) fn maybe_rebuild_isle( + crate_dir: &std::path::Path, + ) -> Result<(), Box> { + let isle_compilations = get_isle_compilations(crate_dir)?; + let mut rebuild_compilations = vec![]; + + for compilation in &isle_compilations.items { + for file in &compilation.inputs { + println!("cargo:rerun-if-changed={}", file.display()); + } + + let manifest = + std::fs::read_to_string(compilation.manifest_filename()).unwrap_or(String::new()); + // Canonicalize Windows line-endings into Unix line-endings in + // the manifest text itself. + let manifest = manifest.replace("\r\n", "\n"); + let expected_manifest = compilation.compute_manifest()?.replace("\r\n", "\n"); + if manifest != expected_manifest { + rebuild_compilations.push((compilation, expected_manifest)); + } } - let mut had_error = false; - for (compilation, expected_manifest) in rebuild_compilations { - if let Err(e) = rebuild_isle(compilation, &expected_manifest) { - eprintln!("Error building ISLE files: {:?}", e); - let mut source = e.source(); - while let Some(e) = source { - eprintln!("{:?}", e); - source = e.source(); + + #[cfg(feature = "rebuild-isle")] + { + if !rebuild_compilations.is_empty() { + set_miette_hook(); + } + let mut had_error = false; + for (compilation, expected_manifest) in rebuild_compilations { + if let Err(e) = rebuild_isle(compilation, &expected_manifest) { + eprintln!("Error building ISLE files: {:?}", e); + let mut source = e.source(); + while let Some(e) = source { + eprintln!("{:?}", e); + source = e.source(); + } + had_error = true; } - had_error = true; } - } - if had_error { - std::process::exit(1); + if had_error { + std::process::exit(1); + } } - } - #[cfg(not(feature = "rebuild-isle"))] - { - if !rebuild_compilations.is_empty() { - for (compilation, _) in rebuild_compilations { + #[cfg(not(feature = "rebuild-isle"))] + { + if !rebuild_compilations.is_empty() { + for (compilation, _) in rebuild_compilations { + eprintln!(""); + eprintln!( + "Error: the ISLE source files that resulted in the generated Rust source" + ); + eprintln!(""); + eprintln!(" * {}", compilation.output.display()); + eprintln!(""); + eprintln!( + "have changed but the generated source was not rebuilt! These ISLE source" + ); + eprintln!("files are:"); + eprintln!(""); + for file in &compilation.inputs { + eprintln!(" * {}", file.display()); + } + } + eprintln!(""); + eprintln!("Please add `--features rebuild-isle` to your `cargo build` command"); eprintln!( - "Error: the ISLE source files that resulted in the generated Rust source" + "if you wish to rebuild the generated source, then include these changes" ); + eprintln!("in any git commits you make that include the changes to the ISLE."); eprintln!(""); - eprintln!(" * {}", compilation.output.display()); + eprintln!("For example:"); eprintln!(""); - eprintln!( - "have changed but the generated source was not rebuilt! These ISLE source" - ); - eprintln!("files are:"); + eprintln!(" $ cargo build -p cranelift-codegen --features rebuild-isle"); eprintln!(""); - for file in &compilation.inputs { - eprintln!(" * {}", file.display()); - } + eprintln!("(This build script cannot do this for you by default because we cannot"); + eprintln!("modify checked-into-git source without your explicit opt-in.)"); + eprintln!(""); + std::process::exit(1); } - - eprintln!(""); - eprintln!("Please add `--features rebuild-isle` to your `cargo build` command"); - eprintln!("if you wish to rebuild the generated source, then include these changes"); - eprintln!("in any git commits you make that include the changes to the ISLE."); - eprintln!(""); - eprintln!("For example:"); - eprintln!(""); - eprintln!(" $ cargo build -p cranelift-codegen --features rebuild-isle"); - eprintln!(""); - eprintln!("(This build script cannot do this for you by default because we cannot"); - eprintln!("modify checked-into-git source without your explicit opt-in.)"); - eprintln!(""); - std::process::exit(1); } - } - Ok(()) -} + Ok(()) + } -#[cfg(feature = "rebuild-isle")] -fn set_miette_hook() { - use std::sync::Once; - static SET_MIETTE_HOOK: Once = Once::new(); - SET_MIETTE_HOOK.call_once(|| { - let _ = miette::set_hook(Box::new(|_| { - Box::new( - miette::MietteHandlerOpts::new() - // This is necessary for `miette` to properly display errors - // until https://github.com/zkat/miette/issues/93 is fixed. - .force_graphical(true) - .build(), - ) - })); - }); -} + #[cfg(feature = "rebuild-isle")] + fn set_miette_hook() { + use std::sync::Once; + static SET_MIETTE_HOOK: Once = Once::new(); + SET_MIETTE_HOOK.call_once(|| { + let _ = miette::set_hook(Box::new(|_| { + Box::new( + miette::MietteHandlerOpts::new() + // This is necessary for `miette` to properly display errors + // until https://github.com/zkat/miette/issues/93 is fixed. + .force_graphical(true) + .build(), + ) + })); + }); + } -/// Rebuild ISLE DSL source text into generated Rust code. -/// -/// NB: This must happen *after* the `cranelift-codegen-meta` functions, since -/// it consumes files generated by them. -#[cfg(feature = "rebuild-isle")] -fn rebuild_isle( - compilation: &IsleCompilation, - manifest: &str, -) -> Result<(), Box> { - use cranelift_isle as isle; - - // First, remove the manifest, if any; we will recreate it - // below if the compilation is successful. Ignore error if no - // manifest was present. - let manifest_filename = compilation.manifest_filename(); - let _ = std::fs::remove_file(&manifest_filename); - - let code = (|| { - let lexer = isle::lexer::Lexer::from_files(&compilation.inputs[..])?; - let defs = isle::parser::parse(lexer)?; - isle::compile::compile(&defs) - })() - .map_err(|e| { - // Make sure to include the source snippets location info along with - // the error messages. - - let report = miette::Report::new(e); - return DebugReport(report); - - struct DebugReport(miette::Report); - - impl std::fmt::Display for DebugReport { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - self.0.handler().debug(&*self.0, f) + /// Rebuild ISLE DSL source text into generated Rust code. + /// + /// NB: This must happen *after* the `cranelift-codegen-meta` functions, since + /// it consumes files generated by them. + #[cfg(feature = "rebuild-isle")] + fn rebuild_isle( + compilation: &IsleCompilation, + manifest: &str, + ) -> Result<(), Box> { + use cranelift_isle as isle; + + // First, remove the manifest, if any; we will recreate it + // below if the compilation is successful. Ignore error if no + // manifest was present. + let manifest_filename = compilation.manifest_filename(); + let _ = std::fs::remove_file(&manifest_filename); + + let code = (|| { + let lexer = isle::lexer::Lexer::from_files(&compilation.inputs[..])?; + let defs = isle::parser::parse(lexer)?; + isle::compile::compile(&defs) + })() + .map_err(|e| { + // Make sure to include the source snippets location info along with + // the error messages. + + let report = miette::Report::new(e); + return DebugReport(report); + + struct DebugReport(miette::Report); + + impl std::fmt::Display for DebugReport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.0.handler().debug(&*self.0, f) + } } - } - impl std::fmt::Debug for DebugReport { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) + impl std::fmt::Debug for DebugReport { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } } - } - impl std::error::Error for DebugReport {} - })?; + impl std::error::Error for DebugReport {} + })?; + + let code = rustfmt(&code).unwrap_or_else(|e| { + println!( + "cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}", + e + ); + code + }); - let code = rustfmt(&code).unwrap_or_else(|e| { println!( - "cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}", - e + "Writing ISLE-generated Rust code to {}", + compilation.output.display() ); - code - }); - - println!( - "Writing ISLE-generated Rust code to {}", - compilation.output.display() - ); - std::fs::write(&compilation.output, code)?; - - // Write the manifest so that, in the default build configuration - // without the `rebuild-isle` feature, we can at least verify that - // no changes were made that will not be picked up. Note that we - // only write this *after* we write the source above, so no - // manifest is produced if there was an error. - std::fs::write(&manifest_filename, manifest)?; - - return Ok(()); - - fn rustfmt(code: &str) -> std::io::Result { - use std::io::Write; - - let mut rustfmt = std::process::Command::new("rustfmt") - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .spawn()?; - - let mut stdin = rustfmt.stdin.take().unwrap(); - stdin.write_all(code.as_bytes())?; - drop(stdin); - - let mut stdout = rustfmt.stdout.take().unwrap(); - let mut data = vec![]; - stdout.read_to_end(&mut data)?; - - let status = rustfmt.wait()?; - if !status.success() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("`rustfmt` exited with status {}", status), - )); - } + std::fs::write(&compilation.output, code)?; + + // Write the manifest so that, in the default build configuration + // without the `rebuild-isle` feature, we can at least verify that + // no changes were made that will not be picked up. Note that we + // only write this *after* we write the source above, so no + // manifest is produced if there was an error. + std::fs::write(&manifest_filename, manifest)?; + + return Ok(()); + + fn rustfmt(code: &str) -> std::io::Result { + use std::io::Write; + + let mut rustfmt = std::process::Command::new("rustfmt") + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn()?; + + let mut stdin = rustfmt.stdin.take().unwrap(); + stdin.write_all(code.as_bytes())?; + drop(stdin); + + let mut stdout = rustfmt.stdout.take().unwrap(); + let mut data = vec![]; + stdout.read_to_end(&mut data)?; + + let status = rustfmt.wait()?; + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("`rustfmt` exited with status {}", status), + )); + } - Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout")) + Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout")) + } } }