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 ArgumentStringHandlerEx 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 ArgumentStringHandlerEx 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 object _lock = new();
163
164 private readonly string _toolPath;
165
166 public ToolExExecutor(string toolPath)
167 {
168 _toolPath = toolPath;
169 }
170
171 public IReadOnlyCollection<Output>? Execute(
172 // Nuke Tool
173 ArgumentStringHandlerEx arguments = default,
174 string? workingDirectory = null,
175 IReadOnlyDictionary<string, string>? environmentVariables = null,
176 int? timeout = null,
177 bool? logOutput = null,
178 bool? logInvocation = null,
179 Action<OutputType, string>? logger = null,
180 Action<IProcess>? exitHandler = null,
181
182 // Extension
183 Action<StreamWriter>? input = null,
184 Encoding? standardOutputEncoding = null,
185 Encoding? standardInputEncoding = null
186 )
187 {
188 workingDirectory ??= EnvironmentInfo.WorkingDirectory;
189 logInvocation ??= true;
190 logOutput ??= true;
191 logger ??= ProcessTasks.DefaultLogger;
192 standardOutputEncoding ??= Encoding.UTF8;
193 standardInputEncoding ??= new UTF8Encoding(false);
194 var outputFilter = arguments.GetFilter();
195
196 var toolPath = _toolPath;
197 var args = arguments.ToStringAndClear();
198
199 if (!Path.IsPathRooted(_toolPath) && !_toolPath.Contains(Path.DirectorySeparatorChar))
200 toolPath = ToolPathResolver.GetPathExecutable(_toolPath);
201
202 var toolPathOverride = GetToolPathOverride(toolPath);
203 if (!string.IsNullOrEmpty(toolPathOverride))
204 {
205 args = $"{toolPath.DoubleQuoteIfNeeded()} {args}".TrimEnd();
206 toolPath = toolPathOverride;
207 }
208
209 Assert.FileExists(toolPath);
210 Assert.DirectoryExists(workingDirectory);
211
212 var startInfo = new ProcessStartInfo
213 {
214 FileName = toolPath,
215 Arguments = args,
216 WorkingDirectory = workingDirectory,
217 RedirectStandardOutput = true,
218 RedirectStandardError = true,
219 RedirectStandardInput = input != null,
220 UseShellExecute = false,
221 StandardErrorEncoding = standardOutputEncoding,
222 StandardOutputEncoding = standardOutputEncoding,
223 };
224 if (input != null)
225 startInfo.StandardInputEncoding = standardInputEncoding;
226
227 if (environmentVariables != null)
228 {
229 startInfo.Environment.Clear();
230 foreach (var (key, value) in environmentVariables)
231 startInfo.Environment[key] = value;
232 }
233
234 if (logInvocation.Value)
235 LogInvocation(startInfo, outputFilter);
236
237 var process = Process.Start(startInfo);
238 if (process == null)
239 return null;
240
241 input?.Invoke(process.StandardInput);
242
243 var output = GetOutputCollection(process, logger, outputFilter);
244 var proc2 = new Process2(process, outputFilter, timeout, output);
245
246 (exitHandler ?? (p => p.AssertZeroExitCode())).Invoke(proc2.AssertWaitForExit());
247 return proc2.Output;
248 }
249
250 private static string? GetToolPathOverride(string toolPath)
251 {
252 if (toolPath.EndsWithOrdinalIgnoreCase(".dll"))
253 {
254 return ToolPathResolver.TryGetEnvironmentExecutable("DOTNET_EXE") ??
255 ToolPathResolver.GetPathExecutable("dotnet");
256 }
257
258 if (EnvironmentInfo.IsUnix &&
259 toolPath.EndsWithOrdinalIgnoreCase(".exe") &&
260 !EnvironmentInfo.IsWsl)
261 return ToolPathResolver.GetPathExecutable("mono");
262
263 return null;
264 }
265
266 private static BlockingCollection<Output> GetOutputCollection(
267 Process process,
268 Action<OutputType, string>? logger,
269 Func<string, string> outputFilter)
270 {
271 var output = new BlockingCollection<Output>();
272
273 process.OutputDataReceived += (_, e) =>
274 {
275 if (e.Data == null)
276 return;
277
278 var filteredOutput = outputFilter(e.Data);
279 output.Add(new Output { Text = filteredOutput, Type = OutputType.Std });
280 logger?.Invoke(OutputType.Std, filteredOutput);
281 };
282 process.ErrorDataReceived += (_, e) =>
283 {
284 if (e.Data == null)
285 return;
286
287 var filteredOutput = outputFilter(e.Data);
288 output.Add(new Output { Text = filteredOutput, Type = OutputType.Err });
289 logger?.Invoke(OutputType.Err, filteredOutput);
290 };
291
292 process.BeginOutputReadLine();
293 process.BeginErrorReadLine();
294
295 return output;
296 }
297
298 private static void LogInvocation(ProcessStartInfo startInfo, Func<string, string> outputFilter)
299 {
300 lock (_lock)
301 {
302 Log.Information("> {ToolPath} {Arguments}", startInfo.FileName.DoubleQuoteIfNeeded(), outputFilter(startInfo.Arguments));
303 Log.Write(
304 startInfo.WorkingDirectory != EnvironmentInfo.WorkingDirectory
305 ? LogEventLevel.Information
306 : LogEventLevel.Verbose,
307 "@ {WorkingDirectory}",
308 startInfo.WorkingDirectory);
309 }
310 }
311}
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
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.
delegate? IReadOnlyCollection< Output > ToolEx(ArgumentStringHandlerEx 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 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