Created
July 22, 2025 21:09
-
-
Save equalent/567c7a63d6815ec44209bbe9221cde0b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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