-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathComputeFingerprint.cs
More file actions
249 lines (219 loc) · 9.87 KB
/
ComputeFingerprint.cs
File metadata and controls
249 lines (219 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
using System.Reflection;
using System.Text;
using JD.Efcpt.Build.Tasks.Decorators;
using JD.Efcpt.Build.Tasks.Extensions;
using Microsoft.Build.Framework;
using Task = Microsoft.Build.Utilities.Task;
namespace JD.Efcpt.Build.Tasks;
/// <summary>
/// MSBuild task that computes a deterministic fingerprint for efcpt inputs and detects when generation is needed.
/// </summary>
/// <remarks>
/// <para>
/// The fingerprint is derived from multiple sources to ensure regeneration when any relevant input changes:
/// <list type="bullet">
/// <item><description>Library version (JD.Efcpt.Build.Tasks assembly)</description></item>
/// <item><description>Tool version (EF Core Power Tools CLI version)</description></item>
/// <item><description>Database schema (DACPAC or connection string schema fingerprint)</description></item>
/// <item><description>Configuration JSON file contents</description></item>
/// <item><description>Renaming JSON file contents</description></item>
/// <item><description>MSBuild config property overrides (EfcptConfig* properties)</description></item>
/// <item><description>All template files under the template directory</description></item>
/// <item><description>Generated files (optional, via <c>EfcptDetectGeneratedFileChanges</c>)</description></item>
/// </list>
/// For each input, an XxHash64 hash is computed and written into an internal manifest string,
/// which is itself hashed using XxHash64 to produce the final <see cref="Fingerprint"/>.
/// </para>
/// <para>
/// The computed fingerprint is compared to the existing value stored in <see cref="FingerprintFile"/>.
/// If the file is missing or contains a different value, <see cref="HasChanged"/> is set to <c>true</c>,
/// the fingerprint is written back to <see cref="FingerprintFile"/>, and a log message indicates that
/// generation should proceed. Otherwise <see cref="HasChanged"/> is set to <c>false</c> and a message is
/// logged indicating that generation can be skipped.
/// </para>
/// </remarks>
public sealed class ComputeFingerprint : Task
{
/// <summary>
/// Path to the DACPAC file to include in the fingerprint (used in .sqlproj mode).
/// </summary>
public string DacpacPath { get; set; } = "";
/// <summary>
/// Schema fingerprint from QuerySchemaMetadata (used in connection string mode).
/// </summary>
public string SchemaFingerprint { get; set; } = "";
/// <summary>
/// Indicates whether we're in connection string mode.
/// </summary>
public string UseConnectionStringMode { get; set; } = "false";
/// <summary>
/// Path to the efcpt configuration JSON file to include in the fingerprint.
/// </summary>
[Required]
public string ConfigPath { get; set; } = "";
/// <summary>
/// Path to the efcpt renaming JSON file to include in the fingerprint.
/// </summary>
[Required]
public string RenamingPath { get; set; } = "";
/// <summary>
/// Root directory containing template files to include in the fingerprint.
/// </summary>
[Required]
public string TemplateDir { get; set; } = "";
/// <summary>
/// Path to the file that stores the last computed fingerprint.
/// </summary>
[Required]
public string FingerprintFile { get; set; } = "";
/// <summary>
/// Controls how much diagnostic information the task writes to the MSBuild log.
/// </summary>
public string LogVerbosity { get; set; } = "minimal";
/// <summary>
/// Version of the EF Core Power Tools CLI tool package being used.
/// </summary>
public string ToolVersion { get; set; } = "";
/// <summary>
/// Directory containing generated files to optionally include in the fingerprint.
/// </summary>
public string GeneratedDir { get; set; } = "";
/// <summary>
/// Indicates whether to detect changes to generated files (default: false to avoid overwriting manual edits).
/// </summary>
public string DetectGeneratedFileChanges { get; set; } = "false";
/// <summary>
/// Serialized JSON string containing MSBuild config property overrides.
/// </summary>
public string ConfigPropertyOverrides { get; set; } = "";
/// <summary>
/// Newly computed fingerprint value for the current inputs.
/// </summary>
[Output]
public string Fingerprint { get; set; } = "";
/// <summary>
/// Indicates whether the fingerprint has changed compared to the last recorded value.
/// </summary>
/// <value>
/// The string <c>true</c> if the fingerprint differs from the value stored in
/// <see cref="FingerprintFile"/>, or the file is missing; otherwise <c>false</c>.
/// </value>
[Output]
public string HasChanged { get; set; } = "true";
/// <inheritdoc />
public override bool Execute()
{
var decorator = TaskExecutionDecorator.Create(ExecuteCore);
var ctx = new TaskExecutionContext(Log, nameof(ComputeFingerprint));
return decorator.Execute(in ctx);
}
private bool ExecuteCore(TaskExecutionContext ctx)
{
var log = new BuildLog(ctx.Logger, LogVerbosity);
var manifest = new StringBuilder();
// Library version (JD.Efcpt.Build.Tasks assembly)
var libraryVersion = GetLibraryVersion();
if (!string.IsNullOrWhiteSpace(libraryVersion))
{
manifest.Append("library\0").Append(libraryVersion).Append('\n');
log.Detail($"Library version: {libraryVersion}");
}
// Tool version (EF Core Power Tools CLI)
if (!string.IsNullOrWhiteSpace(ToolVersion))
{
manifest.Append("tool\0").Append(ToolVersion).Append('\n');
log.Detail($"Tool version: {ToolVersion}");
}
// Source fingerprint (DACPAC OR schema fingerprint)
if (UseConnectionStringMode.IsTrue())
{
if (!string.IsNullOrWhiteSpace(SchemaFingerprint))
{
manifest.Append("schema\0").Append(SchemaFingerprint).Append('\n');
log.Detail($"Using schema fingerprint: {SchemaFingerprint}");
}
}
else
{
if (!string.IsNullOrWhiteSpace(DacpacPath) && File.Exists(DacpacPath))
{
// Use schema-based fingerprinting instead of raw file hash
// This produces consistent hashes for identical schemas even when
// build-time metadata (paths, timestamps) differs
var dacpacHash = DacpacFingerprint.Compute(DacpacPath);
manifest.Append("dacpac").Append('\0').Append(dacpacHash).Append('\n');
log.Detail($"Using DACPAC (schema fingerprint): {DacpacPath}");
}
}
Append(manifest, ConfigPath, "config");
Append(manifest, RenamingPath, "renaming");
// Config property overrides (MSBuild properties that override efcpt-config.json)
if (!string.IsNullOrWhiteSpace(ConfigPropertyOverrides))
{
manifest.Append("config-overrides\0").Append(ConfigPropertyOverrides).Append('\n');
log.Detail("Including MSBuild config property overrides in fingerprint");
}
manifest = Directory
.EnumerateFiles(TemplateDir, "*", SearchOption.AllDirectories)
.Select(p => p.Replace('\u005C', '/'))
.OrderBy(p => p, StringComparer.Ordinal)
.Select(file => (
rel: Path.GetRelativePath(TemplateDir, file).Replace('\u005C', '/'),
h: FileHash.HashFile(file)))
.Aggregate(manifest, (builder, data)
=> builder.Append("template/")
.Append(data.rel).Append('\0')
.Append(data.h).Append('\n'));
// Generated files (optional, off by default to avoid overwriting manual edits)
if (!string.IsNullOrWhiteSpace(GeneratedDir) && Directory.Exists(GeneratedDir) && DetectGeneratedFileChanges.IsTrue())
{
log.Detail("Detecting generated file changes (EfcptDetectGeneratedFileChanges=true)");
manifest = Directory
.EnumerateFiles(GeneratedDir, "*.g.cs", SearchOption.AllDirectories)
.Select(p => p.Replace('\u005C', '/'))
.OrderBy(p => p, StringComparer.Ordinal)
.Select(file => (
rel: Path.GetRelativePath(GeneratedDir, file).Replace('\u005C', '/'),
h: FileHash.HashFile(file)))
.Aggregate(manifest, (builder, data)
=> builder.Append("generated/")
.Append(data.rel).Append('\0')
.Append(data.h).Append('\n'));
}
Fingerprint = FileHash.HashString(manifest.ToString());
var prior = File.Exists(FingerprintFile) ? File.ReadAllText(FingerprintFile).Trim() : "";
HasChanged = prior.EqualsIgnoreCase(Fingerprint) ? "false" : "true";
if (HasChanged.IsTrue())
{
Directory.CreateDirectory(Path.GetDirectoryName(FingerprintFile)!);
File.WriteAllText(FingerprintFile, Fingerprint);
log.Info($"efcpt fingerprint changed: {Fingerprint}");
}
else
{
log.Info("efcpt fingerprint unchanged; skipping generation.");
}
return true;
}
private static string GetLibraryVersion()
{
try
{
var assembly = typeof(ComputeFingerprint).Assembly;
var version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? assembly.GetName().Version?.ToString()
?? "";
return version;
}
catch
{
return "";
}
}
private static void Append(StringBuilder manifest, string path, string label)
{
var full = Path.GetFullPath(path);
var h = FileHash.HashFile(full);
manifest.Append(label).Append('\0').Append(h).Append('\n');
}
}