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
21 changes: 19 additions & 2 deletions src/Fallout.Build/Execution/Extensions/BuildGraphUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,23 @@ private static TargetModel ToModel(ExecutableTarget target)
SortedNames(target.ExecutionDependencies),
SortedNames(target.OrderDependencies),
SortedNames(target.TriggerDependencies),
SortedNames(target.Triggers));
SortedNames(target.Triggers),
ToStatus(target.Status));

// Projects the internal execution status onto the small vocabulary the graph control
// renders. Neutral states (not scheduled, not run, collective grouping) emit null, so a
// structural/plan snapshot stays unstyled and only a real run paints the nodes. Additive:
// a consumer that predates the field simply ignores it, so this is not a schema break.
private static string ToStatus(ExecutionStatus status)
=> status switch
{
ExecutionStatus.Scheduled => "queued",
ExecutionStatus.Running => "running",
ExecutionStatus.Succeeded => "succeeded",
ExecutionStatus.Failed or ExecutionStatus.Aborted => "failed",
ExecutionStatus.Skipped => "skipped",
_ => null,
};

// Sorted for deterministic output — the graph carries no execution order, so the display
// order is irrelevant to consumers and a stable ordering avoids spurious file churn.
Expand All @@ -95,5 +111,6 @@ internal sealed record TargetModel(
IReadOnlyList<string> DependsOn,
IReadOnlyList<string> After,
IReadOnlyList<string> TriggeredBy,
IReadOnlyList<string> Triggers);
IReadOnlyList<string> Triggers,
string Status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,58 @@
namespace Fallout.Build.Execution.Extensions;

/// <summary>
/// Emits <c>build-graph.json</c> into the temporary directory on every build initialization,
/// giving editor tooling (the VS Code extension) a machine-readable projection of the target
/// graph. The projection itself lives in <see cref="BuildGraphUtility"/>; this attribute only owns
/// the build-lifecycle hook and the file write. Best-effort — a serialization failure never fails
/// the build.
/// Emits <c>build-graph.json</c> into the temporary directory at build initialization, then re-emits
/// it on every target state transition so editor tooling (the VS Code extension) can watch the file
/// and animate the run live — queued → running → succeeded/failed. The projection itself lives in
/// <see cref="BuildGraphUtility"/>; this attribute only owns the build-lifecycle hooks and the file
/// write. Best-effort — a serialization failure never fails the build.
/// </summary>
internal class SerializeBuildGraphAttribute : BuildExtensionAttributeBase, IOnBuildInitialized
internal class SerializeBuildGraphAttribute : BuildExtensionAttributeBase,
IOnBuildInitialized, IOnTargetRunning, IOnTargetSucceeded, IOnTargetFailed, IOnTargetSkipped
{
private const string GraphFileName = "build-graph.json";

// Guards the file write only; the executor updates target.Status in place, so each transition
// re-serializes the same captured collection with its now-current statuses.
private readonly object writeLock = new();
private IReadOnlyCollection<ExecutableTarget> targets;
private string falloutVersion;

private AbsolutePath GraphFile => Build.TemporaryDirectory / GraphFileName;

public void OnBuildInitialized(
IReadOnlyCollection<ExecutableTarget> executableTargets,
IReadOnlyCollection<ExecutableTarget> executionPlan)
{
targets = executableTargets;
falloutVersion = FindFalloutVersion();
WriteGraph();
}

// Each transition re-emits the whole graph with current statuses. The executor sets
// target.Status before invoking these, so the write reflects the transition that just happened.
public void OnTargetRunning(ExecutableTarget target) => WriteGraph();

public void OnTargetSucceeded(ExecutableTarget target) => WriteGraph();

public void OnTargetFailed(ExecutableTarget target) => WriteGraph();

public void OnTargetSkipped(ExecutableTarget target) => WriteGraph();

private void WriteGraph()
{
if (targets == null)
{
return;
}

try
{
var json = BuildGraphUtility.GetJsonString(executableTargets, FindFalloutVersion());
GraphFile.WriteAllText(json);
var json = BuildGraphUtility.GetJsonString(targets, falloutVersion);
lock (writeLock)
{
GraphFile.WriteAllText(json);
}
}
catch (Exception exception)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"triggeredBy": [],
"triggers": [
"Publish"
]
],
"status": null
},
{
"name": "Publish",
Expand All @@ -28,7 +29,8 @@
"triggeredBy": [
"Test"
],
"triggers": []
"triggers": [],
"status": null
},
{
"name": "Restore",
Expand All @@ -39,7 +41,8 @@
"dependsOn": [],
"after": [],
"triggeredBy": [],
"triggers": []
"triggers": [],
"status": null
},
{
"name": "Test",
Expand All @@ -54,7 +57,8 @@
"Restore"
],
"triggeredBy": [],
"triggers": []
"triggers": [],
"status": null
}
]
}
29 changes: 28 additions & 1 deletion tests/Fallout.Build.Specs/BuildGraphUtilitySpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,34 @@ public void Root_and_target_property_names_are_camelCase()
firstTarget.EnumerateObject().Select(x => x.Name)
.Should().Equal(
"name", "description", "declaredIn", "default", "listed",
"dependsOn", "after", "triggeredBy", "triggers");
"dependsOn", "after", "triggeredBy", "triggers", "status");
}

[Theory]
[InlineData(ExecutionStatus.Scheduled, "queued")]
[InlineData(ExecutionStatus.Running, "running")]
[InlineData(ExecutionStatus.Succeeded, "succeeded")]
[InlineData(ExecutionStatus.Failed, "failed")]
[InlineData(ExecutionStatus.Aborted, "failed")]
[InlineData(ExecutionStatus.Skipped, "skipped")]
// Neutral states emit null so a structural/plan snapshot stays unstyled; only a real run paints.
[InlineData(ExecutionStatus.None, null)]
[InlineData(ExecutionStatus.NotRun, null)]
[InlineData(ExecutionStatus.Collective, null)]
public void Execution_status_projects_to_the_control_vocabulary(ExecutionStatus status, string expected)
{
var target = new ExecutableTarget { Name = "Compile", Status = status };

BuildGraphUtility.GetModel(new[] { target }, SampleVersion).Targets.Single().Status
.Should().Be(expected);
}

[Fact]
public void Status_is_added_without_bumping_the_schema_version()
{
// The status field is additive — a consumer that predates it ignores the extra key — so it
// rides schema v1. This guard pairs with Schema_version_is_1 to make that intent explicit.
BuildGraphUtility.SchemaVersion.Should().Be(1);
}

[Theory]
Expand Down
Loading