Skip to content
Draft
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
66 changes: 66 additions & 0 deletions src/Fallout.Build/Execution/BuildContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Fallout.Common.Tooling;
using Fallout.Common.Utilities.Collections;
using Fallout.Common.ValueInjection;

namespace Fallout.Common.Execution;

/// <summary>
/// Per-run, process-ambient build state. Activated once at the top of <see cref="BuildManager.Execute{T}"/>
/// and disposed at the end of the run, so the process-global statics a build touches are owned by a
/// scope rather than leaking across invocations (the cleanup FT-1 centralised now lives here).
/// The static surface (e.g. <see cref="BuildManager.CancellationHandler"/>) reads through
/// <see cref="Current"/>; <c>AsyncLocal</c> keeps concurrent/nested runs isolated.
/// </summary>
/// <remarks>
/// FT-2 / <see href="https://github.com/Fallout-build/Fallout/issues/307">#307</see>. Intentionally
/// <c>internal</c> — not a public contract until the SDK lands (milestone #7). Subsequent steps move
/// the per-run services (parameters, logging scope, tool-path config) onto this context.
/// </remarks>
internal sealed class BuildContext : IDisposable
{
private static readonly AsyncLocal<BuildContext> s_current = new();

/// <summary>The context for the current build run, or <c>null</c> outside a run.</summary>
public static BuildContext Current => s_current.Value;

private readonly LinkedList<Action> _cancellationHandlers = new();
private readonly ConsoleCancelEventHandler _onCancelKeyPress;
private readonly EventHandler _onToolOptionsCreated;

/// <summary>The per-run parameter service (FT-4 / #309) — replaces the process-global
/// <c>ParameterService.Instance</c>, so prod and tests exercise the same instance form.</summary>
public ParameterService Parameters { get; } =
new(() => EnvironmentInfo.ArgumentParser, () => EnvironmentInfo.Variables);

private BuildContext()
{
_onCancelKeyPress = (_, _) => _cancellationHandlers.ForEach(x => x());
_onToolOptionsCreated = (options, _) => VerbosityMapping.Apply((ToolOptions)options);
Console.CancelKeyPress += _onCancelKeyPress;
ToolOptions.Created += _onToolOptionsCreated;
}

/// <summary>Creates the ambient context for a build run and installs it as <see cref="Current"/>.</summary>
public static BuildContext Activate() => s_current.Value = new BuildContext();

public void RegisterCancellationHandler(Action handler) => _cancellationHandlers.AddFirst(handler);

public void UnregisterCancellationHandler(Action handler) => _cancellationHandlers.Remove(handler);

public void Dispose()
{
// Undo this run's process-global state (the FT-1 cleanup, now owned by the scope).
Console.CancelKeyPress -= _onCancelKeyPress;
ToolOptions.Created -= _onToolOptionsCreated;
Logging.InMemorySink.Instance.Clear();
ValueInjectionUtility.ClearCache();
NuGetToolPathResolver.Reset();
NpmToolPathResolver.Reset();

if (ReferenceEquals(s_current.Value, this))
s_current.Value = null;
}
}
15 changes: 9 additions & 6 deletions src/Fallout.Build/Execution/BuildManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ internal static class BuildManager
{
private const int ErrorExitCode = -1;

private static readonly LinkedList<Action> s_cancellationHandlers = new();

// Facade over the active BuildContext (FT-2): callers register/unregister during a run; the
// context owns the handler list and discards it on dispose.
public static event Action CancellationHandler
{
add => s_cancellationHandlers.AddFirst(value);
remove => s_cancellationHandlers.Remove(value);
add => BuildContext.Current?.RegisterCancellationHandler(value);
remove => BuildContext.Current?.UnregisterCancellationHandler(value);
}

[ModuleInitializer]
Expand All @@ -38,9 +38,10 @@ public static int Execute<T>(Expression<Func<T, Target>>[] defaultTargetExpressi
{
Console.OutputEncoding = Encoding.UTF8;
Console.InputEncoding = Encoding.UTF8;
Console.CancelKeyPress += (_, _) => s_cancellationHandlers.ForEach(x => x());
ToolOptions.Created += (options, _) => VerbosityMapping.Apply((ToolOptions)options);

// The per-run scope owns the global subscriptions + teardown (FT-2 / #307). Disposed at
// method exit, so re-invocation in the same process starts clean even if `new T()` throws.
using var context = BuildContext.Activate();
var build = new T();

try
Expand Down Expand Up @@ -88,6 +89,8 @@ public static int Execute<T>(Expression<Func<T, Target>>[] defaultTargetExpressi
{
Finish();
Log.CloseAndFlush();
// Per-run teardown (handler unsubscription + state reset) is owned by the BuildContext,
// run when `context` is disposed at method exit.
}

void Finish()
Expand Down
11 changes: 8 additions & 3 deletions src/Fallout.Build/Execution/ParameterService.Statics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Fallout.Common.Execution;
using Fallout.Common.Utilities;

namespace Fallout.Common;

internal partial class ParameterService
{
internal static ParameterService Instance = new(
() => EnvironmentInfo.ArgumentParser,
() => EnvironmentInfo.Variables);
// FT-4 (#309): the active instance is the per-run one held on BuildContext, so prod uses the
// same instance form as tests and nothing leaks across runs. The lazy fallback covers the rare
// access outside a build run (no cross-run concern there); it can retire once that's confirmed dead.
private static readonly Lazy<ParameterService> s_ambient = new(
() => new ParameterService(() => EnvironmentInfo.ArgumentParser, () => EnvironmentInfo.Variables));

internal static ParameterService Instance => BuildContext.Current?.Parameters ?? s_ambient.Value;

public static T GetParameter<T>(string name, char? separator = null)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Fallout.Build/Execution/ValueInjectionUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ internal static class ValueInjectionUtility
{
private static readonly Dictionary<MemberInfo, object> s_valueCache = new();

/// <summary>Clears the per-run injected-value cache so a subsequent build in the same process re-injects. FT-1 / #306.</summary>
internal static void ClearCache() => s_valueCache.Clear();

public static T TryGetValue<T>(Expression<Func<T>> parameterExpression)
where T : class
{
Expand Down
8 changes: 7 additions & 1 deletion src/Fallout.Build/Logging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,16 @@ public void Emit(LogEvent logEvent)
_logEvents.Add(logEvent);
}

public void Dispose()
/// <summary>Drops accumulated events so a subsequent build in the same process starts clean. FT-1 / #306.</summary>
public void Clear()
{
_logEvents.Clear();
}

public void Dispose()
{
Clear();
}
}

internal class ExecutingTargetLogEventEnricher : ILogEventEnricher
Expand Down
6 changes: 6 additions & 0 deletions src/Fallout.Tooling/NpmToolPathResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public static class NpmToolPathResolver
{
public static AbsolutePath NpmPackageJsonFile;

/// <summary>Resets the per-run package.json location so a subsequent build in the same process starts from defaults. FT-1 / #306.</summary>
public static void Reset()
{
NpmPackageJsonFile = null;
}

public static string GetNpmExecutable(string npmExecutable)
{
Assert.FileExists(NpmPackageJsonFile);
Expand Down
9 changes: 9 additions & 0 deletions src/Fallout.Tooling/NuGetToolPathResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ public static class NuGetToolPathResolver
public static string NuGetAssetsConfigFile;
public static string PaketPackagesConfigFile;

/// <summary>Resets the per-run package-location config so a subsequent build in the same process starts from defaults. FT-1 / #306.</summary>
public static void Reset()
{
EmbeddedPackagesDirectory = null;
NuGetPackagesConfigFile = null;
NuGetAssetsConfigFile = null;
PaketPackagesConfigFile = null;
}

public static string GetPackageExecutable(string packageId, string packageExecutable, string version = null, string framework = null)
{
Assert.True(packageId != null && packageExecutable != null);
Expand Down
Loading