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
22 changes: 18 additions & 4 deletions .buildvana/hooks/release/post-release.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,31 @@
using System.Text.RegularExpressions;
using Buildvana.Runtime;

// release/post-release hook: keeps the $schema URL in buildvana.jsonc pointing at the release tag
// of the version being released. The guard mirrors the built-in self-reference rewrites: the
// release/post-release hook: keeps the $schema URL in the configuration file pointing at the release
// tag of the version being released. The guard mirrors the built-in self-reference rewrites: the
// $schema URL is itself a self-reference, so it moves only when dogfooding moves the rest.
var hookArgs = PostReleaseHookArgs.Load();
if (!hookArgs.Dogfooding)
{
return;
}

// AFTER THE NEXT RELEASE: replace the search below with
// var configFile = hookArgs.RuntimeInfo.ConfigFile;
// keeping the null check. That member names the file bv itself read, which is what a hook rewriting the
// configuration file should act on, and what Hooks.md tells hooks to use instead of searching for one.
// It cannot be used yet: the SDK pins Buildvana.Runtime to its own version, so this hook compiles against
// the last published release, and RuntimeInfo.ConfigFile ships with the next one. Searching is correct in
// the meantime — it is the same search bv performs, over the directory bv reports as home — the hook just
// answers on its own rather than being told.
var configFile = BuildvanaConfig.FindFile(hookArgs.RuntimeInfo.HomeDirectory);
if (configFile is null)
{
return;
}

// Same expression as SelfVersionService.SchemaUrlRegex in src/Buildvana.Tool, which `bv update` applies to
// consumer repositories' configuration files; keep the two copies identical.
var text = File.ReadAllText("buildvana.jsonc");
var text = File.ReadAllText(configFile);
text = Regex.Replace(text, "(Tenacom/Buildvana/)[^/]+(/schemas/)", $"${{1}}{hookArgs.Release.SemVer}$2");
File.WriteAllText("buildvana.jsonc", text);
File.WriteAllText(configFile, text);
5 changes: 2 additions & 3 deletions CHANGELOG.md

Large diffs are not rendered by default.

