Skip to content

Instantly share code, notes, and snippets.

@equalent
Created July 22, 2025 21:09
Show Gist options
  • Select an option

  • Save equalent/567c7a63d6815ec44209bbe9221cde0b to your computer and use it in GitHub Desktop.

Select an option

Save equalent/567c7a63d6815ec44209bbe9221cde0b to your computer and use it in GitHub Desktop.
using System.Text.RegularExpressions;
namespace Heath.Build.Generators;
public class NinjaWriter
{
private StreamWriter _writer;
private int _width;
public static string EscapePath(string word)
{
return word.Replace("$ ", "$$ ").Replace(" ", "$ ").Replace(":", "$:");
}
public static string Escape(string str)
{
if (str.Contains('\n'))
{
throw new ArgumentException("Ninja syntax does not allow newlines", str);
}
return str.Replace("$", "$$");
}
public static string Expand(string input, IDictionary<string, string> vars, IDictionary<string, string>? localVars = null)
{
localVars ??= new Dictionary<string, string>();
return Regex.Replace(input, @"\$(\$|\w*)", match =>
{
var var = match.Groups[1].Value;
if (var == "$")
return "$";
return localVars.TryGetValue(var, out var localValue)
? localValue
: vars.TryGetValue(var, out var globalValue)
? globalValue
: "";
});
}
public NinjaWriter(StreamWriter writer, int width = 78)
{
_writer = writer;
_width = width;
}
public void WriteNewline()
{
_writer.WriteLine();
}
public void WriteComment(string text)
{
foreach (var line in Utils.WrapText(text, _width - 2))
{
_writer.WriteLine("# " + line);
}
}
public void WriteVariable(string key, string value, int indent = 0)
{
WriteWrappedLine($"{key} = {value}", indent);
}
public void WriteVariable<T>(string key, T value, int indent = 0) where T : notnull
{
WriteWrappedLine($"{key} = {value.ToString()}", indent);
}
public void WritePool(string name, int depth)
{
WriteWrappedLine($"pool {name}");
WriteVariable("depth", depth, indent: 1);
}
public void WriteRule(string name,
string command,
string? description,
string? depfile,
bool generator,
string? pool,
bool restat = false,
string? rspfile = null,
string? rspfileContent = null,
string? deps = null)
{
WriteWrappedLine($"rule {name}");
WriteVariable("command", command, indent: 1);
if (description is not null)
WriteVariable("description", description, indent: 1);
if (depfile is not null)
WriteVariable("depfile", depfile, indent: 1);
if (generator)
WriteVariable("generator", "1", indent: 1);
if (pool is not null)
WriteVariable("pool", pool, indent: 1);
if (restat)
WriteVariable("restat", "1", indent: 1);
if (rspfile is not null)
WriteVariable("rspfile", rspfile, indent: 1);
if (rspfileContent is not null)
WriteVariable("rspfile_content", rspfileContent, indent: 1);
if (deps is not null)
WriteVariable("deps", deps, indent: 1);
}
public List<string> WriteBuild(
IEnumerable<string> outputs,
string rule,
IEnumerable<string>? inputs = null,
IEnumerable<string>? implicitDeps = null,
IEnumerable<string>? orderOnly = null,
IEnumerable<string>? implicitOutputs = null,
IDictionary<string, string>? variables = null,
string? pool = null,
string? dyndep = null
)
{
var outputList = outputs?.ToList() ?? new();
var outOutputs = outputList.Select(EscapePath).ToList();
var allInputs = inputs?.Select(EscapePath).ToList() ?? new();
if (implicitDeps != null)
{
allInputs.Add("|");
allInputs.AddRange(implicitDeps.Select(EscapePath));
}
if (orderOnly != null)
{
allInputs.Add("||");
allInputs.AddRange(orderOnly.Select(EscapePath));
}
if (implicitOutputs != null)
{
outOutputs.Add("|");
outOutputs.AddRange(implicitOutputs.Select(EscapePath));
}
WriteWrappedLine($"build {string.Join(' ', outOutputs)}: {rule} {string.Join(' ', allInputs)}");
if (pool != null)
WriteWrappedLine($" pool = {pool}");
if (dyndep != null)
WriteWrappedLine($" dyndep = {dyndep}");
if (variables != null)
{
foreach (var kv in variables)
WriteVariable(kv.Key, kv.Value, indent: 1);
}
return outputList;
}
public void WriteInclude(string path)
{
WriteWrappedLine($"include {path}");
}
public void WriteSubninja(string path)
{
WriteWrappedLine($"subninja {path}");
}
public void WriteDefault(IEnumerable<string> paths)
{
WriteWrappedLine($"default {string.Join(' ', paths)}");
}
private int CountDollarsBeforeIndex(string text, int index)
{
int count = 0;
for (int i = index - 1; i >= 0 && text[i] == '$'; i--)
count++;
return count;
}
private void WriteWrappedLine(string text, int indent = 0)
{
string leadingSpace = new string(' ', indent * 2);
while (leadingSpace.Length + text.Length > _width)
{
int availableSpace = _width - leadingSpace.Length - " $\n".Length;
int space = availableSpace;
// Look for rightmost unescaped space
while (true)
{
space = text.LastIndexOf(' ', 0, space);
if (space < 0 || CountDollarsBeforeIndex(text, space) % 2 == 0)
break;
}
// If no safe space found, try forward search
if (space < 0)
{
space = availableSpace - 1;
while (true)
{
space = text.IndexOf(' ', space + 1);
if (space < 0 || CountDollarsBeforeIndex(text, space) % 2 == 0)
break;
}
}
if (space < 0)
{
// Give up and write the rest as-is
break;
}
_writer.WriteLine(leadingSpace + text.Substring(0, space) + " $");
text = text.Substring(space + 1);
// Continuation lines are indented more
leadingSpace = new string(' ', (indent + 2) * 2);
}
_writer.WriteLine(leadingSpace + text);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment