Nuke.Cola
Loading...
Searching...
No Matches
FolderComposition.cs
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Threading.Tasks;
6using GlobExpressions;
8using Nuke.Common;
9using Nuke.Common.IO;
10using Nuke.Common.Utilities;
11using Nuke.Common.Utilities.Collections;
12using Nuke.Utilities.Text.Yaml;
13using Serilog;
14
16
17/// <summary>
18/// A record for suffix replacement mapping
19/// </summary>
20/// <param name="To">The target suffix desired by the importing project</param>
21/// <param name="From">The original suffix inside the source folder</param>
22/// <returns></returns>
23public record class ImportFolderSuffixes(string To, string From = "Origin")
24{
25 public static implicit operator ImportFolderSuffixes (string from)
26 => new(from);
27
28 public static implicit operator ImportFolderSuffixes ((string to, string from) from)
29 => new(from.to, from.from);
30
31 public static implicit operator (string, string) (ImportFolderSuffixes from)
32 => (from.To, from.From);
33}
34
35/// <summary>
36/// A record importing a folder from a source to a parent folder
37/// </summary>
38/// <param name="From">The source or origin folder</param>
39/// <param name="ToParent">The destination parent folder</param>
40/// <param name="ManifestFilePattern">
41/// An explicit glob for finding export manifest files in the wprking~ and its subdirectories
42/// </param>
43/// <param name="Manifest">
44/// Control how the contents of the folder should be imported into target project. If null and if
45/// an export.yml manifest file exists in the imported folder, that manifest file will be used.
46/// If both this parameter is null and an export.yml doesn't exist, then the folder will be simply
47/// symlinked.
48/// </param>
49/// <returns></returns>
50public record class ImportFolderItem(AbsolutePath From, AbsolutePath ToParent, ExportManifest? Manifest = null, string ManifestFilePattern = "export.y*ml")
51{
52 public static implicit operator ImportFolderItem ((AbsolutePath from, AbsolutePath toParent) from)
53 => new(from.from, from.toParent);
54
55 public static implicit operator ImportFolderItem ((AbsolutePath from, AbsolutePath toParent, ExportManifest manifest) from)
56 => new(from.from, from.toParent, from.manifest);
57
58 public static implicit operator ImportFolderItem ((AbsolutePath from, AbsolutePath toParent, string manifestFilePattern) from)
59 => new(from.from, from.toParent, ManifestFilePattern: from.manifestFilePattern);
60
61 public static implicit operator ImportFolderItem ((AbsolutePath from, AbsolutePath toParent, ExportManifest manifest, string manifestFilePattern) from)
62 => new(from.from, from.toParent, from.manifest, from.manifestFilePattern);
63
64 public static implicit operator (AbsolutePath, AbsolutePath) (ImportFolderItem from)
65 => (from.From, from.ToParent);
66
67 public static implicit operator (AbsolutePath, AbsolutePath, ExportManifest?) (ImportFolderItem from)
68 => (from.From, from.ToParent, from.Manifest);
69
70 public static implicit operator (AbsolutePath, AbsolutePath, ExportManifest?, string) (ImportFolderItem from)
71 => (from.From, from.ToParent, from.Manifest, from.ManifestFilePattern);
72}
73
74/// <summary>
75/// Further options for ImportFolder
76/// </summary>
77/// <param name="UseSubfolder">
78/// When and by default true, a subfolder is created for the import, or when false, the folder
79/// is composited with the given target folder directly.
80/// </param>
81/// <param name="Pretend">
82/// When true, file system actions are not invoked. This is useful for querying which files/folders
83/// would be handled by this operation. Logging is also disabled while pretending
84/// </param>
85/// <param name="CopyByDefault">
86/// The default behavior is to link unassumed folder when importing it. CopyByDefault will make
87/// unassuming folders to be copied instead.
88/// </param>
89/// <param name="ForceCopyLinks">
90/// Force copy folders which are listed only for linking. Only use this when it's absolutely
91/// necessary. This will also imply CopyByDefault.
92/// </param>
93/// <param name="ImplicitImport">
94/// Only used by
95/// </param>
96/// <param name="Suffixes">
97/// Can be the desired project suffix, <see cref="ImportFolderSuffixes"/> for more details
98/// </param>
99/// <param name="AddToMain">
100/// Merge these export manifests into the one only mentioned by explicit arguments.
101/// </param>
102/// <param name="AddToAll">
103/// Merge these export manifests into all export manifests found by current import
104/// (through Use items for example).
105/// </param>
106public record class ImportOptions(
107 bool UseSubfolder = true,
108 bool Pretend = false,
109 bool CopyByDefault = false,
110 bool ForceCopyLinks = false,
111 bool ImplicitImport = false,
112 ImportFolderSuffixes? Suffixes = null,
113 IEnumerable<ExportManifest>? AddToMain = null,
114 IEnumerable<ExportManifest>? AddToAll = null
115);
116
117public enum ImportMethod
118{
119 Copy,
120 Link
121}
122
123public record class ImportedItem(AbsolutePath From, AbsolutePath To, ImportMethod Method);
124
125public static class FolderComposition
126{
127 private static string ProcessSuffix(this string target, ImportFolderSuffixes? suffixes, string leads = "_.:")
128 {
129 if (suffixes == null) return target;
130 string result = target;
131 foreach (char lead in leads)
132 {
133 result = result.Replace(
134 lead + suffixes.From, lead + suffixes.To,
135 true, CultureInfo.InvariantCulture
136 );
137 }
138 return result;
139 }
140
141 private static AbsolutePath ProcessSuffixPath(
142 this AbsolutePath target,
143 ImportFolderSuffixes? suffixes,
144 AbsolutePath? until = null,
145 string leads = "_.:"
146 ) {
147 if (suffixes == null) return target;
148 if (until == null)
149 return target.Parent / target.Name.ProcessSuffix(suffixes, leads);
150 var relative = until.GetRelativePathTo(target).ToString();
151 return until / relative.ProcessSuffix(suffixes, leads);
152 }
153
154 private static void ProcessSuffixContent(this AbsolutePath target, ImportFolderSuffixes? suffixes, string leads = "_.:")
155 {
156 if (suffixes == null) return;
157 if (target.FileExists())
158 {
159 target.WriteAllText(
160 target.ReadAllText().ProcessSuffix(suffixes, leads)
161 );
162 }
163 }
164
165 /// <summary>
166 /// Convenience method for specifying multiple folder from/to pairs for <see cref="ImportFolder"/>
167 /// <code>
168 /// this.ImportFolders(
169 /// , new ImportOptions(Suffixes: "Test")
170 /// , (root / "Unassuming", target)
171 /// , (root / "FolderOnly_Origin", target)
172 /// , (root / "WithManifest" / "Both_Origin", target / "WithManifest")
173 /// , (root / "WithManifest" / "Copy_Origin", target / "WithManifest")
174 /// , (root / "WithManifest" / "Link_Origin", target / "WithManifest")
175 /// , (root / "ScriptControlled", target, new ExportManifest
176 /// {
177 /// Link = {
178 /// new() { Directory = "Private/SharedSubfolder"},
179 /// new() { Directory = "Public/SharedSubfolder"},
180 /// },
181 /// Copy = {
182 /// new() {
183 /// File = "**/*_Origin.*",
184 /// ProcessContent = true
185 /// }
186 /// }
187 /// })
188 /// );
189 /// </code>
190 /// </summary>
191 /// <param name="self">For easier access this is an extension method</param>
192 /// <param name="options">
193 /// When and by default true, a subfolder is created for the import, or when false, the folder
194 /// is composited with the given target folder directly.
195 /// </param>
196 /// <param name="imports">
197 /// The folder import from / to pair. Optionally can specify an export manifest.
198 /// <see cref="ImportFolderItem"/>
199 /// </param>
200 public static List<ImportedItem> ImportFolders(this INukeBuild self, ImportOptions? options, params ImportFolderItem[] imports)
201 => [.. imports.SelectMany(i => ImportFolder(self, i, options))];
202
203 /// <inheritdoc cref="ImportFolders(INukeBuild, ImportOptions?, ImportFolderItem[])"/>
204 public static List<ImportedItem> ImportFolders(this INukeBuild self, params ImportFolderItem[] imports)
205 => [.. imports.SelectMany(i => ImportFolder(self, i))];
206
207 /// <summary>
208 /// There are cases when one project needs to compose from one pre-existing rigid folder
209 /// structure of one dependency to another rigid folder structure of the current project. For
210 /// scenarios like this Nuke.Cola provides this extension method which will copy/link the target
211 /// folder and its contents according to some instructions expressed by either an `export.yml`
212 /// file in the imported folder or provided explicitly from <see cref="ImportFolderItem"/>.
213 /// </summary>
214 /// <param name="self">For easier access this is an extension method</param>
215 /// <param name="import">
216 /// The folder import from / to pair. Optionally can specify an export manifest.
217 /// <see cref="ImportFolderItem"/>
218 /// </param>
219 /// <param name="options">
220 /// When and by default true, a subfolder is created for the import, or when false, the folder
221 /// is composited with the given target folder directly.
222 /// </param>
223 public static List<ImportedItem> ImportFolder(
224 this INukeBuild self,
225 ImportFolderItem import,
226 ImportOptions? options = null
227 ) {
228 options ??= new();
229 var manifestPath = import.From.GetFiles(import.ManifestFilePattern).FirstOrDefault();
230 var to = options.UseSubfolder ? import.ToParent / import.From.Name.ProcessSuffix(options.Suffixes) : import.ToParent;
231 var instructions = import.Manifest ?? manifestPath?.ReadYaml<ExportManifest>();
232 instructions = instructions.Combine(options.AddToAll);
233 if (!options.ImplicitImport)
234 {
235 instructions = instructions.Combine(options.AddToMain);
236 }
237
238 var result = new List<ImportedItem>();
239
240 if (instructions == null)
241 {
242 var copyByDefault = options.CopyByDefault || options.ForceCopyLinks;
243 if (!options.Pretend)
244 {
245 if (copyByDefault)
246 import.From.Copy(to, ExistsPolicy.MergeAndOverwrite);
247 else to.LinksDirectory(import.From);
248 }
249 var importMethod = copyByDefault ? ImportMethod.Copy : ImportMethod.Link;
250 result.Add(new(import.From, to, importMethod));
251 return result;
252 }
253
254 if (!to.DirectoryExists() && !options.Pretend) to.CreateDirectory();
255
256 void FileSystemTask(
257 IEnumerable<FileOrDirectory> list,
258 Func<AbsolutePath, AbsolutePath, FileOrDirectory, ImportedItem> handleDirectories,
259 Func<AbsolutePath, AbsolutePath, FileOrDirectory, ImportedItem> handleFiles
260 ) {
261 foreach (var glob in list)
262 {
263 if (string.IsNullOrWhiteSpace(glob.Directory) && string.IsNullOrWhiteSpace(glob.File))
264 continue;
265
266 var exclude = glob.Not.Concat(instructions.Not);
267
268 if (string.IsNullOrWhiteSpace(glob.File))
269 import.From.SearchDirectories(glob.Directory!)
270 .ForEach((p, i) =>
271 {
272 var dst = glob.GetDestination(import.From, to, p, i, exclude);
273 if (dst == null) return;
274 result.Add(handleDirectories(p, dst.ProcessSuffixPath(options.Suffixes, to), glob));
275 });
276 else
277 import.From.SearchFiles(glob.File!)
278 .ForEach((p, i) =>
279 {
280 var dst = glob.GetDestination(import.From, to, p, i, exclude);
281 if (dst == null) return;
282 result.Add(handleFiles(p, dst.ProcessSuffixPath(options.Suffixes, to), glob));
283 });
284 }
285 }
286
287 ImportedItem HandleDirectoriesCopy(AbsolutePath src, AbsolutePath dst, FileOrDirectory glob)
288 {
289 if (!options.Pretend) src.Copy(dst, ExistsPolicy.MergeAndOverwrite);
290 return new(src, dst, ImportMethod.Copy);
291 }
292
293 ImportedItem HandleFilesCopy(AbsolutePath src, AbsolutePath dst, FileOrDirectory glob)
294 {
295 if (!options.Pretend)
296 {
297 src.Copy(dst, ExistsPolicy.FileOverwrite);
298 if (glob.ProcessContent)
299 dst.ProcessSuffixContent(options.Suffixes);
300 }
301 return new(src, dst, ImportMethod.Copy);
302 }
303
304 ImportedItem HandleDirectoriesLink(AbsolutePath src, AbsolutePath dst, FileOrDirectory glob)
305 {
306 if (!options.Pretend) dst.LinksDirectory(src);
307 return new(src, dst, ImportMethod.Link);
308 }
309
310 ImportedItem HandleFilesLink(AbsolutePath src, AbsolutePath dst, FileOrDirectory glob)
311 {
312 if (!options.Pretend) dst.LinksFile(src);
313 return new(src, dst, ImportMethod.Link);
314 }
315
316 FileSystemTask(instructions.Copy, HandleDirectoriesCopy, HandleFilesCopy);
317 FileSystemTask(
318 instructions.Link,
319 options.ForceCopyLinks ? HandleDirectoriesCopy : HandleDirectoriesLink,
320 options.ForceCopyLinks ? HandleFilesCopy : HandleFilesLink
321 );
322
323 foreach (var glob in instructions.Use)
324 {
325 if (string.IsNullOrWhiteSpace(glob.Directory) && string.IsNullOrWhiteSpace(glob.File))
326 continue;
327
328 var manifestFilePattern = glob.ManifestFilePattern ?? import.ManifestFilePattern;
329
330 var manifestGlob = glob.Directory != null
331 ? glob.Directory + "/" + manifestFilePattern
332 : glob.File;
333
334 var manifests = import.From.SearchFiles(manifestGlob!)
335 .Select(p => p.Parent)
336 .Where(p => p != import.From)
337 .ToList();
338
339 if (!options.Pretend)
340 {
341 if (manifests.Count > 0)
342 Log.Information(
343 "Folder {0} uses {1} importing\n {2}",
344 import.From, manifestGlob,
345 string.Join("\n ", manifests)
346 );
347 else
348 Log.Warning(
349 "Folder {0} attempted to import {1} but no importable subfolders were found (none of them had an export manifest file)",
350 import.From, manifestGlob
351 );
352 }
353
354 var exclude = glob.Not.Concat(instructions.Not);
355
356 manifests.ForEach((p, i) =>
357 {
358 var dst = glob.GetDestination(import.From, to, p, i, exclude);
359 if (dst == null)
360 {
361 if (!options.Pretend) Log.Information("Ignoring folder {0}", p, dst);
362 return;
363 }
364 if (!options.Pretend) Log.Information("Importing folder {0} -> {1}", p, dst);
365 result.AddRange(
366 self.ImportFolder(
367 (p, dst.Parent, Path.GetFileName(manifestGlob!)),
368 options with { UseSubfolder = true, ImplicitImport = true }
369 )
370 );
371 });
372 }
373
374 return result;
375 }
376
377 /// <summary>
378 /// The result of ImportFolder only contain directory references if they were imported
379 /// explicitly as a directory as a singular item (so not via file globbing). However there are
380 /// cases in which all affected files should be minded. WithFilesExpanded converts the result
381 /// of ImportFolder into a proper flat list of files only. It can work with pretended data as
382 /// well, maintaining correct relations between From / To members.
383 /// </summary>
384 /// <param name="importedItems"></param>
385 /// <param name="pattern"></param>
386 /// <param name="depth"></param>
387 /// <returns>
388 /// A flat list of all files affected even in directories referenced as singular items.
389 /// </returns>
390 public static IEnumerable<ImportedItem> WithFilesExpanded(
391 this IEnumerable<ImportedItem> importedItems,
392 string pattern = "*",
393 int depth = 40
394 ) => importedItems
395 .SelectMany(i =>
396 {
397 if (i.From.DirectoryExists())
398 return i.From.GetFiles(pattern, depth)
399 .Select(f => new ImportedItem(
400 f, i.To / i.From.GetRelativePathTo(f), i.Method
401 ))
402 ;
403 else return [i];
404 });
405}
Controls how a folder should be exported for composition. It is meant to be used with export....
A union provided for denoting wether we want to link/copy a file or a directory. It is undefined beha...
bool ProcessContent
When working with a file, process its contents for replacing specified suffixes.
static IEnumerable< ImportedItem > WithFilesExpanded(this IEnumerable< ImportedItem > importedItems, string pattern="*", int depth=40)
The result of ImportFolder only contain directory references if they were imported explicitly as a di...
static List< ImportedItem > ImportFolder(this INukeBuild self, ImportFolderItem import, ImportOptions? options=null)
There are cases when one project needs to compose from one pre-existing rigid folder structure of one...
static List< ImportedItem > ImportFolders(this INukeBuild self, ImportOptions? options, params ImportFolderItem[] imports)
Convenience method for specifying multiple folder from/to pairs for ImportFolder
static List< ImportedItem > ImportFolders(this INukeBuild self, params ImportFolderItem[] imports)
record class ImportOptions(bool UseSubfolder=true, bool Pretend=false, bool CopyByDefault=false, bool ForceCopyLinks=false, bool ImplicitImport=false, ImportFolderSuffixes? Suffixes=null, IEnumerable< ExportManifest >? AddToMain=null, IEnumerable< ExportManifest >? AddToAll=null)
Further options for ImportFolder.
record class ImportFolderSuffixes(string To, string From="Origin")
A record for suffix replacement mapping.
record class ImportFolderItem(AbsolutePath From, AbsolutePath ToParent, ExportManifest? Manifest=null, string ManifestFilePattern="export.y*ml")
A record importing a folder from a source to a parent folder.