File renamed without changes.
8 changes: 3 additions & 5 deletions docs/DirectoryStructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ We will follow the MSBuild convention of a backslash (`\`) as a path separator.
| | +--- release\
| | |
| | +--- post-release.cs
| |
| +--- buildvana.jsonc <<< Buildvana configuration file, if not in the home directory root
|
+--- .buildvana-temp\ <<< bv's scratch directory (machine-generated; add to .gitignore)
|
Expand All @@ -63,7 +61,7 @@ We will follow the MSBuild convention of a backslash (`\`) as a path separator.
| +--- Common.props <<< Portions of MSBuild code common to all projects in tests\
| +--- Common.targets
|
+--- buildvana.jsonc <<< Buildvana configuration file (or buildvana.json), if not in .buildvana\
+--- buildvana.jsonc <<< Buildvana configuration file (or buildvana.json)
|
+--- Common.props <<< Common parts of MSBuild projects
+--- Common.targets
Expand Down Expand Up @@ -108,11 +106,11 @@ The full path of the home directory, including a trailing path separator, is sto

Buildvana SDK determines the location of the home directory by walking up the directory hierarchy, starting from the project's directory (included), and stopping at the nearest directory that contains any of these home markers:

- a Buildvana configuration file (`buildvana.json` or `buildvana.jsonc`), either directly in the directory or in a `.buildvana` subdirectory (a `.buildvana` directory without a configuration file is _not_ a marker);
- a Buildvana configuration file (`buildvana.json` or `buildvana.jsonc`);
- a Git worktree or submodule (a file named `.git`);
- a regular Git repository (a file named `HEAD` in a `.git` subdirectory).

The directory containing the marker becomes the home directory, and its full path becomes the value of `HomeDirectory`. Note that a configuration file inside `.buildvana` marks the directory containing `.buildvana`, not `.buildvana` itself. A configuration file does not have to actually configure anything: an empty JSON object (`{}`) is valid content, making the file usable as a pure home-directory marker.
The directory containing the marker becomes the home directory, and its full path becomes the value of `HomeDirectory`. Every marker sits in the directory it marks, so nothing under a subdirectory — the `.buildvana` directory included — takes part in discovery: hooks are projects living under `.buildvana\`, and a marker recognized in there would make each of them discover `.buildvana\` as its own home directory. A configuration file does not have to actually configure anything: an empty JSON object (`{}`) is valid content, making the file usable as a pure home-directory marker.

If no marker is found, the build (or project loading in Visual Studio) stops with error [BVSDK1003](SdkDiagnostics.md#buildvana-sdk-core-1000-1049).

Expand Down
23 changes: 19 additions & 4 deletions docs/Hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ The well-known paths themselves ship in the package too: `WellKnownPaths` expose
| `RuntimeInfo.HomeDirectory` | string | Absolute path of the home directory, without a trailing separator (also the hook's working directory). |
| `RuntimeInfo.ArtifactsDirectory` | string | Absolute path of the directory containing the build artifacts. |
| `RuntimeInfo.ScratchDirectory` | string | Absolute path of bv's scratch directory (`.buildvana-temp/`), where hooks can write temporary files without affecting working-tree change detection. |
| `RuntimeInfo.ConfigFile` | string or null | Absolute path of the configuration file this run read, or `null` when the repository has none. See [Loading the repository configuration](#loading-the-repository-configuration). |
| `Release.Version` | string | The version being released, in simple `MAJOR.MINOR.PATCH` form, without any prerelease tag. |
| `Release.SemVer` | string | The version being released, in full semantic version form. This is the form used by release tags and embedded in artifact names. |
| `Release.PreviousVersion` | string or null | The previously released version (the latest release tag reachable from `HEAD`), or `null` when no previous release exists. |
Expand All @@ -94,14 +95,26 @@ In the JSON file, member names are camelCase (`runtimeInfo.homeDirectory`, `rele

## Loading the repository configuration

The args carry the facts of the run; for any standing repository setting, load the configuration file instead: `BuildvanaConfig.Load()` probes the four well-known candidates (`buildvana.json`, `buildvana.jsonc`, and the same names under `.buildvana/`), applies the usual exactly-one rule, tolerates comments and trailing commas, and returns the typed configuration (an empty instance when no configuration file exists):
The args carry the facts of the run; for any standing repository setting, load the configuration instead. `hookArgs.LoadConfig()` reads the file `bv` itself read for this run, and returns the typed configuration (an empty instance when the repository has no configuration file):

```csharp
var config = BuildvanaConfig.Load();
var config = hookArgs.LoadConfig();
var branches = config.Release?.Branches;
```

The loader is strict — an unknown member fails the load — but does not re-validate what `bv` has already validated with schema-based diagnostics before running any hook.
Which file to read comes from the args, so a hook never searches for one; what it says is read at the moment of the call, so the hook sees the file as it stands even if an earlier hook in the same run rewrote it. The loader tolerates comments and trailing commas, and is strict about content — an unknown member fails the load — but it does not re-validate what `bv` has already validated with schema-based diagnostics before running any hook.

`BuildvanaConfig.Load()`, which searches a directory for a configuration file, remains available for code that has no hook args to hand.

A hook that works on the configuration file _itself_ — rewriting a value in it, say — needs the path rather than the settings, and must act on the file `bv` actually read. That path is in the args, as `RuntimeInfo.ConfigFile` (`null` when the repository has no configuration file); do not hardcode a file name, and do not search for one:

```csharp
var configFile = hookArgs.RuntimeInfo.ConfigFile;
if (configFile is not null)
{
File.WriteAllText(configFile, Rewrite(File.ReadAllText(configFile)));
}
```

## Dependencies

Expand All @@ -124,4 +137,6 @@ Local file-based-app caching may not notice implicit-build-file changes; CI is a

## Contract evolution

The args file is written by the installed `bv` and read through the `Buildvana.Runtime` version pinned by the repository's Buildvana SDK; `bv` and the SDK are released in lockstep and designed as a matched pair. The contract is nevertheless additive-only: new members may be added, but existing ones are never removed or repurposed, and additions ship as optional members with default values — so an args file written before an update stays loadable after it.
The args file is written by the installed `bv` and read through the `Buildvana.Runtime` version pinned by the repository's Buildvana SDK — the version of the SDK in use, which `bv` refuses to run against unless it matches its own. The hook is compiled from source at every run, and its args file is rewritten immediately before it. Writer and reader are therefore the same version by construction, and the JSON never has to survive a version boundary.

What must stay stable is the _source_ surface a hook compiles against: members are never removed or repurposed, so that a hook written today still compiles after an update. Additions may be required members — every run then states every fact the args carry, and none can be left unset by mistake. (An args file left over from a run that predates such an addition no longer loads; re-run the command that raises the hook, and it is rewritten.)
2 changes: 1 addition & 1 deletion docs/SdkDiagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Each module is assigned a contiguous range of 100 diagnostics, as listed below.
| BVSDK1002 | Error | Sdk.props and Sdk.targets are in different directories. | `Sdk.props` and `Sdk.targets` were imported from two different versions of Buildvana SDK; look for stray `Version` attributes in the `<Import>` directives. |
| BVSDK1003 | Error | Home directory not defined. | No suitable value for the `HomeDirectory` property has been found. |
| BVSDK1004 | Error | Buildvana SDK requires at least MSBuild v... | You are trying to use Buildvana SDK with an unsupported version of MSBuild. See [the README](../README.md#toolchain) for a list of supported MSBuild versions. |
| BVSDK1005 | Error | Multiple Buildvana configuration files found. | A home directory contains more than one configuration file (in the root and/or the .buildvana subdirectory); keep only one. |
| BVSDK1005 | Error | Multiple Buildvana configuration files found. | A home directory contains both `buildvana.json` and `buildvana.jsonc`; keep only one. |

## Buildvana SDK tasks (1050-1099)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Buildvana.Core.HomeDirectory;
using Buildvana.Core.IO;
using Buildvana.Core.JsonSchema;
using Buildvana.Runtime;
Expand All @@ -14,47 +16,79 @@
namespace Buildvana.Core.Configuration;

/// <summary>
/// Loads and validates the Buildvana configuration file found in a home directory.
/// Provides the Buildvana configuration of a home directory: which file holds it, and what it says.
/// </summary>
/// <remarks>
/// <para>Unlike the lean loader shipped with <c>Buildvana.Runtime</c> (<see cref="BuildvanaConfig.Load"/>), this
/// loader validates the file against the configuration schema and reports each problem as a diagnostic with its
/// source location. It is the loader used by <c>bv</c> and by Buildvana SDK tasks; hooks, which read a file
/// <c>bv</c> has already validated, use the lean loader instead.</para>
/// one validates the file against the configuration schema and reports each problem as a diagnostic with its
/// source location. It is used by <c>bv</c> and by Buildvana SDK tasks; hooks, which read a file <c>bv</c> has
/// already validated, use the lean loader instead.</para>
/// <para><see cref="Path"/> and <see cref="Config"/> are each resolved on first read and cached — result and
/// exception alike — for the lifetime of the instance, as <c>HomeDirectoryProvider</c> does for the home
/// directory. Finding the file is this class's business alone, so the path a run reports and the file it parses
/// are the same by construction rather than by agreement between callers.</para>
/// </remarks>
public static class BuildvanaConfigLoader
public sealed class BuildvanaConfigProvider
{
private static readonly JsonDocumentOptions DocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
};

private readonly Lazy<string?> _lazyPath;
private readonly Lazy<BuildvanaConfig> _lazyConfig;

/// <summary>
/// Initializes a new instance of the <see cref="BuildvanaConfigProvider"/> class.
/// </summary>
/// <param name="home">The provider of the home directory the configuration file is looked for in.</param>
public BuildvanaConfigProvider(IHomeDirectoryProvider home)
{
Guard.IsNotNull(home);
_lazyPath = new Lazy<string?>(() => FindFile(home));

// Reads the path through its own Lazy, so that a run holding both facts has probed exactly once.
_lazyConfig = new Lazy<BuildvanaConfig>(() => LoadFile(_lazyPath.Value));
}

/// <summary>
/// Loads the configuration file found in <paramref name="homeDirectory"/>.
/// Gets the absolute path of the configuration file, or <see langword="null"/> when the home directory
/// holds none.
/// </summary>
/// <param name="homeDirectory">The home directory to search for a configuration file.</param>
/// <returns>The parsed configuration, or an empty <see cref="BuildvanaConfig"/> if no file is present.</returns>
/// <exception cref="BuildFailedException">
/// <para>More than one configuration file is present (among <c>buildvana.json</c>, <c>buildvana.jsonc</c>,
/// <c>.buildvana/buildvana.json</c>, and <c>.buildvana/buildvana.jsonc</c>), or the file cannot be read.</para>
/// Both configuration files (<c>buildvana.json</c> and <c>buildvana.jsonc</c>) are present.
/// </exception>
public string? Path => _lazyPath.Value;

/// <summary>
/// Gets the parsed configuration, or an empty <see cref="BuildvanaConfig"/> when the home directory holds
/// no configuration file.
/// </summary>
/// <exception cref="BuildFailedException">
/// <para>Both configuration files (<c>buildvana.json</c> and <c>buildvana.jsonc</c>) are present,
/// or the file cannot be read.</para>
/// <para>The file is present but not valid JSON, or does not conform to the schema; in that case
/// <see cref="BuildFailedException.Diagnostics"/> lists each problem with its source location.</para>
/// </exception>
public static BuildvanaConfig Load(string homeDirectory)
{
Guard.IsNotNullOrEmpty(homeDirectory);

string? path;
try
{
path = BuildvanaConfig.FindFile(homeDirectory);
}
catch (BuildvanaRuntimeException e)
{
throw new BuildFailedException(e.Message, e);
}
public BuildvanaConfig Config => _lazyConfig.Value;

/// <summary>
/// Loads the configuration file at an already-known path, bypassing both the search and the cache.
/// </summary>
/// <param name="path">The path of the configuration file, or <see langword="null"/> for none.</param>
/// <returns>The parsed configuration, or an empty <see cref="BuildvanaConfig"/> if <paramref name="path"/>
/// is <see langword="null"/>.</returns>
/// <exception cref="BuildFailedException">
/// <para>The file cannot be read, is not valid JSON, or does not conform to the schema; in the latter cases
/// <see cref="BuildFailedException.Diagnostics"/> lists each problem with its source location.</para>
/// </exception>
/// <remarks>
/// <para>For the caller that must re-read a file it has just rewritten, and therefore wants the parse this
/// instance has cached to be bypassed rather than reused. Everything else reads <see cref="Config"/>.</para>
/// </remarks>
public static BuildvanaConfig LoadFile(string? path)
{
if (path is null)
{
return new BuildvanaConfig();
Expand All @@ -68,6 +102,20 @@ public static BuildvanaConfig Load(string homeDirectory)
return node!.Deserialize(BuildvanaJsonContext.Default.BuildvanaConfig) ?? new BuildvanaConfig();
}

// The one probe for the configuration file: nothing outside this class asks which file a home directory
// holds, so no two callers can answer differently.
private static string? FindFile(IHomeDirectoryProvider home)
{
try
{
return BuildvanaConfig.FindFile(home.HomeDirectory);
}
catch (BuildvanaRuntimeException e)
{
throw new BuildFailedException(e.Message, e);
}
}

// Removes a leading UTF-8 byte order mark, if present, so the reader sees only JSON and positions start at 1.
private static byte[] StripBom(byte[] bytes)
=> bytes is [0xEF, 0xBB, 0xBF, .. var rest] ? rest : bytes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ public DiscoveredHomeDirectoryProvider(string startDirectory)
protected override string Resolve()
=> HomeDirectoryDiscovery.TryDiscover(_startDirectory, out var homeDirectory)
? homeDirectory
: throw new BuildFailedException($"Home directory not defined (no buildvana.json[c], .buildvana/buildvana.json[c], .git, or .git/HEAD found at or above '{_startDirectory}').");
: throw new BuildFailedException($"Home directory not defined (no buildvana.json[c], .git, or .git/HEAD found at or above '{_startDirectory}').");
}
Loading