diff --git a/buildpacks/dotnet/src/dotnet_buildpack_configuration.rs b/buildpacks/dotnet/src/dotnet_buildpack_configuration.rs new file mode 100644 index 0000000..ab5a64f --- /dev/null +++ b/buildpacks/dotnet/src/dotnet_buildpack_configuration.rs @@ -0,0 +1,89 @@ +use crate::dotnet_publish_command::VerbosityLevel; + +pub(crate) struct DotnetBuildpackConfiguration { + pub(crate) msbuild_verbosity_level: VerbosityLevel, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum DotnetBuildpackConfigurationError { + InvalidMsbuildVerbosityLevel(String), +} + +impl TryFrom<&libcnb::Env> for DotnetBuildpackConfiguration { + type Error = DotnetBuildpackConfigurationError; + + fn try_from(env: &libcnb::Env) -> Result { + Ok(Self { + msbuild_verbosity_level: detect_msbuild_verbosity_level(env)?, + }) + } +} + +fn detect_msbuild_verbosity_level( + env: &libcnb::Env, +) -> Result { + env.get("MSBUILD_VERBOSITY_LEVEL") + .map(|value| value.to_string_lossy()) + .map_or(Ok(VerbosityLevel::Minimal), |value| { + match value.to_lowercase().as_str() { + "q" | "quiet" => Ok(VerbosityLevel::Quiet), + "m" | "minimal" => Ok(VerbosityLevel::Minimal), + "n" | "normal" => Ok(VerbosityLevel::Normal), + "d" | "detailed" => Ok(VerbosityLevel::Detailed), + "diag" | "diagnostic" => Ok(VerbosityLevel::Diagnostic), + _ => Err( + DotnetBuildpackConfigurationError::InvalidMsbuildVerbosityLevel( + value.to_string(), + ), + ), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use libcnb::Env; + + fn create_env(variables: &[(&str, &str)]) -> Env { + let mut env = Env::new(); + for &(key, value) in variables { + env.insert(key, value); + } + env + } + + #[test] + fn test_detect_msbuild_verbosity_level() { + let cases = [ + (Some("quiet"), Ok(VerbosityLevel::Quiet)), + (Some("q"), Ok(VerbosityLevel::Quiet)), + (Some("minimal"), Ok(VerbosityLevel::Minimal)), + (Some("m"), Ok(VerbosityLevel::Minimal)), + (Some("normal"), Ok(VerbosityLevel::Normal)), + (Some("n"), Ok(VerbosityLevel::Normal)), + (Some("detailed"), Ok(VerbosityLevel::Detailed)), + (Some("d"), Ok(VerbosityLevel::Detailed)), + (Some("diagnostic"), Ok(VerbosityLevel::Diagnostic)), + (Some("diag"), Ok(VerbosityLevel::Diagnostic)), + ( + Some("invalid"), + Err( + DotnetBuildpackConfigurationError::InvalidMsbuildVerbosityLevel( + "invalid".to_string(), + ), + ), + ), + (None, Ok(VerbosityLevel::Minimal)), + ]; + + for (input, expected) in &cases { + let env = match input { + Some(value) => create_env(&[("MSBUILD_VERBOSITY_LEVEL", value)]), + None => Env::new(), + }; + let result = detect_msbuild_verbosity_level(&env); + assert_eq!(result, *expected); + } + } +} diff --git a/buildpacks/dotnet/src/dotnet_publish_command.rs b/buildpacks/dotnet/src/dotnet_publish_command.rs index 7da80d6..3a05310 100644 --- a/buildpacks/dotnet/src/dotnet_publish_command.rs +++ b/buildpacks/dotnet/src/dotnet_publish_command.rs @@ -27,8 +27,7 @@ impl From for Command { } } -#[derive(Clone, Copy)] -#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq)] pub(crate) enum VerbosityLevel { Quiet, Minimal, diff --git a/buildpacks/dotnet/src/errors.rs b/buildpacks/dotnet/src/errors.rs index 95b2cff..097cdca 100644 --- a/buildpacks/dotnet/src/errors.rs +++ b/buildpacks/dotnet/src/errors.rs @@ -1,5 +1,6 @@ use crate::dotnet::target_framework_moniker::ParseTargetFrameworkError; use crate::dotnet::{project, solution}; +use crate::dotnet_buildpack_configuration::DotnetBuildpackConfigurationError; use crate::launch_process::LaunchProcessDetectionError; use crate::layers::sdk::SdkLayerError; use crate::utils::StreamedCommandError; @@ -157,6 +158,27 @@ fn on_buildpack_error(error: &DotnetBuildpackError) { io_error, ), }, + DotnetBuildpackError::ParseDotnetBuildpackConfigurationError(error) => match error { + DotnetBuildpackConfigurationError::InvalidMsbuildVerbosityLevel(verbosity_level) => { + log_error( + "Invalid MSBuild verbosity level", + formatdoc! {" + The 'MSBUILD_VERBOSITY_LEVEL' environment variable value ('{verbosity_level}') could not be parsed. Did you mean one of the following? + + d + detailed + diag + diagnostic + m + minimal + n + normal + q + quiet + "}, + ); + } + }, DotnetBuildpackError::PublishCommand(error) => match error { StreamedCommandError::Io(io_error) => log_io_error( "Unable to publish .NET file", diff --git a/buildpacks/dotnet/src/main.rs b/buildpacks/dotnet/src/main.rs index ee297a9..e00bd6d 100644 --- a/buildpacks/dotnet/src/main.rs +++ b/buildpacks/dotnet/src/main.rs @@ -1,5 +1,6 @@ mod detect; mod dotnet; +mod dotnet_buildpack_configuration; mod dotnet_layer_env; mod dotnet_publish_command; mod errors; @@ -12,7 +13,10 @@ use crate::dotnet::project::Project; use crate::dotnet::runtime_identifier; use crate::dotnet::solution::Solution; use crate::dotnet::target_framework_moniker::{ParseTargetFrameworkError, TargetFrameworkMoniker}; -use crate::dotnet_publish_command::{DotnetPublishCommand, VerbosityLevel}; +use crate::dotnet_buildpack_configuration::{ + DotnetBuildpackConfiguration, DotnetBuildpackConfigurationError, +}; +use crate::dotnet_publish_command::DotnetPublishCommand; use crate::launch_process::LaunchProcessDetectionError; use crate::layers::sdk::SdkLayerError; use crate::utils::StreamedCommandError; @@ -109,12 +113,15 @@ impl Buildpack for DotnetBuildpack { ) .map_err(DotnetBuildpackError::LaunchProcessDetection); + let buildpack_configuration = DotnetBuildpackConfiguration::try_from(&Env::from_current()) + .map_err(DotnetBuildpackError::ParseDotnetBuildpackConfigurationError)?; + utils::run_command_and_stream_output( Command::from(DotnetPublishCommand { path: solution.path, configuration: build_configuration, runtime_identifier, - verbosity_level: VerbosityLevel::Normal, + verbosity_level: buildpack_configuration.msbuild_verbosity_level, }) .current_dir(&context.app_dir) .envs(&command_env.apply(Scope::Build, &Env::from_current())), @@ -244,6 +251,7 @@ enum DotnetBuildpackError { ParseVersionRequirement(semver::Error), ResolveSdkVersion(VersionReq), SdkLayer(SdkLayerError), + ParseDotnetBuildpackConfigurationError(DotnetBuildpackConfigurationError), PublishCommand(StreamedCommandError), CopyRuntimeFilesToRuntimeLayer(io::Error), LaunchProcessDetection(LaunchProcessDetectionError), diff --git a/buildpacks/dotnet/tests/dotnet_publish_test.rs b/buildpacks/dotnet/tests/dotnet_publish_test.rs index c5df749..b8fdf47 100644 --- a/buildpacks/dotnet/tests/dotnet_publish_test.rs +++ b/buildpacks/dotnet/tests/dotnet_publish_test.rs @@ -6,11 +6,10 @@ use libcnb_test::{assert_contains, assert_empty, TestRunner}; #[ignore = "integration test"] fn test_dotnet_publish_with_rid() { TestRunner::default().build( - default_build_config("tests/fixtures/basic_web_8.0_with_global_json"), + default_build_config("tests/fixtures/basic_web_8.0_with_global_json") + .env("MSBUILD_VERBOSITY_LEVEL", "normal"), |context| { assert_empty!(context.pack_stderr); - // TODO: Find a more elegant approach to testing MSBuild logs (and/or just reduce MSbuild verbosity level) - #[cfg(target_arch = "x86_64")] let arch = "x64"; #[cfg(target_arch = "aarch64")] diff --git a/buildpacks/dotnet/tests/nuget_layer_test.rs b/buildpacks/dotnet/tests/nuget_layer_test.rs index 526e347..c194101 100644 --- a/buildpacks/dotnet/tests/nuget_layer_test.rs +++ b/buildpacks/dotnet/tests/nuget_layer_test.rs @@ -6,7 +6,8 @@ use libcnb_test::{assert_contains, assert_empty, TestRunner}; #[ignore = "integration test"] fn test_nuget_restore_and_cache() { TestRunner::default().build( - default_build_config("tests/fixtures/console_with_nuget_package"), + default_build_config("tests/fixtures/console_with_nuget_package") + .env("MSBUILD_VERBOSITY_LEVEL", "normal"), |context| { assert_empty!(context.pack_stderr); assert_contains!(