Nuke.Cola
Loading...
Searching...
No Matches
ToolEx.cs
1using System.Collections.Concurrent;
2using System.Diagnostics;
3using System.Text;
4using Nuke.Common;
5using Nuke.Common.Tooling;
6using Nuke.Common.Utilities;
7using Serilog;
8using Serilog.Events;
9
10namespace Nuke.Cola.Tooling;
11
12/// <summary>
13/// Extended copy of Tool delegate of Nuke
14/// </summary>
15public delegate IReadOnlyCollection<Output>? ToolEx(
16 // Nuke Tool
17 ArgumentStringHandler arguments = default,
18 string? workingDirectory = null,
19 IReadOnlyDictionary<string, string>? environmentVariables = null,
20 int? timeout = null,
21 bool? logOutput = null,
22 bool? logInvocation = null,
23 Action<OutputType, string>? logger = null,
24 Action<IProcess>? exitHandler = null,
25
26 // Extension
27 Action<StreamWriter>? input = null,
28 Encoding? standardOutputEncoding = null,
29 Encoding? standardInputEncoding = null
30);
31
32/// <summary>
33/// A record listing Tool and ToolEx delegate parameters and provides a way to meaningfully merge multiple together
34/// </summary>
35/// <param name="ToolArgs">Regular Tool delegate arguments</param>
36/// <param name="Input">Handle standard input stream after process creation</param>
37/// <param name="StandardOutputEncoding">Encoding for standard output. Default is UTF8 (with BOM)</param>
38/// <param name="StandardInputEncoding">Encoding for standard input. Default is UTF8 (without BOM)</param>
39public record class ToolExArguments(
40 ToolArguments ToolArgs,
41 Action<StreamWriter>? Input = null,
42 Encoding? StandardOutputEncoding = null,
43 Encoding? StandardInputEncoding = null
44) {
45
46 /// <summary>
47 /// Merge two ToolEx argument records together.
48 /// </summary>
49 /// <remarks>
50 /// <list>
51 /// <item><term>Arguments </term><description> will be concatenated</description></item>
52 /// <item><term>Working directory </term><description> B overrides the one from A but not when B doesn't have one</description></item>
53 /// <item><term>Environmnent variables </term><description> will be merged</description></item>
54 /// <item><term>TimeOut </term><description> will be maxed</description></item>
55 /// <item><term>LogOutput </term><description> is OR-ed</description></item>
56 /// <item><term>LogInvocation </term><description> is OR-ed</description></item>
57 /// <item><term>Logger / ExitHandler </term><description> A + B is invoked</description></item>
58 /// <item><term>Input </term><description> A + B is invoked</description></item>
59 /// <item><term>Encodings </term><description> B overrides the one from A but not when B doesn't have one</description></item>
60 /// </list>
61 /// </remarks>
62 public static ToolExArguments operator | (ToolExArguments? a, ToolExArguments? b)
63 => new(
64 a?.ToolArgs | b?.ToolArgs,
65 a?.Input + b?.Input,
66 b?.StandardOutputEncoding ?? a?.StandardOutputEncoding,
67 b?.StandardInputEncoding ?? a?.StandardInputEncoding
68 );
69
70 /// <summary>
71 /// Merge a ToolEx and a Tool argument record together.
72 /// </summary>
73 /// <remarks>
74 /// <list>
75 /// <item><term>Arguments </term><description> will be concatenated</description></item>
76 /// <item><term>Working directory </term><description> B overrides the one from A but not when B doesn't have one</description></item>
77 /// <item><term>Environmnent variables </term><description> will be merged</description></item>
78 /// <item><term>TimeOut </term><description> will be maxed</description></item>
79 /// <item><term>LogOutput </term><description> is OR-ed</description></item>
80 /// <item><term>LogInvocation </term><description> is OR-ed</description></item>
81 /// <item><term>Logger / ExitHandler </term><description> A + B is invoked</description></item>
82 /// <item><term>Input </term><description> Used from ToolEx arguments</description></item>
83 /// <item><term>Encodings </term><description> Used from ToolEx arguments</description></item>
84 /// </list>
85 /// </remarks>
86 public static ToolExArguments operator | (ToolExArguments? a, ToolArguments? b)
87 => new(
88 a?.ToolArgs | b,
89 a?.Input,
90 a?.StandardOutputEncoding,
91 a?.StandardInputEncoding
92 );
93
94 /// <summary>
95 /// Merge a Tool and a ToolEx argument record together.
96 /// </summary>
97 /// <remarks>
98 /// <list>
99 /// <item><term>Arguments </term><description> will be concatenated</description></item>
100 /// <item><term>Working directory </term><description> B overrides the one from A but not when B doesn't have one</description></item>
101 /// <item><term>Environmnent variables </term><description> will be merged</description></item>
102 /// <item><term>TimeOut </term><description> will be maxed</description></item>
103 /// <item><term>LogOutput </term><description> is OR-ed</description></item>
104 /// <item><term>LogInvocation </term><description> is OR-ed</description></item>
105 /// <item><term>Logger / ExitHandler </term><description> A + B is invoked</description></item>
106 /// <item><term>Input </term><description> Used from ToolEx arguments</description></item>
107 /// <item><term>Encodings </term><description> Used from ToolEx arguments</description></item>
108 /// </list>
109 /// </remarks>
110 public static ToolExArguments operator | (ToolArguments? a, ToolExArguments? b)
111 => new(
112 a | b?.ToolArgs,
113 b?.Input,
114 b?.StandardOutputEncoding,
115 b?.StandardInputEncoding
116 );
117}
118
119/// <summary>
120/// Propagated ToolEx delegate provider for launch parameter composition.
121/// </summary>
122/// <param name="Target"></param>
123/// <param name="PropagateArguments"></param>
124public record class PropagateToolExExecution(ToolEx Target, ToolExArguments? PropagateArguments = null)
125{
126 public IReadOnlyCollection<Output>? Execute(
127 // Nuke Tool
128 ArgumentStringHandler arguments = default,
129 string? workingDirectory = null,
130 IReadOnlyDictionary<string, string>? environmentVariables = null,
131 int? timeout = null,
132 bool? logOutput = null,
133 bool? logInvocation = null,
134 Action<OutputType, string>? logger = null,
135 Action<IProcess>? exitHandler = null,
136
137 // Extension
138 Action<StreamWriter>? input = null,
139 Encoding? standardOutputEncoding = null,
140 Encoding? standardInputEncoding = null
141 ) => Target.ExecuteWith(
142 PropagateArguments | new ToolExArguments(
143 new(
144 arguments.ToStringAndClear(),
145 workingDirectory,
146 environmentVariables,
147 timeout,
148 logOutput,
149 logInvocation,
150 logger,
151 exitHandler
152 ),
153 input,
154 standardOutputEncoding,
155 standardInputEncoding
156 )
157 );
158}
159
160internal class ToolExExecutor
161{
162 private static readonly char[] s_pathSeparators = { EnvironmentInfo.IsWin ? ';' : ':' };
163 private static readonly object s_lock = new();
164
165 private readonly string _toolPath;
166
167 public ToolExExecutor(string toolPath)
168 {
169 _toolPath = toolPath;
170 }
171
172 public IReadOnlyCollection<Output>? Execute(
173 // Nuke Tool
174 ArgumentStringHandler arguments = default,
175 string? workingDirectory = null,
176 IReadOnlyDictionary<string, string>? environmentVariables = null,
177 int? timeout = null,
178 bool? logOutput = null,
179 bool? logInvocation = null,
180 Action<OutputType, string>? logger = null,
181 Action<IProcess>? exitHandler = null,
182
183 // Extension
184 Action<StreamWriter>? input = null,
185 Encoding? standardOutputEncoding = null,
186 Encoding? standardInputEncoding = null
187 )
188 {
189 workingDirectory ??= EnvironmentInfo.WorkingDirectory;
190 logInvocation ??= true;
191 logOutput ??= true;
192 logger ??= ProcessTasks.DefaultLogger;
193 standardOutputEncoding ??= Encoding.UTF8;
194 standardInputEncoding ??= new UTF8Encoding(false);
195 var outputFilter = arguments.GetFilter();
196
197 var toolPath = _toolPath;
198 var args = arguments.ToStringAndClear();
199
200 if (!Path.IsPathRooted(_toolPath) && !_toolPath.Contains(Path.DirectorySeparatorChar))
201 toolPath = ToolPathResolver.GetPathExecutable(_toolPath);
202
203 var toolPathOverride = GetToolPathOverride(toolPath);
204 if (!string.IsNullOrEmpty(toolPathOverride))
205 {
206 args = $"{toolPath.DoubleQuoteIfNeeded()} {args}".TrimEnd();
207 toolPath = toolPathOverride;
208 }
209
210 Assert.FileExists(toolPath);
211 Assert.DirectoryExists(workingDirectory);
212
213 var startInfo = new ProcessStartInfo
214 {
215 FileName = toolPath,
216 Arguments = args,
217 WorkingDirectory = workingDirectory,
218 RedirectStandardOutput = true,
219 RedirectStandardError = true,
220 RedirectStandardInput = input != null,
221 UseShellExecute = false,
222 StandardErrorEncoding = standardOutputEncoding,
223 StandardOutputEncoding = standardOutputEncoding,
224 };
225 if (input != null)
226 startInfo.StandardInputEncoding = standardInputEncoding;
227
228 if (environmentVariables != null)
229 {
230 startInfo.Environment.Clear();
231 foreach (var (key, value) in environmentVariables)
232 startInfo.Environment[key] = value;
233 }
234
235 if (logInvocation.Value)
236 LogInvocation(startInfo, outputFilter);
237
238 var process = Process.Start(startInfo);
239 if (process == null)
240 return null;
241
242 input?.Invoke(process.StandardInput);
243
244 var output = GetOutputCollection(process, logger, outputFilter);
245 var proc2 = new Process2(process, outputFilter, timeout, output);
246
247 (exitHandler ?? (p => p.AssertZeroExitCode())).Invoke(proc2.AssertWaitForExit());
248 return proc2.Output;
249 }
250
251 private static string? GetToolPathOverride(string toolPath)
252 {
253 if (toolPath.EndsWithOrdinalIgnoreCase(".dll"))
254 {
255 return ToolPathResolver.TryGetEnvironmentExecutable("DOTNET_EXE") ??
256 ToolPathResolver.GetPathExecutable("dotnet");
257 }
258
259 if (EnvironmentInfo.IsUnix &&
260 toolPath.EndsWithOrdinalIgnoreCase(".exe") &&
261 !EnvironmentInfo.IsWsl)
262 return ToolPathResolver.GetPathExecutable("mono");
263
264 return null;
265 }
266
267 private static BlockingCollection<Output> GetOutputCollection(
268 Process process,
269 Action<OutputType, string>? logger,
270 Func<string, string> outputFilter)
271 {
272 var output = new BlockingCollection<Output>();
273
274 process.OutputDataReceived += (_, e) =>
275 {
276 if (e.Data == null)
277 return;
278
279 var filteredOutput = outputFilter(e.Data);
280 output.Add(new Output { Text = filteredOutput, Type = OutputType.Std });
281 logger?.Invoke(OutputType.Std, filteredOutput);
282 };
283 process.ErrorDataReceived += (_, e) =>
284 {
285 if (e.Data == null)
286 return;
287
288 var filteredOutput = outputFilter(e.Data);
289 output.Add(new Output { Text = filteredOutput, Type = OutputType.Err });
290 logger?.Invoke(OutputType.Err, filteredOutput);
291 };
292
293 process.BeginOutputReadLine();
294 process.BeginErrorReadLine();
295
296 return output;
297 }
298
299 private static void LogInvocation(ProcessStartInfo startInfo, Func<string, string> outputFilter)
300 {
301 lock (s_lock)
302 {
303 Log.Information("> {ToolPath} {Arguments}", startInfo.FileName.DoubleQuoteIfNeeded(), outputFilter(startInfo.Arguments));
304 Log.Write(
305 startInfo.WorkingDirectory != EnvironmentInfo.WorkingDirectory
306 ? LogEventLevel.Information
307 : LogEventLevel.Verbose,
308 "@ {WorkingDirectory}",
309 startInfo.WorkingDirectory);
310 }
311 }
312}
Extension class for dealing with passing arguments from the user through nuke to a tool.
Definition Arguments.cs:15
record class PropagateToolExExecution(ToolEx Target, ToolExArguments? PropagateArguments=null)
Propagated ToolEx delegate provider for launch parameter composition.
Definition ToolEx.cs:124
delegate? IReadOnlyCollection< Output > ToolEx(ArgumentStringHandler arguments=default, string? workingDirectory=null, IReadOnlyDictionary< string, string >? environmentVariables=null, int? timeout=null, bool? logOutput=null, bool? logInvocation=null, Action< OutputType, string >? logger=null, Action< IProcess >? exitHandler=null, Action< StreamWriter >? input=null, Encoding? standardOutputEncoding=null, Encoding? standardInputEncoding=null)
Extended copy of Tool delegate of Nuke.
record class ToolArguments(string? Arguments=null, string? WorkingDirectory=null, IReadOnlyDictionary< string, string >? EnvironmentVariables=null, int? Timeout=null, bool? LogOutput=null, bool? LogInvocation=null, Action< OutputType, string >? Logger=null, Action< IProcess >? ExitHandler=null)
A record listing Tool delegate parameters and provides a way to meaningfully merge multiple together.
record class ToolExArguments(ToolArguments ToolArgs, Action< StreamWriter >? Input=null, Encoding? StandardOutputEncoding=null, Encoding? StandardInputEncoding=null)
A record listing Tool and ToolEx delegate parameters and provides a way to meaningfully merge multipl...
Definition ToolEx.cs:39