Skip to content
Open
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
61 changes: 61 additions & 0 deletions src/Fallout.Build/Execution/BuildContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static readonly AsyncLocal<BuildContext> s_current = new();
private static readonly AsyncLocal<BuildContext> currentInstance = new();

probably predating the rules change we made this week, so wrong naming convention...


/// <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;

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
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