com.elestrago.unity.generated-code-dump 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/CHANGELOG.md.meta +7 -0
- package/Documentation~/api.md +583 -0
- package/Documentation~/examples/dump-in-batch-mode.md +43 -0
- package/Documentation~/examples/dump-now.md +29 -0
- package/Documentation~/examples/read-the-dump.md +63 -0
- package/Documentation~/examples/turn-on-the-dump.md +33 -0
- package/Documentation~/examples.md +24 -0
- package/Documentation~/manual.md +618 -0
- package/Editor/AssemblyInfo.cs +3 -0
- package/Editor/AssemblyInfo.cs.meta +2 -0
- package/Editor/GeneratedCodeDump.Editor.asmdef +16 -0
- package/Editor/GeneratedCodeDump.Editor.asmdef.meta +7 -0
- package/Editor/GeneratedCodeDumpArgs.cs +113 -0
- package/Editor/GeneratedCodeDumpArgs.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpFiles.cs +120 -0
- package/Editor/GeneratedCodeDumpFiles.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpFinisher.cs +103 -0
- package/Editor/GeneratedCodeDumpFinisher.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpIdentity.cs +40 -0
- package/Editor/GeneratedCodeDumpIdentity.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpJobs.cs +242 -0
- package/Editor/GeneratedCodeDumpJobs.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpMigration.cs +112 -0
- package/Editor/GeneratedCodeDumpMigration.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpNames.cs +104 -0
- package/Editor/GeneratedCodeDumpNames.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpOutput.cs +449 -0
- package/Editor/GeneratedCodeDumpOutput.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpPathBudget.cs +67 -0
- package/Editor/GeneratedCodeDumpPathBudget.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpPaths.cs +145 -0
- package/Editor/GeneratedCodeDumpPaths.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpProcess.cs +119 -0
- package/Editor/GeneratedCodeDumpProcess.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpRsp.cs +107 -0
- package/Editor/GeneratedCodeDumpRsp.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSession.cs +65 -0
- package/Editor/GeneratedCodeDumpSession.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettings.cs +111 -0
- package/Editor/GeneratedCodeDumpSettings.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettingsFile.cs +231 -0
- package/Editor/GeneratedCodeDumpSettingsFile.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettingsProvider.cs +93 -0
- package/Editor/GeneratedCodeDumpSettingsProvider.cs.meta +2 -0
- package/Editor/GeneratedCodeDumpSettingsWatcher.cs +199 -0
- package/Editor/GeneratedCodeDumpSettingsWatcher.cs.meta +2 -0
- package/Editor/GeneratedCodeDumper.cs +689 -0
- package/Editor/GeneratedCodeDumper.cs.meta +2 -0
- package/Editor/GeneratedCodeMenuItems.cs +46 -0
- package/Editor/GeneratedCodeMenuItems.cs.meta +2 -0
- package/Editor.meta +8 -0
- package/LICENSE.md +21 -0
- package/LICENSE.md.meta +7 -0
- package/README.md +164 -0
- package/README.md.meta +7 -0
- package/package.json +31 -0
- package/package.json.meta +7 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using UnityEngine;
|
|
5
|
+
|
|
6
|
+
namespace GeneratedCodeDump.Editor
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Moves a project off the 1.0.0 folders: <see cref="GeneratedCodeDumpPaths.LegacySettingsFile"/> becomes
|
|
10
|
+
/// <see cref="GeneratedCodeDumpPaths.SettingsFile"/> as it is, unless that file already exists; then the rest
|
|
11
|
+
/// of <see cref="GeneratedCodeDumpPaths.LegacyRoot"/> and this project's old temp folder are deleted.
|
|
12
|
+
/// </summary>
|
|
13
|
+
/// <remarks>
|
|
14
|
+
/// Runs from the dumper's static constructor, before the setting is first read. A step that fails is a
|
|
15
|
+
/// warning and is retried on the next start; the legacy folder is kept while its setting could not be moved.
|
|
16
|
+
/// No UnityEditor use.
|
|
17
|
+
/// </remarks>
|
|
18
|
+
internal static class GeneratedCodeDumpMigration
|
|
19
|
+
{
|
|
20
|
+
/// <summary>
|
|
21
|
+
/// Whether the legacy setting moves: only when it exists and the new file does not.
|
|
22
|
+
/// </summary>
|
|
23
|
+
internal static bool ShouldMoveSettings(bool legacySettingsExists, bool settingsExists)
|
|
24
|
+
{
|
|
25
|
+
return legacySettingsExists && !settingsExists;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// <summary>
|
|
29
|
+
/// Migrates this project. Never throws.
|
|
30
|
+
/// </summary>
|
|
31
|
+
internal static void Run()
|
|
32
|
+
{
|
|
33
|
+
try
|
|
34
|
+
{
|
|
35
|
+
var legacyRoot = GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.LegacyRoot);
|
|
36
|
+
var hadLegacyRoot = Directory.Exists(legacyRoot);
|
|
37
|
+
var problems = Migrate(
|
|
38
|
+
legacyRoot,
|
|
39
|
+
GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.LegacySettingsFile),
|
|
40
|
+
GeneratedCodeDumpPaths.ToFullPath(GeneratedCodeDumpPaths.SettingsFile),
|
|
41
|
+
GeneratedCodeDumpPaths.LegacyScratchRoot,
|
|
42
|
+
GeneratedCodeDumpPaths.LegacyScratchBase
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
foreach (var problem in problems)
|
|
46
|
+
Debug.LogWarning(GeneratedCodeDumpIdentity.LogPrefix + problem);
|
|
47
|
+
|
|
48
|
+
if (hadLegacyRoot && !Directory.Exists(legacyRoot))
|
|
49
|
+
{
|
|
50
|
+
Debug.Log(
|
|
51
|
+
$"{GeneratedCodeDumpIdentity.LogPrefix}{GeneratedCodeDumpPaths.LegacyRoot}/ is replaced by {GeneratedCodeDumpPaths.Root}/ "
|
|
52
|
+
+ $"and {GeneratedCodeDumpPaths.SettingsFile}, so it was deleted."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (Exception exception)
|
|
57
|
+
{
|
|
58
|
+
Debug.LogWarning($"{GeneratedCodeDumpIdentity.LogPrefix}moving off {GeneratedCodeDumpPaths.LegacyRoot}/ failed: {exception.Message}");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// <summary>
|
|
63
|
+
/// Moves <paramref name="legacySettingsFile"/> to <paramref name="settingsFile"/> when
|
|
64
|
+
/// <see cref="ShouldMoveSettings"/> says so, then deletes <paramref name="legacyRoot"/> and
|
|
65
|
+
/// <paramref name="legacyScratchRoot"/>, and <paramref name="legacyScratchBase"/> when it is empty. Full
|
|
66
|
+
/// paths. Returns what failed.
|
|
67
|
+
/// </summary>
|
|
68
|
+
internal static List<string> Migrate(string legacyRoot, string legacySettingsFile, string settingsFile, string legacyScratchRoot, string legacyScratchBase)
|
|
69
|
+
{
|
|
70
|
+
var problems = new List<string>();
|
|
71
|
+
|
|
72
|
+
if (Directory.Exists(legacyRoot) && TryMoveSettings(legacySettingsFile, settingsFile, problems))
|
|
73
|
+
TryDelete(legacyRoot, problems);
|
|
74
|
+
|
|
75
|
+
if (Directory.Exists(legacyScratchRoot))
|
|
76
|
+
TryDelete(legacyScratchRoot, problems);
|
|
77
|
+
|
|
78
|
+
// Other projects on a 1.0.0 dump may still use it.
|
|
79
|
+
GeneratedCodeDumpFiles.TryDeleteEmptyDirectory(legacyScratchBase);
|
|
80
|
+
return problems;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static bool TryMoveSettings(string legacySettingsFile, string settingsFile, List<string> problems)
|
|
84
|
+
{
|
|
85
|
+
try
|
|
86
|
+
{
|
|
87
|
+
var text = GeneratedCodeDumpFiles.ReadTextOrNull(legacySettingsFile);
|
|
88
|
+
if (ShouldMoveSettings(text != null, File.Exists(settingsFile)))
|
|
89
|
+
GeneratedCodeDumpFiles.WriteAtomic(settingsFile, text);
|
|
90
|
+
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
94
|
+
{
|
|
95
|
+
problems.Add($"{legacySettingsFile} could not be moved to {settingsFile}: {exception.Message} The next Editor start retries.");
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static void TryDelete(string folder, List<string> problems)
|
|
101
|
+
{
|
|
102
|
+
try
|
|
103
|
+
{
|
|
104
|
+
GeneratedCodeDumpFiles.DeleteDirectory(folder);
|
|
105
|
+
}
|
|
106
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
107
|
+
{
|
|
108
|
+
problems.Add($"the old folder {folder} could not be deleted: {exception.Message} The next Editor start retries.");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.Globalization;
|
|
4
|
+
using System.IO;
|
|
5
|
+
|
|
6
|
+
namespace GeneratedCodeDump.Editor
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// Names in the dump: reserved Windows names made safe, and too-long names cut to
|
|
10
|
+
/// <c><start>~<fnv8 of the full name></c>, so a cut name stays unique and readable.
|
|
11
|
+
/// </summary>
|
|
12
|
+
internal static class GeneratedCodeDumpNames
|
|
13
|
+
{
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// Names of this many characters or fewer are never cut.
|
|
16
|
+
/// </summary>
|
|
17
|
+
internal const int MaxUncutLength = 25;
|
|
18
|
+
|
|
19
|
+
/// <summary>
|
|
20
|
+
/// Characters a cut folder name keeps from the start of the full name.
|
|
21
|
+
/// </summary>
|
|
22
|
+
internal const int CutFolderStart = 16;
|
|
23
|
+
|
|
24
|
+
/// <summary>
|
|
25
|
+
/// Fewest characters a cut file name keeps from the start of the full name.
|
|
26
|
+
/// </summary>
|
|
27
|
+
internal const int MinCutFileStart = 8;
|
|
28
|
+
|
|
29
|
+
// "~" and 8 hex digits.
|
|
30
|
+
private const int TagLength = 9;
|
|
31
|
+
|
|
32
|
+
// Longest first, so "A.generated.cs" is not split as "A.generated" + ".cs".
|
|
33
|
+
private static readonly string[] FileSuffixes = { ".generated.cs", ".g.cs" };
|
|
34
|
+
|
|
35
|
+
private static readonly HashSet<string> ReservedWindowsNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
36
|
+
{
|
|
37
|
+
"CON", "PRN", "AUX", "NUL",
|
|
38
|
+
"COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
|
39
|
+
"LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/// <summary>
|
|
43
|
+
/// <c>~<fnv8></c>: FNV-1a 32 of <paramref name="fullName"/> as 8 lowercase hex digits.
|
|
44
|
+
/// </summary>
|
|
45
|
+
internal static string Tag(string fullName)
|
|
46
|
+
{
|
|
47
|
+
return "~" + GeneratedCodeDumpPaths.Fnv1a(fullName).ToString("x8", CultureInfo.InvariantCulture);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// <summary>
|
|
51
|
+
/// The folder name itself when it has at most <see cref="MaxUncutLength"/> characters, else its first
|
|
52
|
+
/// <see cref="CutFolderStart"/> characters and <see cref="Tag"/>.
|
|
53
|
+
/// </summary>
|
|
54
|
+
internal static string CutFolder(string name)
|
|
55
|
+
{
|
|
56
|
+
// Cut from the safe name, so a reserved start stays renamed; the tag keeps any other start safe.
|
|
57
|
+
var safe = SafeSegment(name);
|
|
58
|
+
return name.Length <= MaxUncutLength ? safe : safe.Substring(0, CutFolderStart) + Tag(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// <summary>
|
|
62
|
+
/// The file name itself when it fits in <paramref name="room"/> characters, else
|
|
63
|
+
/// <c><start>~<fnv8><suffix></c> exactly <paramref name="room"/> long, keeping the
|
|
64
|
+
/// <see cref="Suffix"/> whole. Null when fewer than <see cref="MinCutFileStart"/> characters of the start
|
|
65
|
+
/// would remain.
|
|
66
|
+
/// </summary>
|
|
67
|
+
internal static string CutFile(string fileName, int room)
|
|
68
|
+
{
|
|
69
|
+
var safe = SafeSegment(fileName);
|
|
70
|
+
if (safe.Length <= room)
|
|
71
|
+
return safe;
|
|
72
|
+
|
|
73
|
+
var suffix = Suffix(fileName);
|
|
74
|
+
var start = room - TagLength - suffix.Length;
|
|
75
|
+
if (start < MinCutFileStart)
|
|
76
|
+
return null;
|
|
77
|
+
|
|
78
|
+
return SafeSegment(safe.Substring(0, start) + Tag(fileName) + suffix);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// <summary>
|
|
82
|
+
/// The part of a file name a cut keeps: <c>.generated.cs</c>, <c>.g.cs</c>, else the extension.
|
|
83
|
+
/// </summary>
|
|
84
|
+
internal static string Suffix(string fileName)
|
|
85
|
+
{
|
|
86
|
+
foreach (var suffix in FileSuffixes)
|
|
87
|
+
{
|
|
88
|
+
if (fileName.Length > suffix.Length && fileName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
|
89
|
+
return fileName.Substring(fileName.Length - suffix.Length);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var extension = Path.GetExtension(fileName);
|
|
93
|
+
return extension.Length < fileName.Length ? extension : string.Empty;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Windows refuses a folder or file named CON, AUX, NUL… whatever follows the name.
|
|
97
|
+
internal static string SafeSegment(string segment)
|
|
98
|
+
{
|
|
99
|
+
var dot = segment.IndexOf('.');
|
|
100
|
+
var name = dot < 0 ? segment : segment.Substring(0, dot);
|
|
101
|
+
return ReservedWindowsNames.Contains(name) ? name + "_" + segment.Substring(name.Length) : segment;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Linq;
|
|
5
|
+
using System.Text;
|
|
6
|
+
|
|
7
|
+
namespace GeneratedCodeDump.Editor
|
|
8
|
+
{
|
|
9
|
+
/// <summary>
|
|
10
|
+
/// One file of the compiler's generated-files output and its dump name before any cut.
|
|
11
|
+
/// </summary>
|
|
12
|
+
internal sealed class GeneratedCodeDumpEntry
|
|
13
|
+
{
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// Path under the generated-files output, '/'-separated:
|
|
16
|
+
/// <c><GeneratorAssembly>/<GeneratorType>/<hint name></c>.
|
|
17
|
+
/// </summary>
|
|
18
|
+
internal string Source;
|
|
19
|
+
|
|
20
|
+
/// <summary>
|
|
21
|
+
/// The generator's assembly name; null for a file fewer than two folders deep.
|
|
22
|
+
/// </summary>
|
|
23
|
+
internal string GeneratorAssembly;
|
|
24
|
+
|
|
25
|
+
/// <summary>
|
|
26
|
+
/// The generator's full type name; null for a file fewer than two folders deep.
|
|
27
|
+
/// </summary>
|
|
28
|
+
internal string GeneratorType;
|
|
29
|
+
|
|
30
|
+
/// <summary>
|
|
31
|
+
/// The hint name as the generator gave it; <see cref="Source"/> when no generator is known.
|
|
32
|
+
/// </summary>
|
|
33
|
+
internal string Hint;
|
|
34
|
+
|
|
35
|
+
/// <summary>
|
|
36
|
+
/// The generator's folder with its full, safe name; null when no generator is known.
|
|
37
|
+
/// </summary>
|
|
38
|
+
internal string GeneratorFolder;
|
|
39
|
+
|
|
40
|
+
/// <summary>
|
|
41
|
+
/// Path under <see cref="GeneratorFolder"/> (else under the assembly folder), '/'-separated and safe.
|
|
42
|
+
/// Its last part is the file name.
|
|
43
|
+
/// </summary>
|
|
44
|
+
internal string Name;
|
|
45
|
+
|
|
46
|
+
/// <summary>
|
|
47
|
+
/// Path under the assembly folder with full names.
|
|
48
|
+
/// </summary>
|
|
49
|
+
internal string FullPath => GeneratorFolder == null ? Name : GeneratorFolder + "/" + Name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// <summary>
|
|
53
|
+
/// Where one assembly's files go: its folder (full or cut), each file's path in it, and the files that get
|
|
54
|
+
/// no path.
|
|
55
|
+
/// </summary>
|
|
56
|
+
internal sealed class GeneratedCodeDumpLayout
|
|
57
|
+
{
|
|
58
|
+
internal string AssemblyFolder;
|
|
59
|
+
|
|
60
|
+
/// <summary>
|
|
61
|
+
/// The assembly folder's other form (full or cut), which a sync removes; null when both are the same.
|
|
62
|
+
/// </summary>
|
|
63
|
+
internal string OtherAssemblyFolder;
|
|
64
|
+
|
|
65
|
+
/// <summary>
|
|
66
|
+
/// Every placed file with its '/'-separated path under <see cref="AssemblyFolder"/>, in entry order.
|
|
67
|
+
/// </summary>
|
|
68
|
+
internal readonly List<(GeneratedCodeDumpEntry Entry, string Path)> Files = new List<(GeneratedCodeDumpEntry Entry, string Path)>();
|
|
69
|
+
|
|
70
|
+
/// <summary>
|
|
71
|
+
/// Files whose path is over the budget even with every cut.
|
|
72
|
+
/// </summary>
|
|
73
|
+
internal readonly List<GeneratedCodeDumpEntry> TooLong = new List<GeneratedCodeDumpEntry>();
|
|
74
|
+
|
|
75
|
+
/// <summary>
|
|
76
|
+
/// Files whose path an earlier file took.
|
|
77
|
+
/// </summary>
|
|
78
|
+
internal readonly List<GeneratedCodeDumpEntry> Taken = new List<GeneratedCodeDumpEntry>();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// <summary>
|
|
82
|
+
/// What a sync could not do, one line per file with its path under the dump root.
|
|
83
|
+
/// </summary>
|
|
84
|
+
internal sealed class GeneratedCodeDumpSyncResult
|
|
85
|
+
{
|
|
86
|
+
/// <summary>
|
|
87
|
+
/// Files skipped, or not written or deleted, because their path is over the OS limit.
|
|
88
|
+
/// </summary>
|
|
89
|
+
internal readonly List<string> TooLong = new List<string>();
|
|
90
|
+
|
|
91
|
+
/// <summary>
|
|
92
|
+
/// Files that could not be written or deleted, with the reason.
|
|
93
|
+
/// </summary>
|
|
94
|
+
internal readonly List<string> Failed = new List<string>();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// <summary>
|
|
98
|
+
/// Turns the compiler's generated-files output (<c><GeneratorAssembly>/<GeneratorType>/<hint name></c>)
|
|
99
|
+
/// into the dump layout <c><Assembly>/<GeneratorAssembly>/<hint name></c>, adds the header
|
|
100
|
+
/// lines to every file and syncs it into the dump folder. No UnityEditor use.
|
|
101
|
+
/// </summary>
|
|
102
|
+
/// <remarks>
|
|
103
|
+
/// Names stay whole wherever the path fits the OS limit. When one doesn't, the assembly folder is cut first,
|
|
104
|
+
/// then the generator folder, then the file name (<see cref="GeneratedCodeDumpNames"/>); a file that still
|
|
105
|
+
/// doesn't fit is skipped. The header keeps the full names, so a grep finds a cut file.
|
|
106
|
+
/// </remarks>
|
|
107
|
+
internal static class GeneratedCodeDumpOutput
|
|
108
|
+
{
|
|
109
|
+
private static readonly char[] Separators = { '/', '\\' };
|
|
110
|
+
private static readonly byte[] Utf8Bom = { 0xEF, 0xBB, 0xBF };
|
|
111
|
+
private static readonly string WorkFolderName = Path.GetFileName(GeneratedCodeDumpPaths.WorkFolder);
|
|
112
|
+
|
|
113
|
+
/// <summary>
|
|
114
|
+
/// One entry per generated file, from its paths relative to the generated-files output, in ordinal order.
|
|
115
|
+
/// The hint becomes one file name in its generator assembly's folder. When an earlier file took that
|
|
116
|
+
/// name there, the later one becomes <c><stem>~<fnv8 of "<GeneratorType>/<hint>"><suffix></c>.
|
|
117
|
+
/// A file fewer than two folders deep keeps its path, every part made safe.
|
|
118
|
+
/// </summary>
|
|
119
|
+
internal static List<GeneratedCodeDumpEntry> Entries(IEnumerable<string> sources)
|
|
120
|
+
{
|
|
121
|
+
var entries = new List<GeneratedCodeDumpEntry>();
|
|
122
|
+
var taken = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
123
|
+
|
|
124
|
+
foreach (var source in sources.Select(source => source.Replace('\\', '/')).OrderBy(source => source, StringComparer.Ordinal))
|
|
125
|
+
{
|
|
126
|
+
var parts = source.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
|
|
127
|
+
if (parts.Length == 0)
|
|
128
|
+
continue;
|
|
129
|
+
|
|
130
|
+
var entry = new GeneratedCodeDumpEntry { Source = source };
|
|
131
|
+
if (parts.Length < 3)
|
|
132
|
+
{
|
|
133
|
+
entry.Hint = string.Join("/", parts);
|
|
134
|
+
entry.Name = string.Join("/", parts.Select(GeneratedCodeDumpNames.SafeSegment));
|
|
135
|
+
}
|
|
136
|
+
else
|
|
137
|
+
{
|
|
138
|
+
entry.GeneratorAssembly = parts[0];
|
|
139
|
+
entry.GeneratorType = parts[1];
|
|
140
|
+
entry.Hint = string.Join("/", parts.Skip(2));
|
|
141
|
+
entry.GeneratorFolder = GeneratedCodeDumpNames.SafeSegment(parts[0]);
|
|
142
|
+
entry.Name = string.Join("/", parts.Skip(2).Select(GeneratedCodeDumpNames.SafeSegment));
|
|
143
|
+
if (taken.Contains(entry.FullPath))
|
|
144
|
+
entry.Name = WithTag(entry.Name, entry.GeneratorType + "/" + entry.Hint);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
taken.Add(entry.FullPath);
|
|
148
|
+
entries.Add(entry);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return entries;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// <summary>
|
|
155
|
+
/// Places every entry under <c><dumpRoot>/<assembly folder>/</c> within <paramref name="budget"/>.
|
|
156
|
+
/// If any file doesn't fit with full names, the assembly folder is cut for every file; then a generator
|
|
157
|
+
/// folder is cut for all its files if any of them still doesn't fit; then each file that still doesn't fit
|
|
158
|
+
/// gets its name cut to the room left, or is skipped as too long. Only counts lengths.
|
|
159
|
+
/// </summary>
|
|
160
|
+
internal static GeneratedCodeDumpLayout Plan(IReadOnlyList<GeneratedCodeDumpEntry> entries, string dumpRoot, string assembly, GeneratedCodeDumpPathBudget budget)
|
|
161
|
+
{
|
|
162
|
+
var root = dumpRoot.TrimEnd(Separators);
|
|
163
|
+
var full = GeneratedCodeDumpNames.SafeSegment(assembly);
|
|
164
|
+
var cut = GeneratedCodeDumpNames.CutFolder(assembly);
|
|
165
|
+
var assemblyFolder = entries.All(entry => Fits(budget, root + "/" + full, entry.FullPath)) ? full : cut;
|
|
166
|
+
var layout = new GeneratedCodeDumpLayout
|
|
167
|
+
{
|
|
168
|
+
AssemblyFolder = assemblyFolder,
|
|
169
|
+
OtherAssemblyFolder = full == cut ? null : assemblyFolder == full ? cut : full
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
var assemblyPath = root + "/" + assemblyFolder;
|
|
173
|
+
var generatorFolders = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
174
|
+
foreach (var generator in entries.Where(entry => entry.GeneratorAssembly != null).GroupBy(entry => entry.GeneratorAssembly, StringComparer.Ordinal))
|
|
175
|
+
{
|
|
176
|
+
var fullFolder = generator.First().GeneratorFolder;
|
|
177
|
+
generatorFolders[generator.Key] = generator.All(entry => Fits(budget, assemblyPath + "/" + fullFolder, entry.Name))
|
|
178
|
+
? fullFolder
|
|
179
|
+
: GeneratedCodeDumpNames.CutFolder(generator.Key);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
var placed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
183
|
+
foreach (var entry in entries)
|
|
184
|
+
{
|
|
185
|
+
var generatorFolder = entry.GeneratorAssembly == null ? null : generatorFolders[entry.GeneratorAssembly];
|
|
186
|
+
var name = FitName(budget, generatorFolder == null ? assemblyPath : assemblyPath + "/" + generatorFolder, entry.Name);
|
|
187
|
+
if (name == null)
|
|
188
|
+
{
|
|
189
|
+
layout.TooLong.Add(entry);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Two cut names can still meet (32-bit hash).
|
|
194
|
+
var path = generatorFolder == null ? name : generatorFolder + "/" + name;
|
|
195
|
+
if (placed.Add(path))
|
|
196
|
+
layout.Files.Add((entry, path));
|
|
197
|
+
else
|
|
198
|
+
layout.Taken.Add(entry);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return layout;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// <summary>
|
|
205
|
+
/// The header lines of a dumped file, with full names: <c>Dumped</c>, <c>Assembly</c>,
|
|
206
|
+
/// <c>Generator: <assembly> / <type></c> (left out when <paramref name="generatorAssembly"/> is
|
|
207
|
+
/// null) and <c>Hint</c>.
|
|
208
|
+
/// </summary>
|
|
209
|
+
internal static List<string> Header(string stamp, string assembly, string generatorAssembly, string generatorType, string hint)
|
|
210
|
+
{
|
|
211
|
+
var header = new List<string> { "Dumped: " + stamp, "Assembly: " + assembly };
|
|
212
|
+
if (generatorAssembly != null)
|
|
213
|
+
header.Add($"Generator: {generatorAssembly} / {generatorType}");
|
|
214
|
+
|
|
215
|
+
header.Add("Hint: " + hint);
|
|
216
|
+
return header;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// <summary>
|
|
220
|
+
/// Adds <paramref name="header"/> to the file's header: inside a <c>// <auto-generated></c> block,
|
|
221
|
+
/// after a <c>// <auto-generated/></c> line, or else as new first lines. Keeps the file's line
|
|
222
|
+
/// endings.
|
|
223
|
+
/// </summary>
|
|
224
|
+
internal static string Stamp(string text, IReadOnlyList<string> header)
|
|
225
|
+
{
|
|
226
|
+
var newLine = text.Contains("\r\n") ? "\r\n" : "\n";
|
|
227
|
+
var lines = text.Split(new[] { newLine }, StringSplitOptions.None).ToList();
|
|
228
|
+
var headerEnd = Math.Min(lines.Count, 12);
|
|
229
|
+
|
|
230
|
+
for (var i = 0; i < headerEnd; i++)
|
|
231
|
+
{
|
|
232
|
+
var line = lines[i].Trim();
|
|
233
|
+
if (line == "// </auto-generated>")
|
|
234
|
+
{
|
|
235
|
+
lines.InsertRange(i, header.Select(entry => "// " + entry));
|
|
236
|
+
return string.Join(newLine, lines);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (line == "// <auto-generated/>")
|
|
240
|
+
{
|
|
241
|
+
lines.InsertRange(i + 1, header.Select(entry => "// " + entry));
|
|
242
|
+
return string.Join(newLine, lines);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return string.Concat(header.Select(entry => "// " + entry + newLine)) + text;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// <summary>
|
|
250
|
+
/// Writes every file under <paramref name="generatedFilesOut"/> into
|
|
251
|
+
/// <c><dumpRoot>/<assembly folder>/</c> as <see cref="Plan"/> places it, with the header lines,
|
|
252
|
+
/// then deletes every other file there and the assembly folder's other form. Files are replaced in place,
|
|
253
|
+
/// so a file another process holds open keeps its older header. The assembly folder is created by the
|
|
254
|
+
/// first write and removed when it ends up empty, so an assembly without generated files has no folder.
|
|
255
|
+
/// Every listing, write and delete is tried on its own; never throws for a single file.
|
|
256
|
+
/// </summary>
|
|
257
|
+
internal static GeneratedCodeDumpSyncResult Sync(string generatedFilesOut, string dumpRoot, string assembly, string stamp, GeneratedCodeDumpPathBudget budget)
|
|
258
|
+
{
|
|
259
|
+
var result = new GeneratedCodeDumpSyncResult();
|
|
260
|
+
var source = Path.GetFullPath(generatedFilesOut);
|
|
261
|
+
var root = Path.GetFullPath(dumpRoot);
|
|
262
|
+
|
|
263
|
+
// An output that can't be listed keeps the previous dump.
|
|
264
|
+
var files = new List<string>();
|
|
265
|
+
if (Directory.Exists(source) && !TryList(source, files, new List<string>(), result, path => path))
|
|
266
|
+
return result;
|
|
267
|
+
|
|
268
|
+
var entries = Entries(files.Select(file => Relative(source, file)));
|
|
269
|
+
var layout = Plan(entries, root, assembly, budget);
|
|
270
|
+
if (string.Equals(layout.AssemblyFolder, WorkFolderName, StringComparison.OrdinalIgnoreCase))
|
|
271
|
+
{
|
|
272
|
+
result.Failed.Add($"{assembly}: its dump folder would be the {WorkFolderName} folder");
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
result.TooLong.AddRange(layout.TooLong.Select(entry => GeneratedCodeDumpNames.SafeSegment(assembly) + "/" + entry.FullPath));
|
|
277
|
+
result.Failed.AddRange(layout.Taken.Select(entry => $"{entry.Source}: every dump path for it is taken by another file"));
|
|
278
|
+
|
|
279
|
+
var target = Path.Combine(root, layout.AssemblyFolder);
|
|
280
|
+
var written = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
281
|
+
foreach (var (entry, path) in layout.Files)
|
|
282
|
+
{
|
|
283
|
+
// Also when the write fails, so the previous dump's file stays.
|
|
284
|
+
written.Add(path);
|
|
285
|
+
var file = Path.Combine(target, ToOsPath(path));
|
|
286
|
+
var header = Header(stamp, assembly, entry.GeneratorAssembly, entry.GeneratorType, entry.Hint);
|
|
287
|
+
TryFileOperation(result, layout.AssemblyFolder + "/" + path, () =>
|
|
288
|
+
{
|
|
289
|
+
Directory.CreateDirectory(Path.GetDirectoryName(file));
|
|
290
|
+
File.WriteAllBytes(file, StampBytes(File.ReadAllBytes(Path.Combine(source, ToOsPath(entry.Source))), header));
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
Clean(root, layout.AssemblyFolder, written, result);
|
|
295
|
+
if (layout.OtherAssemblyFolder != null)
|
|
296
|
+
Clean(root, layout.OtherAssemblyFolder, new HashSet<string>(), result);
|
|
297
|
+
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// "<stem>~<fnv8 of fullName><suffix>" in place of the last part of path.
|
|
302
|
+
private static string WithTag(string path, string fullName)
|
|
303
|
+
{
|
|
304
|
+
var slash = path.LastIndexOf('/');
|
|
305
|
+
var fileName = path.Substring(slash + 1);
|
|
306
|
+
var suffix = GeneratedCodeDumpNames.Suffix(fileName);
|
|
307
|
+
return path.Substring(0, slash + 1) + fileName.Substring(0, fileName.Length - suffix.Length) + GeneratedCodeDumpNames.Tag(fullName) + suffix;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// True when <folder>/<path> fits; the path's own folders count toward the folder.
|
|
311
|
+
private static bool Fits(GeneratedCodeDumpPathBudget budget, string folder, string path)
|
|
312
|
+
{
|
|
313
|
+
var slash = path.LastIndexOf('/');
|
|
314
|
+
return slash < 0
|
|
315
|
+
? budget.Fits(folder, path)
|
|
316
|
+
: budget.Fits(folder + "/" + path.Substring(0, slash), path.Substring(slash + 1));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The path itself when it fits under folder, else with its file name cut to the room left; null when even
|
|
320
|
+
// the shortest cut doesn't fit.
|
|
321
|
+
private static string FitName(GeneratedCodeDumpPathBudget budget, string folder, string path)
|
|
322
|
+
{
|
|
323
|
+
if (Fits(budget, folder, path))
|
|
324
|
+
return path;
|
|
325
|
+
|
|
326
|
+
var slash = path.LastIndexOf('/');
|
|
327
|
+
var fileFolder = slash < 0 ? folder : folder + "/" + path.Substring(0, slash);
|
|
328
|
+
var fileName = path.Substring(slash + 1);
|
|
329
|
+
var folderLength = budget.Length(fileFolder);
|
|
330
|
+
if (folderLength > budget.Folder)
|
|
331
|
+
return null;
|
|
332
|
+
|
|
333
|
+
// CutFile counts characters; a budget in UTF-8 bytes can need a shorter name.
|
|
334
|
+
var room = Math.Min(budget.File - folderLength - 1, budget.Name);
|
|
335
|
+
while (true)
|
|
336
|
+
{
|
|
337
|
+
var cut = GeneratedCodeDumpNames.CutFile(fileName, room);
|
|
338
|
+
if (cut == null)
|
|
339
|
+
return null;
|
|
340
|
+
|
|
341
|
+
if (budget.Fits(fileFolder, cut))
|
|
342
|
+
return path.Substring(0, slash + 1) + cut;
|
|
343
|
+
|
|
344
|
+
room = cut.Length - 1;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private static byte[] StampBytes(byte[] bytes, IReadOnlyList<string> header)
|
|
349
|
+
{
|
|
350
|
+
var hasBom = bytes.Length >= 3 && bytes[0] == Utf8Bom[0] && bytes[1] == Utf8Bom[1] && bytes[2] == Utf8Bom[2];
|
|
351
|
+
var offset = hasBom ? Utf8Bom.Length : 0;
|
|
352
|
+
var text = Encoding.UTF8.GetString(bytes, offset, bytes.Length - offset);
|
|
353
|
+
var stamped = Encoding.UTF8.GetBytes(Stamp(text, header));
|
|
354
|
+
return hasBom ? Utf8Bom.Concat(stamped).ToArray() : stamped;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Deletes every file under <root>/<folder> not in keep (paths under that folder), then every folder left
|
|
358
|
+
// empty, that one included.
|
|
359
|
+
private static void Clean(string root, string folder, HashSet<string> keep, GeneratedCodeDumpSyncResult result)
|
|
360
|
+
{
|
|
361
|
+
var target = Path.Combine(root, folder);
|
|
362
|
+
if (!Directory.Exists(target))
|
|
363
|
+
return;
|
|
364
|
+
|
|
365
|
+
var files = new List<string>();
|
|
366
|
+
var folders = new List<string>();
|
|
367
|
+
TryList(target, files, folders, result, path => folder + "/" + Relative(target, path));
|
|
368
|
+
|
|
369
|
+
foreach (var file in files)
|
|
370
|
+
{
|
|
371
|
+
var relative = Relative(target, file);
|
|
372
|
+
if (!keep.Contains(relative))
|
|
373
|
+
TryFileOperation(result, folder + "/" + relative, () => File.Delete(file));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
foreach (var directory in folders.OrderByDescending(path => path.Length))
|
|
377
|
+
TryDeleteIfEmpty(directory);
|
|
378
|
+
|
|
379
|
+
TryDeleteIfEmpty(target);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Adds every file and folder under folder, one listing per folder, so a folder that can't be listed
|
|
383
|
+
// skips only itself. False when folder itself can't be listed.
|
|
384
|
+
private static bool TryList(string folder, List<string> files, List<string> folders, GeneratedCodeDumpSyncResult result, Func<string, string> display)
|
|
385
|
+
{
|
|
386
|
+
string[] here = null;
|
|
387
|
+
string[] below = null;
|
|
388
|
+
var listed = TryFileOperation(result, display(folder), () =>
|
|
389
|
+
{
|
|
390
|
+
here = Directory.GetFiles(folder);
|
|
391
|
+
below = Directory.GetDirectories(folder);
|
|
392
|
+
});
|
|
393
|
+
if (!listed)
|
|
394
|
+
return false;
|
|
395
|
+
|
|
396
|
+
files.AddRange(here);
|
|
397
|
+
foreach (var child in below)
|
|
398
|
+
{
|
|
399
|
+
folders.Add(child);
|
|
400
|
+
TryList(child, files, folders, result, display);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// A path over the OS limit goes to TooLong, any other file error to Failed.
|
|
407
|
+
private static bool TryFileOperation(GeneratedCodeDumpSyncResult result, string path, Action operation)
|
|
408
|
+
{
|
|
409
|
+
try
|
|
410
|
+
{
|
|
411
|
+
operation();
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
catch (PathTooLongException)
|
|
415
|
+
{
|
|
416
|
+
result.TooLong.Add(path);
|
|
417
|
+
}
|
|
418
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
419
|
+
{
|
|
420
|
+
result.Failed.Add($"{path}: {exception.Message}");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static void TryDeleteIfEmpty(string directory)
|
|
427
|
+
{
|
|
428
|
+
try
|
|
429
|
+
{
|
|
430
|
+
if (!Directory.EnumerateFileSystemEntries(directory).Any())
|
|
431
|
+
Directory.Delete(directory);
|
|
432
|
+
}
|
|
433
|
+
catch (Exception exception) when (exception is IOException || exception is UnauthorizedAccessException)
|
|
434
|
+
{
|
|
435
|
+
// Held open by another process; the next dump retries.
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private static string Relative(string root, string path)
|
|
440
|
+
{
|
|
441
|
+
return path.Substring(root.Length).TrimStart(Separators).Replace('\\', '/');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private static string ToOsPath(string path)
|
|
445
|
+
{
|
|
446
|
+
return path.Replace('/', Path.DirectorySeparatorChar);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|