Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Buildvana.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<Folder Name="/src/">
<Project Path="src/Buildvana.Core.Abstractions/Buildvana.Core.Abstractions.csproj" />
<Project Path="src/Buildvana.Core.Configuration/Buildvana.Core.Configuration.csproj" />
<Project Path="src/Buildvana.Core.ConsoleOutput/Buildvana.Core.ConsoleOutput.csproj" />
<Project Path="src/Buildvana.Core.HomeDirectory/Buildvana.Core.HomeDirectory.csproj" />
<Project Path="src/Buildvana.Core.Json/Buildvana.Core.Json.csproj" />
<Project Path="src/Buildvana.Core.Process/Buildvana.Core.Process.csproj" />
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `diagnostic` (or `diag`)

Cake verbosity values (e.g., `verbose`) are no longer accepted.
- `bv` no longer prefixes its console output with a log level and a class-name category (e.g. `info: Buildvana.Tool.Services.DotNetService: ...`). Messages now render as clean, color-coded lines: errors in red and warnings in yellow, each line tagged with a short level label (`error:`/`warning:`/`info:`/`detail:`/`trace:`). In addition, `dotnet`/MSBuild output is now streamed through live (standard output to `bv`'s standard output, standard error to its standard error) instead of being hidden unless the build fails; on failure, the first and last lines of the captured output are still included in the error message. Verbosity behavior is unchanged (`--verbosity quiet|minimal|normal|detailed|diagnostic`), as is the handling of `--color`/`--no-color` (with the [`NO_COLOR` environment variable](https://no-color.org) now honored as well).
- **BREAKING CHANGE**: `bv restore`, `bv build`, `bv test`, and `bv pack` forward extra command-line arguments to the underlying `dotnet` invocation(s) only after a `--` separator: everything after the first `--` is passed through verbatim, in the order given, and `bv` no longer parses or validates it. A non-global, option-looking token _before_ `--` is now an error that points you at the separator. Malformed or unknown forwarded arguments produce an error from `dotnet` (or, for `bv test`, from the Microsoft.Testing.Platform test application) rather than from `bv`. Previously only `-p:`/`/p:` MSBuild properties were forwarded. `bv` also always forwards `--nologo` and its resolved `--verbosity` (default `normal`) to those invocations.
- `bv build -- -m:8 -v:minimal` forwards `-m:8 -v:minimal` to `dotnet build`.
- `bv test -- --report-trx` reaches the test application.
Expand Down
1 change: 0 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.6" />
<PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
<PackageVersion Include="NuGet.Versioning" Version="7.3.1" />
<PackageVersion Include="Octokit" Version="14.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

<ItemGroup>
<PackageReference Include="JetBrains.Annotations.Sources" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>

</Project>
25 changes: 25 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/IActivityScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// Represents an open activity: a "begin → detail → outcome" grouping of related work started by
/// <see cref="IReporter.BeginActivity(string)"/>.
/// </summary>
/// <remarks>
/// Use with a <c>using</c> statement and call <see cref="Complete"/> as the last statement of the block. The
/// outcome line is reported only when <see cref="Complete"/> ran before disposal; if the scope is disposed
/// without completing (for example because the work threw), nothing is reported — much like a transaction that
/// is rolled back when not committed.
/// </remarks>
public interface IActivityScope : IDisposable
{
/// <summary>
/// Marks the activity as successfully completed, so that disposing the scope reports its outcome.
/// </summary>
/// <param name="outcomeMessage">An optional message describing the outcome of the activity.</param>
void Complete(string? outcomeMessage = null);
}
51 changes: 51 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/IReporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// Reports human-facing console output: leveled messages, activity grouping, and verbatim child-process
/// passthrough. This is the primitive surface; formatting and per-level convenience helpers are provided as
/// extension methods (see <see cref="ReporterExtensions"/>).
/// </summary>
public interface IReporter
{
/// <summary>
/// Gets the verbosity that gates which <see cref="MessageLevel"/>s are rendered.
/// </summary>
Verbosity Verbosity { get; }

/// <summary>
/// Reports a single message at the given <paramref name="level"/>. The message is rendered only when
/// <paramref name="level"/> is enabled by the current <see cref="Verbosity"/>.
/// </summary>
/// <param name="level">The severity of the message.</param>
/// <param name="message">The message text.</param>
void Report(MessageLevel level, string message);

/// <summary>
/// Begins an activity: a header is rendered now, and the returned scope renders the outcome (and elapsed
/// time) when disposed.
/// </summary>
/// <param name="title">A short description of the work the activity covers.</param>
/// <returns>A scope that closes the activity when disposed.</returns>
IActivityScope BeginActivity(string title);

/// <summary>
/// Writes a line from a child process's standard output verbatim: no level label, no color, no category.
/// Used to stream a spawned process's standard output through to this process's standard output.
/// </summary>
/// <param name="line">The line of child-process standard output to write.</param>
/// <param name="minimumVerbosity">The line is written only when the reporter's <see cref="Verbosity"/> is at
/// least this value. If <see langword="null"/>, the line is always written.</param>
void ChildOutput(string line, Verbosity? minimumVerbosity);

/// <summary>
/// Writes a line from a child process's standard error verbatim: no level label, no color, no category.
/// Used to stream a spawned process's standard error through to this process's standard error.
/// </summary>
/// <param name="line">The line of child-process standard error to write.</param>
/// <param name="minimumVerbosity">The line is written only when the reporter's <see cref="Verbosity"/> is at
/// least this value. If <see langword="null"/>, the line is always written.</param>
void ChildError(string line, Verbosity? minimumVerbosity);
}
32 changes: 32 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/MessageLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// The severity of a message reported through an <see cref="IReporter"/>. The level both classifies the
/// message and, together with the reporter's <see cref="Verbosity"/>, decides whether it is shown.
/// </summary>
/// <remarks>
/// Members are ordered from highest to lowest severity, mapping one-to-one onto the <see cref="Verbosity"/>
/// thresholds: <see cref="Error"/>↔<see cref="Verbosity.Quiet"/>, <see cref="Warning"/>↔<see cref="Verbosity.Minimal"/>,
/// <see cref="Info"/>↔<see cref="Verbosity.Normal"/>, <see cref="Detail"/>↔<see cref="Verbosity.Detailed"/>,
/// <see cref="Trace"/>↔<see cref="Verbosity.Diagnostic"/>.
/// </remarks>
public enum MessageLevel
{
/// <summary>An error: something went wrong. Shown at every verbosity.</summary>
Error,

/// <summary>A warning: something looks off but is not fatal.</summary>
Warning,

/// <summary>An informational milestone. Shown at <see cref="Verbosity.Normal"/> and above.</summary>
Info,

/// <summary>A detail useful when following along closely. Shown at <see cref="Verbosity.Detailed"/> and above.</summary>
Detail,

/// <summary>Fine-grained diagnostic chatter. Shown only at <see cref="Verbosity.Diagnostic"/>.</summary>
Trace,
}
30 changes: 30 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/NullActivityScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// An <see cref="IActivityScope"/> that does nothing. Returned by <see cref="NullReporter"/> and usable
/// anywhere a non-rendering activity scope is needed.
/// </summary>
public sealed class NullActivityScope : IActivityScope
{
private NullActivityScope()
{
}

/// <summary>
/// Gets the singleton <see cref="NullActivityScope"/> instance.
/// </summary>
public static NullActivityScope Instance { get; } = new();

/// <inheritdoc/>
public void Complete(string? outcomeMessage = null)
{
}

/// <inheritdoc/>
public void Dispose()
{
}
}
41 changes: 41 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/NullReporter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// An <see cref="IReporter"/> that discards everything. Useful as a default argument or in tests that do not
/// assert on output.
/// </summary>
public sealed class NullReporter : IReporter
{
private NullReporter()
{
}

/// <summary>
/// Gets the singleton <see cref="NullReporter"/> instance.
/// </summary>
public static NullReporter Instance { get; } = new();

/// <inheritdoc/>
public Verbosity Verbosity => Verbosity.Quiet;

/// <inheritdoc/>
public void Report(MessageLevel level, string message)
{
}

/// <inheritdoc/>
public IActivityScope BeginActivity(string title) => NullActivityScope.Instance;

/// <inheritdoc/>
public void ChildOutput(string line, Verbosity? minimumVerbosity)
{
}

/// <inheritdoc/>
public void ChildError(string line, Verbosity? minimumVerbosity)
{
}
}
111 changes: 111 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/ReporterExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Globalization;
using System.Text;

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// Provides extension methods for <see cref="IReporter"/> instances: per-level message shortcuts and
/// <see cref="CompositeFormat"/>-based formatting overloads.
/// </summary>
/// <remarks>
/// All formatting uses <see cref="CultureInfo.InvariantCulture"/>. Cache the <see cref="CompositeFormat"/>
/// instances passed to the formatting overloads in <c>static readonly</c> fields at the call site.
/// </remarks>
#pragma warning disable CA1034 // Nested types should not be visible — false positive on C# 14 extension blocks; fixed in .NET 11, backport to .NET 10 requested in https://github.com/dotnet/sdk/issues/53984
#pragma warning disable CA1708 // Identifiers should differ by more than case — false positive on classes with C# 14 extension blocks; fixed in .NET 11, https://github.com/dotnet/sdk/issues/51716
public static class ReporterExtensions
{
extension(IReporter @this)
{
/// <summary>Reports an <see cref="MessageLevel.Error"/> message.</summary>
/// <param name="message">The message text.</param>
public void Error(string message) => @this.Report(MessageLevel.Error, message);

/// <summary>Reports a <see cref="MessageLevel.Warning"/> message.</summary>
/// <param name="message">The message text.</param>
public void Warning(string message) => @this.Report(MessageLevel.Warning, message);

/// <summary>Reports an <see cref="MessageLevel.Info"/> message.</summary>
/// <param name="message">The message text.</param>
public void Info(string message) => @this.Report(MessageLevel.Info, message);

/// <summary>Reports a <see cref="MessageLevel.Detail"/> message.</summary>
/// <param name="message">The message text.</param>
public void Detail(string message) => @this.Report(MessageLevel.Detail, message);

/// <summary>Reports a <see cref="MessageLevel.Trace"/> message.</summary>
/// <param name="message">The message text.</param>
public void Trace(string message) => @this.Report(MessageLevel.Trace, message);

/// <summary>Formats and reports an <see cref="MessageLevel.Error"/> message.</summary>
/// <param name="format">The composite format string.</param>
/// <param name="args">The arguments to format.</param>
public void Error(CompositeFormat format, params ReadOnlySpan<object?> args)
=> @this.Report(MessageLevel.Error, format, args);

/// <summary>Formats and reports a <see cref="MessageLevel.Warning"/> message.</summary>
/// <param name="format">The composite format string.</param>
/// <param name="args">The arguments to format.</param>
public void Warning(CompositeFormat format, params ReadOnlySpan<object?> args)
=> @this.Report(MessageLevel.Warning, format, args);

/// <summary>Formats and reports an <see cref="MessageLevel.Info"/> message.</summary>
/// <param name="format">The composite format string.</param>
/// <param name="args">The arguments to format.</param>
public void Info(CompositeFormat format, params ReadOnlySpan<object?> args)
=> @this.Report(MessageLevel.Info, format, args);

/// <summary>Formats and reports a <see cref="MessageLevel.Detail"/> message.</summary>
/// <param name="format">The composite format string.</param>
/// <param name="args">The arguments to format.</param>
public void Detail(CompositeFormat format, params ReadOnlySpan<object?> args)
=> @this.Report(MessageLevel.Detail, format, args);

/// <summary>Formats and reports a <see cref="MessageLevel.Trace"/> message.</summary>
/// <param name="format">The composite format string.</param>
/// <param name="args">The arguments to format.</param>
public void Trace(CompositeFormat format, params ReadOnlySpan<object?> args)
=> @this.Report(MessageLevel.Trace, format, args);

/// <summary>
/// Formats and reports a message at the given <paramref name="level"/>. Formatting is skipped entirely
/// when <paramref name="level"/> is not enabled by the reporter's <see cref="IReporter.Verbosity"/>.
/// </summary>
/// <param name="level">The severity of the message.</param>
/// <param name="format">The composite format string.</param>
/// <param name="args">The arguments to format.</param>
public void Report(MessageLevel level, CompositeFormat format, params ReadOnlySpan<object?> args)
{
ArgumentNullException.ThrowIfNull(format);
if (!@this.IsEnabled(level))
{
return;
}

@this.Report(level, string.Format(CultureInfo.InvariantCulture, format, args));
}

/// <summary>
/// Determines whether a message at the given <paramref name="level"/> would be rendered at the
/// reporter's current <see cref="IReporter.Verbosity"/>.
/// </summary>
/// <param name="level">The level to test.</param>
/// <returns><see langword="true"/> if the level is enabled; otherwise, <see langword="false"/>.</returns>
public bool IsEnabled(MessageLevel level) => (int)level <= (int)@this.Verbosity;

/// <summary>
/// Determines whether the reporter's <see cref="IReporter.Verbosity"/> is at least the given
/// <paramref name="minimumVerbosity"/>.
/// </summary>
/// <param name="minimumVerbosity">The minimum verbosity to test against.</param>
/// <returns>
/// <see langword="true"/> if the reporter's verbosity is at least <paramref name="minimumVerbosity"/>;
/// otherwise, <see langword="false"/>.
/// </returns>
public bool IsVerbosityAtLeast(Verbosity minimumVerbosity) => (int)minimumVerbosity <= (int)@this.Verbosity;
}
}
31 changes: 31 additions & 0 deletions src/Buildvana.Core.Abstractions/ConsoleOutput/Verbosity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (C) Tenacom and Contributors. Licensed under the MIT license.
// See the LICENSE file in the project root for full license information.

namespace Buildvana.Core.ConsoleOutput;

/// <summary>
/// Controls how much of a reporter's output reaches the user. Each level enables all the
/// <see cref="MessageLevel"/>s enabled by the levels below it (see <see cref="MessageLevel"/> for the mapping).
/// </summary>
/// <remarks>
/// The members mirror <c>bv</c>'s <c>--verbosity</c> command-line vocabulary and are ordered from least to most
/// verbose, so a message at a given <see cref="MessageLevel"/> is shown when
/// <c>(int)level &lt;= (int)verbosity</c>.
/// </remarks>
public enum Verbosity
{
/// <summary>Only errors are shown.</summary>
Quiet,

/// <summary>Errors and warnings are shown.</summary>
Minimal,

/// <summary>Errors, warnings, and informational messages are shown. This is the default.</summary>
Normal,

/// <summary>Everything <see cref="Normal"/> shows, plus detail messages.</summary>
Detailed,

/// <summary>Everything is shown, including trace messages.</summary>
Diagnostic,
}
3 changes: 3 additions & 0 deletions src/Buildvana.Core.Abstractions/Process/IProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public interface IProcessRunner
/// <param name="throwOnNonZero">If <see langword="true"/> (the default), a <see cref="BuildFailedException"/> is thrown when the process exits with a non-zero exit code; if <see langword="false"/>, the result is returned regardless of exit code.</param>
/// <param name="onStdout">An optional callback invoked once per line of standard output as it is produced.
/// The full output text is captured into the returned <see cref="ProcessResult"/> regardless.</param>
/// <param name="onStderr">An optional callback invoked once per line of standard error as it is produced.
/// The full error text is captured into the returned <see cref="ProcessResult"/> regardless.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to cancel the process.</param>
/// <returns>A <see cref="ProcessResult"/> describing the process's outcome.</returns>
Task<ProcessResult> RunAsync(
Expand All @@ -30,5 +32,6 @@ Task<ProcessResult> RunAsync(
string? workingDirectory = null,
bool throwOnNonZero = true,
Action<string>? onStdout = null,
Action<string>? onStderr = null,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Title>Buildvana console output</Title>
<Description>System.Console-backed IReporter: leveled human-facing console output with activity grouping and child-process passthrough.</Description>
<TargetFramework>$(StandardTfm)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Buildvana.Core.Abstractions\Buildvana.Core.Abstractions.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.Diagnostics" />
</ItemGroup>

</Project>
Loading