diff --git a/Cargo.lock b/Cargo.lock index a17585b3..03965dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -88,8 +88,7 @@ dependencies = [ name = "buildpack-heroku-dotnet" version = "0.0.0" dependencies = [ - "bullet_stream", - "fun_run", + "buildpacks-jvm-shared", "hex", "indoc", "libcnb", @@ -101,14 +100,19 @@ dependencies = [ "serde", "serde_json", "sha2", + "shell-words", "tempfile", ] [[package]] -name = "bullet_stream" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e038a9e6b7e36319ab42141434b0c7a93f7714aa90abf63cc5086e106027bb7" +name = "buildpacks-jvm-shared" +version = "0.0.0" +source = "git+https://github.com/heroku/buildpacks-jvm?branch=malax%2Foutput#0668ca33ee60e400ac1d811be788c6e4fe23dfad" +dependencies = [ + "indoc", + "java-properties", + "libherokubuildpack", +] [[package]] name = "bumpalo" @@ -252,6 +256,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -335,16 +348,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "fun_run" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1060b826a0883f31399a72e983a55ad0b38830429a5d1dff3719aac9782415" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -483,6 +486,17 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "java-properties" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bf6f484471c451f2b51eabd9e66b3fa7274550c5ec4b6c3d6070840945117f" +dependencies = [ + "encoding_rs", + "lazy_static", + "regex", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -606,16 +620,18 @@ dependencies = [ [[package]] name = "libherokubuildpack" -version = "0.26.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8dd4cecc7b0cf175eb115cf112e7e6b6ff900be9308b83ba3d5f3c14e59bb8" +checksum = "45070d23cda8614758579eddae211a6267e86bcef8ab75a1cd239eac45a6778d" dependencies = [ + "crossbeam-utils", "flate2", "hex", "semver", "serde", "sha2", "tar", + "termcolor", "thiserror 2.0.6", "toml", "ureq", @@ -914,6 +930,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -966,6 +988,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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.66" diff --git a/buildpacks/dotnet/Cargo.toml b/buildpacks/dotnet/Cargo.toml index b417c3e0..72b229dc 100644 --- a/buildpacks/dotnet/Cargo.toml +++ b/buildpacks/dotnet/Cargo.toml @@ -10,18 +10,18 @@ autotests = false workspace = true [dependencies] -bullet_stream = "0.3.0" -fun_run = "0.2.0" hex = "0.4" indoc = "2" libcnb = "0.26" libherokubuildpack = { version = "0.26", default-features = false, features = ["tar", "download", "inventory", "inventory-semver", "inventory-sha2"] } regex = "1" +buildpacks-jvm-shared = { git = "https://github.com/heroku/buildpacks-jvm", branch = "malax/output" } roxmltree = "0.20" semver = "1.0" serde = "1" serde_json = "1" sha2 = "0.10" +shell-words = "1.1.0" [dev-dependencies] libcnb-test = "0.26" diff --git a/buildpacks/dotnet/src/errors.rs b/buildpacks/dotnet/src/errors.rs index 7bab5443..7339508f 100644 --- a/buildpacks/dotnet/src/errors.rs +++ b/buildpacks/dotnet/src/errors.rs @@ -3,9 +3,9 @@ use crate::dotnet::{project, solution}; use crate::dotnet_buildpack_configuration::DotnetBuildpackConfigurationError; use crate::layers::sdk::SdkLayerError; use crate::DotnetBuildpackError; -use bullet_stream::{style, Print}; +use buildpacks_jvm_shared::output; use indoc::formatdoc; -use std::io::{self, stderr}; +use std::io::{self}; pub(crate) fn on_error(error: libcnb::Error) { match error { @@ -20,8 +20,7 @@ pub(crate) fn on_error(error: libcnb::Error) { Use the debug information above to troubleshoot and retry your build. If you think you found a bug in the buildpack, reproduce the issue locally with a minimal example and file an issue here: - https://github.com/heroku/buildpacks-dotnet/issues/new - "}, + https://github.com/heroku/buildpacks-dotnet/issues/new"}, Some(libcnb_error.to_string()), ), } @@ -46,9 +45,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { To resolve this issue, * Delete the solution file to build a root project file instead. - * Or reference the projects to build from the solution file. - - ", solution_path.to_string_lossy()}, + * Or reference the projects to build from the solution file.", solution_path.to_string_lossy()}, None, ); } @@ -63,8 +60,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { If you’re porting an application from .NET Framework to .NET, or compiling both side-by-side, see Microsoft’s documentation for project organization guidance: - https://learn.microsoft.com/en-us/dotnet/core/porting/project-structure - ", project_file_paths.iter() + https://learn.microsoft.com/en-us/dotnet/core/porting/project-structure", project_file_paths.iter() .map(|f| f.to_string_lossy().to_string()) .collect::>() .join("`, `"), @@ -94,8 +90,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { buildpack currently supports the following TFMs: `net5.0`, `net6.0`, `net7.0`, `net8.0`. For more information, see: - https://github.com/heroku/buildpacks-dotnet#net-version - "}, + https://github.com/heroku/buildpacks-dotnet#net-version"}, None, ); } @@ -110,8 +105,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { formatdoc! {" We can’t parse the root directory `global.json` file because it contains invalid JSON. - Use the debug information above to troubleshoot and retry your build. - "}, + Use the debug information above to troubleshoot and retry your build."}, Some(error.to_string()), ), // TODO: Consider adding more specific errors for the parsed values (e.g. an invalid rollForward value) @@ -122,8 +116,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { Use the debug information above to troubleshoot and retry your build. For more information, see: - https://github.com/heroku/buildpacks-dotnet#net-version - "}, + https://github.com/heroku/buildpacks-dotnet#net-version"}, Some(error.to_string()), ), DotnetBuildpackError::ParseInventory(error) => log_error( @@ -133,9 +126,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { is almost always a buildpack bug. If you see this error, please file an issue here: - https://github.com/heroku/buildpacks-dotnet/issues/new - - "}, + https://github.com/heroku/buildpacks-dotnet/issues/new"}, Some(error.to_string()), ), DotnetBuildpackError::ParseSolutionVersionRequirement(error) => log_error( @@ -146,9 +137,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { Use the debug information above to troubleshoot and retry your build. If you think you found a bug in the buildpack, reproduce the issue locally with a minimal example and file an issue here: - https://github.com/heroku/buildpacks-dotnet/issues/new - - "}, + https://github.com/heroku/buildpacks-dotnet/issues/new"}, Some(error.to_string()), ), DotnetBuildpackError::ResolveSdkVersion(version_req) => log_error( @@ -158,8 +147,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { requirement ({version_req}). For a complete inventory of supported .NET SDK versions and platforms, see: - https://github.com/heroku/buildpacks-dotnet/blob/main/buildpacks/dotnet/inventory.toml - "}, + https://github.com/heroku/buildpacks-dotnet/blob/main/buildpacks/dotnet/inventory.toml"}, None, ), DotnetBuildpackError::SdkLayer(error) => match error { @@ -169,8 +157,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { An unexpected error occurred while downloading the .NET SDK. This error can occur due to an unstable network connection. - Use the debug information above to troubleshoot and retry your build. - "}, + Use the debug information above to troubleshoot and retry your build."}, Some(error.to_string()), ), SdkLayerError::ReadArchive(io_error) => { @@ -191,8 +178,7 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { https://github.com/heroku/buildpacks-dotnet/issues/new Expected: {expected} - Actual: {actual} - ", expected = hex::encode(expected), actual = hex::encode(actual) }, + Actual: {actual}", expected = hex::encode(expected), actual = hex::encode(actual) }, None, ), SdkLayerError::OpenArchive(io_error) => { @@ -225,36 +211,31 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { n normal q - quiet - "}, + quiet"}, None, ); } }, - DotnetBuildpackError::PublishCommand(error) => match error { - fun_run::CmdError::SystemError(_message, io_error) => log_io_error( - "Unable to publish", - "running the command to publish the .NET solution/project", - io_error, - ), - fun_run::CmdError::NonZeroExitNotStreamed(output) - | fun_run::CmdError::NonZeroExitAlreadyStreamed(output) => log_error( - "Unable to publish", - formatdoc! {" - The `dotnet publish` command exited unsuccessfully ({exit_status}). + DotnetBuildpackError::PublishCommandIoError(io_error) => log_io_error( + "Unable to publish", + "running the command to publish the .NET solution/project", + io_error, + ), + DotnetBuildpackError::PublishCommandNonZeroExitCode(output) => log_error( + "Unable to publish", + formatdoc! {" + The `dotnet publish` command exited unsuccessfully ({exit_status}). - This error usually happens due to compilation errors. Use the command output - above to troubleshoot and retry your build. + This error usually happens due to compilation errors. Use the command output + above to troubleshoot and retry your build. - The publish process can also fail for a number of other reasons, such as - intermittent network issues, unavailability of the NuGet package feed and/or - other external dependencies, etc. + The publish process can also fail for a number of other reasons, such as + intermittent network issues, unavailability of the NuGet package feed and/or + other external dependencies, etc. - Try again to see if the error resolves itself. - ", exit_status = output.status()}, - None, - ), - }, + Try again to see if the error resolves itself.", exit_status = output.status}, + None, + ), DotnetBuildpackError::CopyRuntimeFiles(io_error) => log_io_error( "Error copying .NET runtime files", "copying .NET runtime files from the SDK layer to the runtime layer", @@ -285,8 +266,7 @@ fn on_load_dotnet_project_error(error: &project::LoadError, occurred_while: &str You must set this required property. For more information, see: - https://github.com/heroku/buildpacks-dotnet#net-version - ", project_path = project_path.to_string_lossy()}, + https://github.com/heroku/buildpacks-dotnet#net-version", project_path = project_path.to_string_lossy()}, None, ); } @@ -301,22 +281,18 @@ fn log_io_error(header: &str, occurred_while: &str, io_error: &io::Error) { Use the debug information above to troubleshoot and retry your build. If the issue persists, file an issue here: - https://github.com/heroku/buildpacks-dotnet/issues/new - "}, + https://github.com/heroku/buildpacks-dotnet/issues/new"}, Some(io_error.to_string()), ); } fn log_error(header: impl AsRef, body: impl AsRef, error: Option) { - let mut log = Print::new(stderr()).without_header(); - if let Some(error) = error { - let bullet = log.bullet(style::important("Debug info")); - log = bullet.sub_bullet(error).done(); - } - log.error(formatdoc! {" - {header} - - {body} - ", header = header.as_ref(), body = body.as_ref(), - }); + let body = body.as_ref(); + output::print_error( + header, + error.map_or_else( + || body.to_string(), + |x| format!("Debug info: {x}\n\n{body}"), + ), + ); } diff --git a/buildpacks/dotnet/src/layers/nuget_cache.rs b/buildpacks/dotnet/src/layers/nuget_cache.rs index 7f46e62a..18a26f8a 100644 --- a/buildpacks/dotnet/src/layers/nuget_cache.rs +++ b/buildpacks/dotnet/src/layers/nuget_cache.rs @@ -1,5 +1,5 @@ use crate::{DotnetBuildpack, DotnetBuildpackError}; -use bullet_stream::{state, Print}; +use buildpacks_jvm_shared::output; use libcnb::build::BuildContext; use libcnb::data::layer_name; use libcnb::layer::{ @@ -7,7 +7,6 @@ use libcnb::layer::{ RestoredLayerAction, }; use serde::{Deserialize, Serialize}; -use std::io::Stdout; #[derive(Serialize, Deserialize)] struct NugetCacheLayerMetadata { @@ -17,18 +16,9 @@ struct NugetCacheLayerMetadata { const MAX_NUGET_CACHE_RESTORE_COUNT: f32 = 20.0; -type HandleResult = Result< - ( - LayerRef, - Print>, - ), - libcnb::Error, ->; - pub(crate) fn handle( context: &BuildContext, - mut log: Print>, -) -> HandleResult { +) -> Result, libcnb::Error> { let nuget_cache_layer = context.cached_layer( layer_name!("nuget-cache"), CachedLayerDefinition { @@ -64,7 +54,8 @@ pub(crate) fn handle( } }, } { - log = log.bullet("NuGet cache").sub_bullet(message).done(); + output::print_section("NuGet cache"); + output::print_subsection(message); } - Ok((nuget_cache_layer, log)) + Ok(nuget_cache_layer) } diff --git a/buildpacks/dotnet/src/layers/sdk.rs b/buildpacks/dotnet/src/layers/sdk.rs index d92b5872..9150dd3a 100644 --- a/buildpacks/dotnet/src/layers/sdk.rs +++ b/buildpacks/dotnet/src/layers/sdk.rs @@ -1,5 +1,5 @@ use crate::{dotnet_layer_env, DotnetBuildpack, DotnetBuildpackError}; -use bullet_stream::{state, style, Print}; +use buildpacks_jvm_shared::output::{self, BuildpackOutputTextSection}; use inventory::artifact::Artifact; use inventory::checksum::Checksum; use libcnb::data::layer_name; @@ -16,7 +16,6 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha512}; use std::env::temp_dir; use std::fs::{self, File}; -use std::io::Stdout; use std::path::Path; use std::thread; use std::time::Duration; @@ -31,21 +30,12 @@ pub(crate) enum CustomCause { DifferentSdkArtifact(Artifact>), } -type HandleResult = Result< - ( - LayerRef, - Print>, - ), - libcnb::Error, ->; - const MAX_RETRIES: u8 = 4; pub(crate) fn handle( context: &libcnb::build::BuildContext, - log: Print>, artifact: &Artifact>, -) -> HandleResult { +) -> Result, libcnb::Error> { let sdk_layer = context.cached_layer( layer_name!("sdk"), CachedLayerDefinition { @@ -65,19 +55,18 @@ pub(crate) fn handle( }, )?; - let mut log_bullet = log.bullet("SDK installation"); + output::print_section("SDK installation"); match sdk_layer.state { LayerState::Restored { .. } => { - log_bullet = - log_bullet.sub_bullet(format!("Reusing cached SDK (version {})", artifact.version)); + output::print_subsection(format!("Reusing cached SDK (version {})", artifact.version)); } LayerState::Empty { ref cause } => { if let EmptyLayerCause::RestoredLayerAction { cause: CustomCause::DifferentSdkArtifact(old_artifact), } = cause { - log_bullet = log_bullet.sub_bullet(format!( + output::print_subsection(format!( "Deleting cached .NET SDK (version {})", old_artifact.version )); @@ -87,33 +76,33 @@ pub(crate) fn handle( artifact: artifact.clone(), })?; - let mut log_background_bullet = log_bullet.start_timer(format!( - "Downloading SDK from {}", - style::url(artifact.clone().url) - )); - let tarball_path = temp_dir().join("dotnetsdk.tar.gz"); let mut download_attempts = 0; - while download_attempts <= MAX_RETRIES { - match download_file(&artifact.url, &tarball_path) { - Err(DownloadError::IoError(error)) if download_attempts < MAX_RETRIES => { - log_bullet = log_background_bullet.cancel(format!("{error}")); - download_attempts += 1; - thread::sleep(Duration::from_secs(1)); - log_background_bullet = log_bullet.start_timer("Retrying"); - } - result => { - result.map_err(SdkLayerError::DownloadArchive)?; - break; + output::track_subsection_timing(|| { + while download_attempts <= MAX_RETRIES { + output::print_subsection(vec![ + BuildpackOutputTextSection::regular("Downloading SDK from "), + BuildpackOutputTextSection::Url(artifact.clone().url), + ]); + match download_file(&artifact.url, &tarball_path) { + Err(DownloadError::IoError(error)) if download_attempts < MAX_RETRIES => { + output::print_subsection(format!("Error: {error}")); + } + result => { + return result.map_err(SdkLayerError::DownloadArchive); + } } + download_attempts += 1; + thread::sleep(Duration::from_secs(1)); + output::print_subsection("Retrying..."); } - } - log_bullet = log_background_bullet.done(); + Ok(()) + })?; - log_bullet = log_bullet.sub_bullet("Verifying SDK checksum"); + output::print_subsection("Verifying SDK checksum"); verify_checksum(&artifact.checksum, &tarball_path)?; - log_bullet = log_bullet.sub_bullet("Installing SDK"); + output::print_subsection("Installing SDK"); decompress_tarball( &mut File::open(&tarball_path).map_err(SdkLayerError::OpenArchive)?, sdk_layer.path(), @@ -127,7 +116,7 @@ pub(crate) fn handle( } } - Ok((sdk_layer, log_bullet.done())) + Ok(sdk_layer) } fn verify_checksum(checksum: &Checksum, path: impl AsRef) -> Result<(), SdkLayerError> diff --git a/buildpacks/dotnet/src/main.rs b/buildpacks/dotnet/src/main.rs index 9c58c41b..c34d5606 100644 --- a/buildpacks/dotnet/src/main.rs +++ b/buildpacks/dotnet/src/main.rs @@ -19,9 +19,10 @@ use crate::dotnet_buildpack_configuration::{ use crate::dotnet_publish_command::DotnetPublishCommand; use crate::launch_process::LaunchProcessDetectionError; use crate::layers::sdk::SdkLayerError; -use bullet_stream::state::{Bullet, SubBullet}; -use bullet_stream::{style, Print}; -use fun_run::CommandWithName; +use buildpacks_jvm_shared::output::{ + print_buildpack_name, print_section, print_subsection, print_warning, run_command, + track_subsection_timing, BuildpackOutputTextSection, +}; use indoc::formatdoc; use inventory::artifact::{Arch, Os}; use inventory::{Inventory, ParseInventoryError}; @@ -34,9 +35,10 @@ use libcnb::{buildpack_main, Buildpack, Env}; use libherokubuildpack::inventory; use semver::{Version, VersionReq}; use sha2::Sha512; -use std::io::{Stdout, Write}; +use std::ffi::OsStr; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Output}; use std::{fs, io}; struct DotnetBuildpack; @@ -65,34 +67,33 @@ impl Buildpack for DotnetBuildpack { let buildpack_configuration = DotnetBuildpackConfiguration::try_from(&Env::from_current()) .map_err(DotnetBuildpackError::ParseBuildpackConfiguration)?; - let mut log = Print::new(std::io::stdout()).h2("Heroku .NET Buildpack"); - let mut log_bullet = log.bullet("SDK version detection"); + print_buildpack_name("Heroku .NET Buildpack"); + print_section("SDK version detection"); let solution = get_solution_to_publish(&context.app_dir)?; - log_bullet = log_bullet.sub_bullet(format!( - "Detected .NET file to publish: {}", - style::value(solution.path.to_string_lossy()) - )); + print_subsection(vec![ + BuildpackOutputTextSection::regular("Detected .NET file to publish: "), + BuildpackOutputTextSection::value(solution.path.to_string_lossy()), + ]); let sdk_version_requirement = if let Some(version_req) = detect_global_json_sdk_version_requirement(&context.app_dir) { - log_bullet = - log_bullet.sub_bullet("Detecting version requirement from root global.json file"); + print_subsection("Detecting version requirement from root global.json file"); version_req? } else { - log_bullet = log_bullet.sub_bullet(format!( - "Inferring version requirement from {}", - style::value(solution.path.to_string_lossy()) - )); + print_subsection(vec![ + BuildpackOutputTextSection::regular("Inferring version requirement from "), + BuildpackOutputTextSection::value(solution.path.to_string_lossy()), + ]); get_solution_sdk_version_requirement(&solution)? }; - log_bullet = log_bullet.sub_bullet(format!( - "Detected version requirement: {}", - style::value(sdk_version_requirement.to_string()) - )); + print_subsection(vec![ + BuildpackOutputTextSection::regular("Detected version requirement: "), + BuildpackOutputTextSection::value(sdk_version_requirement.to_string()), + ]); let target_os = context.target.os.parse::() .expect("OS should always be parseable, buildpack will not run on unsupported operating systems."); @@ -109,18 +110,19 @@ impl Buildpack for DotnetBuildpack { sdk_version_requirement, ))?; - log = log_bullet - .sub_bullet(format!( - "Resolved .NET SDK version {} {}", - style::value(sdk_artifact.version.to_string()), - style::details(format!("{}-{}", sdk_artifact.os, sdk_artifact.arch)) - )) - .done(); + print_subsection(vec![ + BuildpackOutputTextSection::regular("Resolved .NET SDK version "), + BuildpackOutputTextSection::value(sdk_artifact.version.to_string()), + BuildpackOutputTextSection::regular(format!( + " ({}-{})", + sdk_artifact.os, sdk_artifact.arch + )), + ]); - let (sdk_layer, log) = layers::sdk::handle(&context, log, sdk_artifact)?; - let (nuget_cache_layer, mut log) = layers::nuget_cache::handle(&context, log)?; + let sdk_layer = layers::sdk::handle(&context, sdk_artifact)?; + let nuget_cache_layer = layers::nuget_cache::handle(&context)?; - log_bullet = log.bullet("Publish solution"); + print_section("Publish solution"); let command_env = sdk_layer.read_env()?.chainable_insert( Scope::Build, libcnb::layer_env::ModificationBehavior::Override, @@ -132,10 +134,12 @@ impl Buildpack for DotnetBuildpack { .build_configuration .clone() .unwrap_or_else(|| String::from("Release")); - log_bullet = log_bullet.sub_bullet(format!( - "Using {} build configuration", - style::value(build_configuration.clone()) - )); + + print_subsection(vec![ + BuildpackOutputTextSection::regular("Using "), + BuildpackOutputTextSection::value(build_configuration.clone()), + BuildpackOutputTextSection::regular(" build configuration"), + ]); let mut publish_command = Command::from(DotnetPublishCommand { path: solution.path.clone(), @@ -147,38 +151,43 @@ impl Buildpack for DotnetBuildpack { .current_dir(&context.app_dir) .envs(&command_env.apply(Scope::Build, &Env::from_current())); - log_bullet - .stream_with( - format!("Running {}", style::command(publish_command.name())), - |stdout, stderr| publish_command.stream_output(stdout, stderr), + print_subsection(vec![ + BuildpackOutputTextSection::regular("Running "), + BuildpackOutputTextSection::Command(command_to_string(&publish_command)), + ]); + track_subsection_timing(|| { + run_command( + publish_command, + false, + DotnetBuildpackError::PublishCommandIoError, + DotnetBuildpackError::PublishCommandNonZeroExitCode, ) - .map_err(DotnetBuildpackError::PublishCommand)?; - log = log_bullet.done(); + })?; layers::runtime::handle(&context, &sdk_layer.path())?; - log_bullet = log - .bullet("Setting launch table") - .sub_bullet("Detecting process types from published artifacts"); + print_section("Setting launch table"); + print_subsection("Detecting process types from published artifacts"); let mut launch_builder = LaunchBuilder::new(); - log = match launch_process::detect_solution_processes(&solution) { + match launch_process::detect_solution_processes(&solution) { Ok(processes) => { if processes.is_empty() { - log_bullet = log_bullet.sub_bullet("No processes were detected"); + print_subsection("No processes were detected"); } for process in processes { - log_bullet = log_bullet.sub_bullet(format!( - "Added {}: {}", - style::value(process.r#type.to_string()), - process.command.join(" ") - )); + print_subsection(vec![ + BuildpackOutputTextSection::regular("Added "), + BuildpackOutputTextSection::value(process.r#type.to_string()), + BuildpackOutputTextSection::regular(format!( + ": {}", + process.command.join(" ") + )), + ]); launch_builder.process(process); } - log_bullet.done() } - Err(error) => log_launch_process_detection_warning(error, log_bullet), + Err(error) => log_launch_process_detection_warning(error), }; - log.done(); BuildResultBuilder::new() .launch(launch_builder.build()) @@ -190,6 +199,14 @@ impl Buildpack for DotnetBuildpack { } } +fn command_to_string(cmd: &Command) -> String { + shell_words::join( + std::iter::once(cmd.get_program()) + .chain(cmd.get_args()) + .map(OsStr::to_string_lossy), + ) +} + fn get_solution_to_publish(app_dir: &Path) -> Result { let solution_file_paths = detect::solution_file_paths(app_dir).expect("function to pass after detection"); @@ -250,17 +267,13 @@ fn detect_global_json_sdk_version_requirement( }) } -fn log_launch_process_detection_warning( - error: LaunchProcessDetectionError, - log: Print>, -) -> Print> { +fn log_launch_process_detection_warning(error: LaunchProcessDetectionError) { match error { - LaunchProcessDetectionError::ProcessType(process_type_error) => log - .warning(formatdoc! {" + LaunchProcessDetectionError::ProcessType(process_type_error) => print_warning( + "Launch process detection error", + formatdoc! {" {process_type_error} - Launch process detection error - We detected an invalid launch process type. The buildpack automatically tries to register Cloud Native Buildpacks (CNB) @@ -279,8 +292,8 @@ fn log_launch_process_detection_warning( If you think you found a bug in the buildpack, or have feedback on improving the behavior for your use case, file an issue here: https://github.com/heroku/buildpacks-dotnet/issues/new - "}) - .done(), + "}, + ), } } @@ -300,7 +313,8 @@ enum DotnetBuildpackError { ResolveSdkVersion(VersionReq), SdkLayer(SdkLayerError), ParseBuildpackConfiguration(DotnetBuildpackConfigurationError), - PublishCommand(fun_run::CmdError), + PublishCommandIoError(io::Error), + PublishCommandNonZeroExitCode(Output), CopyRuntimeFiles(io::Error), } diff --git a/buildpacks/dotnet/tests/dotnet_publish_test.rs b/buildpacks/dotnet/tests/dotnet_publish_test.rs index 2c34ab42..b8b9006b 100644 --- a/buildpacks/dotnet/tests/dotnet_publish_test.rs +++ b/buildpacks/dotnet/tests/dotnet_publish_test.rs @@ -40,17 +40,17 @@ fn test_dotnet_publish_with_compilation_error() { assert_contains!( &context.pack_stderr, &indoc! {r" - ! Unable to publish - ! + ! ERROR: Unable to publish + ! ! The `dotnet publish` command exited unsuccessfully (exit status: 1). - ! + ! ! This error usually happens due to compilation errors. Use the command output ! above to troubleshoot and retry your build. - ! + ! ! The publish process can also fail for a number of other reasons, such as ! intermittent network issues, unavailability of the NuGet package feed and/or ! other external dependencies, etc. - ! + ! ! Try again to see if the error resolves itself."} ); }, @@ -92,14 +92,13 @@ fn test_dotnet_publish_with_global_json_and_custom_verbosity_level() { |context| { assert_empty!(context.pack_stderr); let rid = get_rid(); - assert_contains!( replace_msbuild_log_patterns_with_placeholder(&context.pack_stdout, ""), &formatdoc! {r#" - Publish solution - Using `Release` build configuration - - Running `dotnet publish /workspace/foo.csproj --runtime {rid} "-p:PublishDir=bin/publish" --verbosity normal` - + - Running `dotnet publish /workspace/foo.csproj --runtime {rid} '-p:PublishDir=bin/publish' --verbosity normal` + MSBuild version 17.8.3+195e7f5a3 for .NET Build started . 1>Project "/workspace/foo.csproj" on node 1 (Restore target(s)). diff --git a/buildpacks/dotnet/tests/sdk_installation_test.rs b/buildpacks/dotnet/tests/sdk_installation_test.rs index 15ba05c9..7845ec90 100644 --- a/buildpacks/dotnet/tests/sdk_installation_test.rs +++ b/buildpacks/dotnet/tests/sdk_installation_test.rs @@ -78,7 +78,7 @@ fn test_sdk_installation_with_global_json() { - Detected version requirement: `=8.0.101` - Resolved .NET SDK version `8.0.101` (linux-amd64) - SDK installation - - Downloading SDK from https://download.visualstudio.microsoft.com/download/pr/9454f7dc-b98e-4a64-a96d-4eb08c7b6e66/da76f9c6bc4276332b587b771243ae34/dotnet-sdk-8.0.101-linux-x64.tar.gz" + - Downloading SDK from `https://download.visualstudio.microsoft.com/download/pr/9454f7dc-b98e-4a64-a96d-4eb08c7b6e66/da76f9c6bc4276332b587b771243ae34/dotnet-sdk-8.0.101-linux-x64.tar.gz`" } ); assert_contains!( @@ -106,14 +106,14 @@ fn test_sdk_installation_with_global_json() { assert_empty!(context.pack_stderr); assert_contains!( context.pack_stdout, - &indoc! {r" + &indoc! {" - SDK version detection - Detected .NET file to publish: `/workspace/foo.csproj` - Detecting version requirement from root global.json file - Detected version requirement: `=8.0.101` - Resolved .NET SDK version `8.0.101` (linux-arm64) - SDK installation - - Downloading SDK from https://download.visualstudio.microsoft.com/download/pr/092bec24-9cad-421d-9b43-458b3a7549aa/84280dbd1eef750f9ed1625339235c22/dotnet-sdk-8.0.101-linux-arm64.tar.gz" + - Downloading SDK from `https://download.visualstudio.microsoft.com/download/pr/092bec24-9cad-421d-9b43-458b3a7549aa/84280dbd1eef750f9ed1625339235c22/dotnet-sdk-8.0.101-linux-arm64.tar.gz`" } ); assert_contains!(