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
4 changes: 4 additions & 0 deletions docs/website/02-fundamentals/06-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ SET MY_PARAMETER = <value>
SET FALLOUT_MY_PARAMETER = <value>
```

Environment variables are checked in this order: the exact parameter name, then the `FALLOUT_` or `NUKE_` prefixed name, then the bare name with casing and underscores ignored. Use the prefix when the bare name collides with an unrelated variable. For example, login shells on some systems export `HOST` with the machine's hostname, which would otherwise be read as the `Host` parameter.

If a value cannot be converted, Fallout logs a warning and uses the default for that parameter. It does not stop the build.

## Required Parameters

You can specify a parameter as a [target requirement](../02-fundamentals/05-targets.md#requirements) using the following shorthand syntax:
Expand Down
32 changes: 21 additions & 11 deletions src/Fallout.Build/Execution/ParameterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,18 @@ static string GetTrimmedName(string name)
if (!Variables.TryGetValue(variableName, out var value))
{
var trimmedVariableName = GetTrimmedName(variableName);
var alternativeValues = Variables
.Where(x => GetTrimmedName(x.Key).EqualsOrdinalIgnoreCase(trimmedVariableName) ||

// Prefixed names are matched before the bare parameter name. Otherwise an unrelated
// bare variable would shadow the prefixed one, for example the HOST variable that login
// shells on openSUSE export would shadow FALLOUT_HOST.
var prefixedValues = Variables
.Where(x => GetTrimmedName(x.Key).EqualsOrdinalIgnoreCase($"FALLOUT{trimmedVariableName}") ||
GetTrimmedName(x.Key).EqualsOrdinalIgnoreCase($"NUKE{trimmedVariableName}")).ToList();

var alternativeValues = prefixedValues.Count > 0
? prefixedValues
: Variables.Where(x => GetTrimmedName(x.Key).EqualsOrdinalIgnoreCase(trimmedVariableName)).ToList();

if (alternativeValues.Count > 1)
{
Log.Warning("Could not resolve {VariableName} since multiple values are provided", variableName);
Comment thread
SonnyRR marked this conversation as resolved.
Expand All @@ -215,15 +223,17 @@ static string GetTrimmedName(string name)
}
catch (Exception ex)
{
Assert.Fail(new[]
{
ex.Message,
$"Resolving parameter '{variableName}' failed. Environment variable was:",
value
}.JoinNewLine());

// ReSharper disable once HeuristicUnreachableCode
return null;
// The environment is shared with the operating system, so an unrelated variable such as
// the Unix HOST can match a parameter by accident. Built-in parameters are also resolved
// in FalloutBuild's static constructor, where throwing aborts the process before any
// target runs. Warn and use the default instead.
Log.Warning(
"Could not resolve {VariableName} from environment variable value {Value}. Using the default value instead. {Reason}",
variableName,
value,
ex.Message);

return destinationType.GetDefaultValue();
}
}
}
146 changes: 146 additions & 0 deletions tests/Fallout.Build.Specs/ParameterServiceSpecs.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
using System;
using System.Collections.Generic;
using Fallout.Build.Execution;
using Fallout.Common.Execution;
using Fallout.Common.IO;
using FluentAssertions;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Xunit;
using static Fallout.Common.Utilities.ReflectionUtility;

namespace Fallout.Common.Specs;

[Collection(ProcessGlobalStateCollection.Name)]
public class ParameterServiceSpecs
{
private ParameterService GetService(string[] commandLineArguments = null,
Expand Down Expand Up @@ -171,6 +176,147 @@ public void TestPrecedence()
service.GetParameter("string", typeof(string), separator: null).Should().Be("commitMessage");
}

[Fact]
public void UnusableHostEnvironmentValue_UsesDefaultAndWarns()
{
// Arrange. The standard Unix HOST variable. FalloutBuild resolves the Host parameter
// from the environment before any target runs.
var environmentVariables = new Dictionary<string, string> { ["HOST"] = "M2" };

// Act
var (value, warnings) = ResolveWithEnvironment("Host", typeof(Host), environmentVariables);

// Assert
value.Should().BeNull();
warnings.Should().ContainSingle().Which.Should().Contain("Host").And.Contain("M2");
}

[Fact]
public void UnusableEnvironmentValue_UsesDefaultAndWarns()
{
// Arrange
var environmentVariables = new Dictionary<string, string> { ["VERBOSITY"] = "bogus" };

// Act
var (value, warnings) = ResolveWithEnvironment("Verbosity", typeof(Verbosity?), environmentVariables);

// Assert
value.Should().BeNull();
warnings.Should().ContainSingle().Which.Should().Contain("Verbosity").And.Contain("bogus");
}

[Fact]
public void SeveralMatchingEnvironmentValues_UseDefaultAndWarn()
{
// Arrange. Two spellings of the same parameter name, neither an exact match.
var environmentVariables = new Dictionary<string, string>
{
["verbosity"] = "Normal",
["VERBOSITY"] = "Verbose"
};

// Act
var (value, warnings) = ResolveWithEnvironment("Verbosity", typeof(Verbosity?), environmentVariables);

// Assert
value.Should().BeNull();
warnings.Should().ContainSingle().Which.Should().Contain("multiple values are provided");
}

[Theory]
[InlineData("FALLOUT_VERBOSITY")]
[InlineData("fallout_verbosity")]
[InlineData("Fallout_Verbosity")]
public void PrefixedEnvironmentValue_IsResolved(string name)
{
// Arrange
var environmentVariables = new Dictionary<string, string> { [name] = "Verbose" };

// Act
var (value, _) = ResolveWithEnvironment("Verbosity", typeof(Verbosity?), environmentVariables);

// Assert
value.Should().Be(Verbosity.Verbose);
}

[Fact]
public void PrefixedHostValue_TakesPrecedenceOverBareHost()
{
// Arrange. The Unix hostname variable is present and cannot be converted.
var environmentVariables = new Dictionary<string, string>
{
["HOST"] = "M2",
["FALLOUT_HOST"] = "Terminal"
};

// Act
var (value, warnings) = ResolveWithEnvironment("Host", typeof(Host), environmentVariables);

// Assert
value.Should().BeOfType<Terminal>();
warnings.Should().BeEmpty();
}

[Fact]
public void LegacyPrefixedHostValue_TakesPrecedenceOverBareHost()
{
// Arrange
var environmentVariables = new Dictionary<string, string>
{
["HOST"] = "M2",
["NUKE_HOST"] = "Terminal"
};

// Act
var (value, warnings) = ResolveWithEnvironment("Host", typeof(Host), environmentVariables);

// Assert
value.Should().BeOfType<Terminal>();
warnings.Should().BeEmpty();
}

/// <summary>
/// Resolves one parameter from the given environment and returns it together with the warnings
/// logged for that parameter. Swaps the process-wide logger for the duration of the call, so
/// the sink keeps only events tagged with the parameter name. Test classes outside the shared
/// collection run in parallel and would otherwise leak their log output into the result.
/// </summary>
private (object Value, IReadOnlyList<string> Warnings) ResolveWithEnvironment(
string parameterName,
Type destinationType,
IDictionary<string, string> environmentVariables)
{
var service = GetService(environmentVariables: environmentVariables);
var sink = new ParameterWarningSink(parameterName);
var original = Log.Logger;

try
{
Log.Logger = new LoggerConfiguration().WriteTo.Sink(sink, LogEventLevel.Warning).CreateLogger();
var value = service.GetParameter(parameterName, destinationType, separator: null);
return (value, sink.Messages);
}
finally
{
Log.Logger = original;
}
}

private class ParameterWarningSink(string parameterName) : ILogEventSink
{
public List<string> Messages { get; } = new();

public void Emit(LogEvent logEvent)
{
if (logEvent.Properties.TryGetValue("VariableName", out var property) &&
property is ScalarValue { Value: string name } &&
name == parameterName)
{
Messages.Add(logEvent.RenderMessage());
}
}
}

#pragma warning disable CS0649
private class TestBuild : FalloutBuild, ITestComponent
{
Expand Down
Loading