MCRO
C++23 utilities for Unreal Engine.
Loading...
Searching...
No Matches
AbsolutePath.Build.cs
Go to the documentation of this file.
1/** @noop License Comment
2 * @file
3 * @copyright
4 * This Source Code is subject to the terms of the Mozilla Public License, v2.0.
5 * If a copy of the MPL was not distributed with this file You can obtain one at
6 * https://mozilla.org/MPL/2.0/
7 *
8 * @author David Mórász
9 * @date 2025
10 */
11
12/**
13 * @file
14 * An attempt to bring the expressiveness of NUKE's AbsolutePath into Unreal module rules and targets.
15 */
16
17using System;
18using System.Linq;
19using System.IO;
20using System.Collections.Generic;
21using EpicGames.Core;
22
23namespace McroBuild;
24
25/// <summary>
26/// A simplified copy of NUKE's own AbsolutePath class
27/// https://github.com/nuke-build/nuke/blob/develop/source/Nuke.Utilities/IO/AbsolutePath.cs
28/// </summary>
29public class AbsolutePath: IFormattable
30{
31 private readonly string _path;
32
33 private AbsolutePath(string path)
34 {
35 _path = PathUtils.NormalizePath(path);
36 }
37
38 /// <summary>
39 /// Create an AbsolutePath from a string.
40 /// </summary>
41 /// <param name="path">Input path must be rooted.</param>
42 public static AbsolutePath Create(string path)
43 {
44 return new AbsolutePath(path);
45 }
46
47 /// <summary>
48 /// Convert a string to an AbsolutePath.
49 /// </summary>
50 /// <param name="path">Input path must be rooted.</param>
51 public static implicit operator AbsolutePath(string path)
52 {
53 if (path is null)
54 return null;
55 if (!PathUtils.HasPathRoot(path)) throw new Exception($"Path '{path}' must be rooted");
56 return new AbsolutePath(path);
57 }
58
59 /// <summary>
60 /// Use AbsolutePath where an API expects a string representing a path.
61 /// </summary>
62 public static implicit operator string(AbsolutePath path)
63 {
64 return path?.ToString();
65 }
66
67 /// <summary>
68 /// Get the filename (with extension) or the directory name
69 /// </summary>
70 public string Name => Path.GetFileName(_path);
71
72 /// <summary>
73 /// Get the filename (without extension)
74 /// </summary>
75 public string NameWithoutExtension => Path.GetFileNameWithoutExtension(_path);
76
77 /// <summary>
78 /// Get the extension of the filename
79 /// </summary>
80 public string Extension => Path.GetExtension(_path);
81
82 /// <summary>
83 /// The ancestor of this path (..)
84 /// </summary>
87 ? this / ".."
88 : null;
89
90 /// <summary>
91 /// Use completely valid C# syntax `MyPath/ ..` to access ancestor
92 /// </summary>
93 public static AbsolutePath operator / (AbsolutePath left, Range range)
94 {
95 return left.Parent;
96 }
97
98 /// <summary>
99 /// Append a path segment to this AbsolutePath
100 /// </summary>
101 public static AbsolutePath operator / (AbsolutePath left, string right)
102 {
103 return new AbsolutePath(PathUtils.Combine(left!, right));
104 }
105
106 /// <summary>
107 /// Append a piece of string to this AbsolutePath without any intersperse.
108 /// </summary>
109 public static AbsolutePath operator + (AbsolutePath left, string right)
110 {
111 return new AbsolutePath(left.ToString() + right);
112 }
113
114 protected bool Equals(AbsolutePath other)
115 {
116 var stringComparison = PathUtils.HasWinRoot(_path) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
117 return string.Equals(_path, other._path, stringComparison);
118 }
119
120 public static bool operator == (AbsolutePath a, AbsolutePath b)
121 {
122 return a!.Equals(b);
123 }
124
125 public static bool operator !=(AbsolutePath a, AbsolutePath b)
126 {
127 return !a!.Equals(b);
128 }
129
130 public override bool Equals(object obj)
131 {
132 if (ReferenceEquals(objA: null, obj))
133 return false;
134 if (ReferenceEquals(this, obj))
135 return true;
136 if (obj.GetType() != GetType())
137 return false;
138 return Equals((AbsolutePath) obj);
139 }
140
141 public override int GetHashCode()
142 {
143 return _path?.GetHashCode() ?? 0;
144 }
145
146 public override string ToString()
147 {
148 return ((IFormattable)this).ToString(format: null, formatProvider: null);
149 }
150
151 public const string DoubleQuote = "dq";
152 public const string SingleQuote = "sq";
153 public const string NoQuotes = "nq";
154
155 string IFormattable.ToString(string format, IFormatProvider formatProvider)
156 {
157 var path = _path.WithUnixSeparator();
158 return format switch
159 {
160 DoubleQuote => $"\"{path}\"",
161 SingleQuote => $"'{path}'",
162 null or NoQuotes => path,
163 _ => throw new ArgumentException($"Format '{format}' is not recognized")
164 };
165 }
166}
167
168/// <summary>
169/// The API NUKE has for AbsolutePath relies heavily on extension methods. In fact if file system operations are
170/// expressed with extension methods to AbsolutePath it can yield code which is much more comfortable to write.
171/// </summary>
172/// <remarks>
173/// Most generic path operation functions are taken from [NUKE](https://nuke.build)
174/// </remarks>
175public static partial class AbsolutePathExtensions
176{
177 public static AbsolutePath GetRoot(this AbsolutePath self) => Path.GetPathRoot(self);
178 public static bool IsRoot(this AbsolutePath self) => self.GetRoot() == self;
179
180 /// <summary>
181 /// Return a path connecting from right side as a base to the left side as target
182 /// </summary>
183 public static string RelativeToBase(this AbsolutePath self, AbsolutePath root) =>
184 Path.GetRelativePath(root, self).WithUnixSeparator();
185
186 /// <summary>
187 /// Return a path connecting from left side as a base to the right side as target
188 /// </summary>
189 public static string BaseRelativeTo(this AbsolutePath self, AbsolutePath subfolder) =>
190 Path.GetRelativePath(self, subfolder).WithUnixSeparator();
191
192 public static bool FileExists(this AbsolutePath path) => File.Exists(path);
193 public static bool DirectoryExists(this AbsolutePath path) => Directory.Exists(path);
194
195 /// <summary>
196 /// Returns null if designated file doesn't exist. This can be used in null-propagating expressions without the
197 /// need for if statements, like `InPath.ExistingFile()?.Parent ?? "/default/path".AsPath()`.
198 /// </summary>
199 /// <param name="path"></param>
200 /// <returns></returns>
201 public static AbsolutePath ExistingFile(this AbsolutePath path) => path.FileExists() ? path : null;
202
203 /// <summary>
204 /// Returns null if designated directory doesn't exist. This can be used in null-propagating expressions without the
205 /// need for if statements, like `InPath.ExistingDirectory()?.Parent ?? "/default/path".AsPath()`.
206 /// </summary>
207 /// <param name="path"></param>
208 /// <returns></returns>
209 public static AbsolutePath ExistingDirectory(this AbsolutePath path) => path.DirectoryExists() ? path : null;
210
211 /// <summary>
212 /// Return all files in a directory.
213 /// </summary>
214 /// <param name="path"></param>
215 /// <param name="pattern">Filter files with a globbing pattern</param>
216 /// <param name="searchOption">Indicate if files should be taken only from current folder, or any subfolders too</param>
217 public static IEnumerable<AbsolutePath> Files(this AbsolutePath path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
218 => Directory.EnumerateFiles(path, pattern, searchOption)
219 .Select(p => p.AsPath());
220
221 /// <summary>
222 /// Return all subfolders in a directory.
223 /// </summary>
224 /// <param name="path"></param>
225 /// <param name="pattern">Filter folders with a globbing pattern</param>
226 /// <param name="searchOption">Indicate if folders should be taken only from current folder, or any subfolders too</param>
227 public static IEnumerable<AbsolutePath> Directories(this AbsolutePath path, string pattern = "*", SearchOption searchOption = SearchOption.TopDirectoryOnly)
228 => Directory.EnumerateDirectories(path, pattern, searchOption)
229 .Select(p => p.AsPath());
230
231 /// <summary>
232 /// Returns true if path ends in any of the given extensions. Input extensions should have leading `.`
233 /// </summary>
234 public static bool HasExtension(this AbsolutePath path, string extension, params string[] alternativeExtensions)
235 => alternativeExtensions.Append(extension)
236 .Any(e => path.Extension.Equals(e, StringComparison.InvariantCultureIgnoreCase));
237
238 /// <summary>
239 /// Replace extension of left-side with~ or add given extension
240 /// </summary>
241 public static AbsolutePath WithExtension(this AbsolutePath path, string extension)
242 => path.Parent / Path.ChangeExtension(path.Name, extension);
243
244 /// <summary>
245 /// Creates a new directory or does nothing if that's already exists
246 /// </summary>
247 /// <returns>Path to created directory (same as left-side)</returns>
249 {
250 if (!path.DirectoryExists())
251 Directory.CreateDirectory(path);
252 return path;
253 }
254
255 private static List<AbsolutePath> FileSystemTask(
256 Action<string, string> task,
257 AbsolutePath path,
258 AbsolutePath to,
259 string pattern = "*"
260 ) {
261 var output = new List<AbsolutePath>();
262 if (path.FileExists())
263 {
264 to.Parent.CreateDirectory();
265 task(path, to);
266 output.Add(to);
267 }
268 else if (path.DirectoryExists())
269 {
270 foreach (var file in path.Files(pattern, SearchOption.AllDirectories))
271 {
272 var dst = to / file.RelativeToBase(path);
273 output.AddRange(FileSystemTask(task, file, dst));
274 }
275 }
276 return output;
277 }
278
279 /// <summary>
280 /// Copy a file or a directory recursively to be the target path.
281 /// </summary>
282 /// <param name="path"></param>
283 /// <param name="to"></param>
284 /// <param name="pattern">Filter pattern for files when copying recursively</param>
285 /// <returns>The list of new files which has been copied</returns>
286 public static List<AbsolutePath> Copy(this AbsolutePath path, AbsolutePath to, string pattern = "*")
287 => FileSystemTask(
288 (src, dst) => File.Copy(src, dst, true),
289 path, to, pattern
290 );
291
292 /// <summary>
293 /// Copy a file or a directory recursively into a target folder.
294 /// </summary>
295 /// <param name="path"></param>
296 /// <param name="intoDirectory"></param>
297 /// <param name="pattern">Filter pattern for files when copying recursively</param>
298 /// <returns>The list of new files which has been copied</returns>
299 public static List<AbsolutePath> CopyInto(this AbsolutePath path, AbsolutePath intoDirectory, string pattern = "*")
300 => path.Copy(intoDirectory / path.Name, pattern);
301
302 /// <summary>
303 /// Move a file or a directory recursively to be the target path.
304 /// </summary>
305 /// <param name="path"></param>
306 /// <param name="to"></param>
307 /// <param name="pattern">Filter pattern for files when moving recursively</param>
308 /// <returns>The list of moved files in their new place</returns>
309 public static List<AbsolutePath> Move(this AbsolutePath path, AbsolutePath to, string pattern = "*")
310 => FileSystemTask(
311 (src, dst) => File.Move(src, dst, true),
312 path, to, pattern
313 );
314
315 /// <summary>
316 /// Move a file or a directory recursively into a target folder.
317 /// </summary>
318 /// <param name="path"></param>
319 /// <param name="intoDirectory"></param>
320 /// <param name="pattern">Filter pattern for files when moving recursively</param>
321 /// <returns>The list of moved files in their new place</returns>
322 public static List<AbsolutePath> MoveInto(this AbsolutePath path, AbsolutePath intoDirectory, string pattern = "*")
323 => path.Move(intoDirectory / path.Name, pattern);
324}
325
326/// <summary>
327/// Support utilities for AbsolutePath
328/// </summary>
329public static class PathUtils
330{
331 /// <summary>
332 /// Convert a left-side string to an AbsolutePath. In fact this is the recommended way to create an instance of
333 /// AbsolutePath (as its constructor is private)
334 /// </summary>
335 /// <param name="input">Input must be rooted</param>
336 public static AbsolutePath AsPath(this string input) => AbsolutePath.Create(input);
337
338 /// <summary>
339 /// Convert a left-side FileReference to an AbsolutePath. In fact this is the recommended way to create an instance
340 /// of AbsolutePath (as its constructor is private)
341 /// </summary>
342 /// <param name="input">Input must be rooted</param>
343 public static AbsolutePath AsPath(this FileReference input) => AbsolutePath.Create(input.FullName);
344
345 /// <summary>
346 /// Convert a left-side DirectoryReference to an AbsolutePath. In fact this is the recommended way to create an
347 /// instance of AbsolutePath (as its constructor is private)
348 /// </summary>
349 /// <param name="input">Input must be rooted</param>
350 public static AbsolutePath AsPath(this DirectoryReference input) => AbsolutePath.Create(input.FullName);
351
352 internal const char WinSeparator = '\\';
353 internal const char UncSeparator = '\\';
354 internal const char UnixSeparator = '/';
355
356 internal static readonly char[] AllSeparators = new [] { WinSeparator, UncSeparator, UnixSeparator };
357
358 public static string WithUnixSeparator(this string input) => input.Replace(WinSeparator, UnixSeparator);
359
360 private static bool IsSameDirectory(string pathPart)
361 => pathPart?.Length == 1 &&
362 pathPart[index: 0] == '.';
363
364 private static bool IsUpwardsDirectory(string pathPart)
365 => pathPart?.Length == 2 &&
366 pathPart[index: 0] == '.' &&
367 pathPart[index: 1] == '.';
368
369 internal static bool IsWinRoot(string root)
370 => root?.Length == 2 &&
371 char.IsLetter(root[index: 0]) &&
372 root[index: 1] == ':';
373
374 internal static bool IsUnixRoot(string root)
375 => root?.Length == 1 &&
376 root[index: 0] == UnixSeparator;
377
378 internal static bool IsUncRoot(string root)
379 => root?.Length >= 3 &&
380 root[index: 0] == UncSeparator &&
381 root[index: 1] == UncSeparator &&
382 root.Skip(count: 2).All(char.IsLetterOrDigit);
383
384 private static string GetHeadPart(string str, int count) => new((str ?? string.Empty).Take(count).ToArray());
385
386 internal static bool HasUnixRoot(string path) => IsUnixRoot(GetHeadPart(path, count: 1));
387 internal static bool HasUncRoot(string path) => IsUncRoot(GetHeadPart(path, count: 3));
388 internal static bool HasWinRoot(string path) => IsWinRoot(GetHeadPart(path, count: 2));
389
390 public static string GetPathRoot( string path)
391 {
392 if (path == null)
393 return null;
394
395 if (HasUnixRoot(path))
396 return GetHeadPart(path, count: 1);
397
398 if (HasWinRoot(path))
399 return GetHeadPart(path, count: 2);
400
401 if (HasUncRoot(path))
402 {
403 var separatorIndex = path.IndexOf(UncSeparator, startIndex: 2);
404 return separatorIndex == -1 ? path : GetHeadPart(path, separatorIndex);
405 }
406
407 return null;
408 }
409
410 public static bool HasPathRoot(string path) => GetPathRoot(path) != null;
411
412 private static char GetSeparator(string path)
413 {
414 var root = GetPathRoot(path);
415 if (root != null)
416 {
417 if (IsWinRoot(root))
418 return WinSeparator;
419
420 if (IsUncRoot(root))
421 return UncSeparator;
422
423 if (IsUnixRoot(root))
424 return UnixSeparator;
425 }
426
427 return Path.DirectorySeparatorChar;
428 }
429
430 private static string Trim(string path)
431 {
432 if (path == null)
433 return null;
434
435 return IsUnixRoot(path) // TODO: "//" ?
436 ? path
437 : path.TrimEnd(AllSeparators);
438 }
439
440 public static string Combine(string left, string right, char? separator = null)
441 {
442 left = Trim(left);
443 right = Trim(right);
444
445 if (string.IsNullOrWhiteSpace(left))
446 return right;
447 if (string.IsNullOrWhiteSpace(right))
448 return !IsWinRoot(left) ? left : $@"{left}\";
449
450 separator ??= GetSeparator(left);
451
452 if (IsWinRoot(left))
453 return $@"{left}\{right}";
454 if (IsUnixRoot(left))
455 return $"{left}{right}";
456 if (IsUncRoot(left))
457 return $@"{left}\{right}";
458
459 return $"{left}{separator}{right}";
460 }
461
462 public static string NormalizePath(string path, char? separator = null)
463 {
464 path ??= string.Empty;
465 separator ??= GetSeparator(path);
466 var root = GetPathRoot(path);
467
468 var tail = root == null ? path : path.Substring(root.Length);
469 var tailParts = tail.Split(AllSeparators, StringSplitOptions.RemoveEmptyEntries).ToList();
470 for (var i = 0; i < tailParts.Count;)
471 {
472 var part = tailParts[i];
473 if (IsUpwardsDirectory(part))
474 {
475 if (tailParts.Take(i).All(IsUpwardsDirectory))
476 {
477 i++;
478 continue;
479 }
480
481 tailParts.RemoveAt(i);
482 tailParts.RemoveAt(i - 1);
483 i--;
484 continue;
485 }
486
487 if (IsSameDirectory(part))
488 {
489 tailParts.RemoveAt(i);
490 continue;
491 }
492
493 i++;
494 }
495
496 return Combine(root, string.Join(separator.Value, tailParts), separator);
497 }
498}
The API NUKE has for AbsolutePath relies heavily on extension methods. In fact if file system operati...
static AbsolutePath WithExtension(this AbsolutePath path, string extension)
Replace extension of left-side with~ or add given extension
static List< AbsolutePath > CopyInto(this AbsolutePath path, AbsolutePath intoDirectory, string pattern="*")
Copy a file or a directory recursively into a target folder.
static List< AbsolutePath > MoveInto(this AbsolutePath path, AbsolutePath intoDirectory, string pattern="*")
Move a file or a directory recursively into a target folder.
static List< AbsolutePath > Move(this AbsolutePath path, AbsolutePath to, string pattern="*")
Move a file or a directory recursively to be the target path.
static string BaseRelativeTo(this AbsolutePath self, AbsolutePath subfolder)
Return a path connecting from left side as a base to the right side as target.
static AbsolutePath ExistingFile(this AbsolutePath path)
Returns null if designated file doesn't exist. This can be used in null-propagating expressions witho...
static IEnumerable< AbsolutePath > Directories(this AbsolutePath path, string pattern="*", SearchOption searchOption=SearchOption.TopDirectoryOnly)
Return all subfolders in a directory.
static AbsolutePath GetRoot(this AbsolutePath self)
static bool IsRoot(this AbsolutePath self)
static AbsolutePath ExistingDirectory(this AbsolutePath path)
Returns null if designated directory doesn't exist. This can be used in null-propagating expressions ...
static bool HasExtension(this AbsolutePath path, string extension, params string[] alternativeExtensions)
Returns true if path ends in any of the given extensions. Input extensions should have leading .
static bool DirectoryExists(this AbsolutePath path)
static List< AbsolutePath > Copy(this AbsolutePath path, AbsolutePath to, string pattern="*")
Copy a file or a directory recursively to be the target path.
static string RelativeToBase(this AbsolutePath self, AbsolutePath root)
Return a path connecting from right side as a base to the left side as target.
static bool FileExists(this AbsolutePath path)
static AbsolutePath CreateDirectory(this AbsolutePath path)
Creates a new directory or does nothing if that's already exists.
static IEnumerable< AbsolutePath > Files(this AbsolutePath path, string pattern="*", SearchOption searchOption=SearchOption.TopDirectoryOnly)
Return all files in a directory.
A simplified copy of NUKE's own AbsolutePath class https://github.com/nuke-build/nuke/blob/develop/so...
AbsolutePath Parent
The ancestor of this path (..)
string Name
Get the filename (with extension) or the directory name.
static AbsolutePath operator+(AbsolutePath left, string right)
Append a piece of string to this AbsolutePath without any intersperse.
bool Equals(AbsolutePath other)
override bool Equals(object obj)
string NameWithoutExtension
Get the filename (without extension)
static bool operator==(AbsolutePath a, AbsolutePath b)
static AbsolutePath Create(string path)
Create an AbsolutePath from a string.
string Extension
Get the extension of the filename.
static bool operator!=(AbsolutePath a, AbsolutePath b)
static AbsolutePath operator/(AbsolutePath left, Range range)
Use completely valid C# syntax MyPath/ .. to access ancestor.
Support utilities for AbsolutePath.
static bool HasWinRoot(string path)
static readonly char[] AllSeparators
static bool IsUncRoot(string root)
static AbsolutePath AsPath(this FileReference input)
Convert a left-side FileReference to an AbsolutePath. In fact this is the recommended way to create a...
static bool HasUnixRoot(string path)
static AbsolutePath AsPath(this string input)
Convert a left-side string to an AbsolutePath. In fact this is the recommended way to create an insta...
static string GetPathRoot(string path)
static AbsolutePath AsPath(this DirectoryReference input)
Convert a left-side DirectoryReference to an AbsolutePath. In fact this is the recommended way to cre...
static bool HasUncRoot(string path)
static bool HasPathRoot(string path)
static string NormalizePath(string path, char? separator=null)
static string WithUnixSeparator(this string input)
static bool IsWinRoot(string root)
static string Combine(string left, string right, char? separator=null)
static bool IsUnixRoot(string root)