@laurence79/wireit 0.14.13-shared-cache.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/LICENSE +202 -0
- package/README.md +1062 -0
- package/bin/wireit.js +9 -0
- package/lib/analyzer.js +1600 -0
- package/lib/caching/cache.js +7 -0
- package/lib/caching/github-actions-cache.js +832 -0
- package/lib/caching/local-cache.js +78 -0
- package/lib/caching/shared-cache.js +256 -0
- package/lib/cli-options.js +495 -0
- package/lib/cli.js +177 -0
- package/lib/config.js +18 -0
- package/lib/error.js +160 -0
- package/lib/event.js +7 -0
- package/lib/execution/base.js +108 -0
- package/lib/execution/no-command.js +32 -0
- package/lib/execution/service.js +1017 -0
- package/lib/execution/standard.js +683 -0
- package/lib/executor.js +249 -0
- package/lib/fingerprint.js +164 -0
- package/lib/ide.js +583 -0
- package/lib/language-server.js +135 -0
- package/lib/logging/combination-logger.js +41 -0
- package/lib/logging/debug-logger.js +43 -0
- package/lib/logging/logger.js +38 -0
- package/lib/logging/metrics-logger.js +108 -0
- package/lib/logging/quiet/run-tracker.js +597 -0
- package/lib/logging/quiet/stack-map.js +41 -0
- package/lib/logging/quiet/writeover-line.js +197 -0
- package/lib/logging/quiet-logger.js +78 -0
- package/lib/logging/simple-logger.js +296 -0
- package/lib/logging/watch-logger.js +81 -0
- package/lib/script-child-process.js +270 -0
- package/lib/util/ast.js +71 -0
- package/lib/util/async-cache.js +24 -0
- package/lib/util/copy.js +120 -0
- package/lib/util/deferred.js +35 -0
- package/lib/util/delete.js +120 -0
- package/lib/util/dispose.js +16 -0
- package/lib/util/fs.js +258 -0
- package/lib/util/glob.js +255 -0
- package/lib/util/line-monitor.js +69 -0
- package/lib/util/manifest.js +31 -0
- package/lib/util/optimize-mkdirs.js +55 -0
- package/lib/util/package-json-reader.js +61 -0
- package/lib/util/package-json.js +179 -0
- package/lib/util/script-data-dir.js +19 -0
- package/lib/util/shuffle.js +16 -0
- package/lib/util/unreachable.js +12 -0
- package/lib/util/windows.js +87 -0
- package/lib/util/worker-pool.js +61 -0
- package/lib/watcher.js +396 -0
- package/package.json +470 -0
- package/schema.json +132 -0
- package/wireit.svg +1 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import * as pathlib from 'path';
|
|
7
|
+
import * as fs from '../util/fs.js';
|
|
8
|
+
import { getScriptDataDir } from '../util/script-data-dir.js';
|
|
9
|
+
import { unreachable } from '../util/unreachable.js';
|
|
10
|
+
import { glob, GlobOutsideCwdError } from '../util/glob.js';
|
|
11
|
+
import { deleteEntries } from '../util/delete.js';
|
|
12
|
+
import lockfile from 'proper-lockfile';
|
|
13
|
+
import { ScriptChildProcess } from '../script-child-process.js';
|
|
14
|
+
import { BaseExecutionWithCommand } from './base.js';
|
|
15
|
+
import { Fingerprint } from '../fingerprint.js';
|
|
16
|
+
import { computeManifestEntry } from '../util/manifest.js';
|
|
17
|
+
/**
|
|
18
|
+
* Execution for a {@link StandardScriptConfig}.
|
|
19
|
+
*/
|
|
20
|
+
export class StandardScriptExecution extends BaseExecutionWithCommand {
|
|
21
|
+
#state = 'before-running';
|
|
22
|
+
#cache;
|
|
23
|
+
#workerPool;
|
|
24
|
+
constructor(config, executor, workerPool, cache, logger) {
|
|
25
|
+
super(config, executor, logger);
|
|
26
|
+
this.#workerPool = workerPool;
|
|
27
|
+
this.#cache = cache;
|
|
28
|
+
}
|
|
29
|
+
#ensureState(state) {
|
|
30
|
+
if (this.#state !== state) {
|
|
31
|
+
throw new Error(`Expected state ${state} but was ${this.#state}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async _execute() {
|
|
35
|
+
try {
|
|
36
|
+
this.#ensureState('before-running');
|
|
37
|
+
const dependencyFingerprints = await this._executeDependencies();
|
|
38
|
+
if (!dependencyFingerprints.ok) {
|
|
39
|
+
dependencyFingerprints.error.push(this.#startCancelledEvent);
|
|
40
|
+
return dependencyFingerprints;
|
|
41
|
+
}
|
|
42
|
+
// Significant time could have elapsed since we last checked because our
|
|
43
|
+
// dependencies had to finish.
|
|
44
|
+
if (this.#shouldNotStart) {
|
|
45
|
+
return { ok: false, error: [this.#startCancelledEvent] };
|
|
46
|
+
}
|
|
47
|
+
return await this.#acquireSystemLockIfNeeded(async () => {
|
|
48
|
+
// Note we must wait for dependencies to finish before generating the
|
|
49
|
+
// cache key, because a dependency could create or modify an input file to
|
|
50
|
+
// this script, which would affect the key.
|
|
51
|
+
const fingerprintResponse = await Fingerprint.compute(this._config, dependencyFingerprints.value);
|
|
52
|
+
if (!fingerprintResponse.ok) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: [fingerprintResponse.error],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const fingerprint = fingerprintResponse.value;
|
|
59
|
+
if (this._executor.failedInPreviousWatchIteration(this._config, fingerprint)) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: [
|
|
63
|
+
{
|
|
64
|
+
script: this._config,
|
|
65
|
+
type: 'failure',
|
|
66
|
+
reason: 'failed-previous-watch-iteration',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (await this.#fingerprintIsFresh(fingerprint)) {
|
|
72
|
+
const manifestFresh = await this.#outputManifestIsFresh();
|
|
73
|
+
if (!manifestFresh.ok) {
|
|
74
|
+
return { ok: false, error: [manifestFresh.error] };
|
|
75
|
+
}
|
|
76
|
+
if (manifestFresh.value) {
|
|
77
|
+
return this.#handleFresh(fingerprint);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Computing the fingerprint can take some time, and the next operation is
|
|
81
|
+
// destructive. Another good opportunity to check if we should still
|
|
82
|
+
// start.
|
|
83
|
+
if (this.#shouldNotStart) {
|
|
84
|
+
return { ok: false, error: [this.#startCancelledEvent] };
|
|
85
|
+
}
|
|
86
|
+
const cacheHit = fingerprint.data.fullyTracked
|
|
87
|
+
? await this.#cache?.get(this._config, fingerprint)
|
|
88
|
+
: undefined;
|
|
89
|
+
if (this.#shouldNotStart) {
|
|
90
|
+
return { ok: false, error: [this.#startCancelledEvent] };
|
|
91
|
+
}
|
|
92
|
+
if (cacheHit !== undefined) {
|
|
93
|
+
return this.#handleCacheHit(cacheHit, fingerprint);
|
|
94
|
+
}
|
|
95
|
+
return this.#handleNeedsRun(fingerprint);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
this._servicesNotNeeded.resolve();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether we should return early instead of starting this script.
|
|
104
|
+
*
|
|
105
|
+
* We should check this as the first thing we do, and then after any
|
|
106
|
+
* significant amount of time might have elapsed.
|
|
107
|
+
*/
|
|
108
|
+
get #shouldNotStart() {
|
|
109
|
+
return this._executor.shouldStopStartingNewScripts;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Convenience to generate a cancellation failure event for this script.
|
|
113
|
+
*/
|
|
114
|
+
get #startCancelledEvent() {
|
|
115
|
+
return {
|
|
116
|
+
script: this._config,
|
|
117
|
+
type: 'failure',
|
|
118
|
+
reason: 'start-cancelled',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Acquire a system-wide lock on the execution of this script, if the script
|
|
123
|
+
* has any output files that require it.
|
|
124
|
+
*/
|
|
125
|
+
async #acquireSystemLockIfNeeded(workFn) {
|
|
126
|
+
if (this._config.output?.values.length === 0) {
|
|
127
|
+
return workFn();
|
|
128
|
+
}
|
|
129
|
+
// The proper-lockfile library is designed to give an exclusive lock for a
|
|
130
|
+
// *file*. That's slightly misaligned with our use-case, because there's no
|
|
131
|
+
// particular file we need a lock for -- our lock is for the execution of
|
|
132
|
+
// this script.
|
|
133
|
+
//
|
|
134
|
+
// We can still use the library, we just need to pick some arbitrary file to
|
|
135
|
+
// ask it to lock for us. It actually errors if the file doesn't exist. So
|
|
136
|
+
// we end up with a mostly pointless file, and an adjacent "<file>.lock"
|
|
137
|
+
// directory that manages the lock (to acquire a lock, it does a mkdir for
|
|
138
|
+
// "<file>.lock", which will atomically succeed or fail depending on whether
|
|
139
|
+
// it already existed).
|
|
140
|
+
//
|
|
141
|
+
// TODO(aomarks) We could make our own implementation that directly takes a
|
|
142
|
+
// directory to mkdir and doesn't care about the file. There are some nice
|
|
143
|
+
// details proper-lockfile handles.
|
|
144
|
+
const lockFile = pathlib.join(this.#dataDir, 'lock');
|
|
145
|
+
await fs.mkdir(pathlib.dirname(lockFile), { recursive: true });
|
|
146
|
+
await fs.writeFile(lockFile, '', 'utf8');
|
|
147
|
+
let loggedLocked = false;
|
|
148
|
+
while (true) {
|
|
149
|
+
try {
|
|
150
|
+
const release = await lockfile.lock(lockFile, {
|
|
151
|
+
// If this many milliseconds has elapsed since the lock mtime was last
|
|
152
|
+
// updated, proper-lockfile will delete it and attempt to acquire the
|
|
153
|
+
// lock again. This handles the case where a process holding the lock
|
|
154
|
+
// hard-crashed.
|
|
155
|
+
stale: 10_000,
|
|
156
|
+
// How frequently the mtime for the lock will be updated while it is
|
|
157
|
+
// being held. This should be some smallish factor of "stale" so that
|
|
158
|
+
// we're unlikely to appear stale when we're actually still working on
|
|
159
|
+
// the script.
|
|
160
|
+
update: 2000,
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
return await workFn();
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await release();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.code === 'ELOCKED') {
|
|
171
|
+
if (!loggedLocked) {
|
|
172
|
+
// Only log this once.
|
|
173
|
+
this._logger.log({
|
|
174
|
+
script: this._config,
|
|
175
|
+
type: 'info',
|
|
176
|
+
detail: 'locked',
|
|
177
|
+
});
|
|
178
|
+
loggedLocked = true;
|
|
179
|
+
}
|
|
180
|
+
// Wait a moment before attempting to acquire the lock again.
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
182
|
+
if (this.#shouldNotStart) {
|
|
183
|
+
return { ok: false, error: [this.#startCancelledEvent] };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check whether the given fingerprint matches the current one from the
|
|
194
|
+
* `.wireit` directory.
|
|
195
|
+
*/
|
|
196
|
+
async #fingerprintIsFresh(fingerprint) {
|
|
197
|
+
if (!fingerprint.data.fullyTracked) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const prevFingerprint = await this.#readPreviousFingerprint();
|
|
201
|
+
return prevFingerprint !== undefined && fingerprint.equal(prevFingerprint);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Handle the outcome where the script is already fresh.
|
|
205
|
+
*/
|
|
206
|
+
#handleFresh(fingerprint) {
|
|
207
|
+
this._logger.log({
|
|
208
|
+
script: this._config,
|
|
209
|
+
type: 'success',
|
|
210
|
+
reason: 'fresh',
|
|
211
|
+
});
|
|
212
|
+
return { ok: true, value: fingerprint };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Handle the outcome where the script was stale and we got a cache hit.
|
|
216
|
+
*/
|
|
217
|
+
async #handleCacheHit(cacheHit, fingerprint) {
|
|
218
|
+
// Optimization: early signal that services are not needed while we're still
|
|
219
|
+
// restoring from cache.
|
|
220
|
+
this._servicesNotNeeded.resolve();
|
|
221
|
+
// Delete the fingerprint and other files. It's important we do this before
|
|
222
|
+
// restoring from cache, because we don't want to think that the previous
|
|
223
|
+
// fingerprint is still valid when it no longer is.
|
|
224
|
+
await this.#prepareDataDir();
|
|
225
|
+
// If we are restoring from cache, we should always delete existing output.
|
|
226
|
+
// The purpose of "clean:false" and "clean:if-file-deleted" is to allow
|
|
227
|
+
// tools with incremental build (like tsc --build) to work.
|
|
228
|
+
//
|
|
229
|
+
// However, this only applies when the tool is able to observe each
|
|
230
|
+
// incremental change to the input files. When we restore from cache, we are
|
|
231
|
+
// directly replacing the output files, and not invoking the tool at all, so
|
|
232
|
+
// there is no way for the tool to do any cleanup.
|
|
233
|
+
await this.#cleanOutput();
|
|
234
|
+
await cacheHit.apply();
|
|
235
|
+
this.#state = 'after-running';
|
|
236
|
+
const writeFingerprintPromise = this.#writeFingerprintFile(fingerprint);
|
|
237
|
+
const outputFilesAfterRunning = await this.#globOutputFilesAfterRunning();
|
|
238
|
+
if (!outputFilesAfterRunning.ok) {
|
|
239
|
+
return { ok: false, error: [outputFilesAfterRunning.error] };
|
|
240
|
+
}
|
|
241
|
+
if (outputFilesAfterRunning.value !== undefined) {
|
|
242
|
+
const outputManifest = await this.#computeOutputManifest(outputFilesAfterRunning.value);
|
|
243
|
+
if (!outputManifest.ok) {
|
|
244
|
+
return { ok: false, error: [outputManifest.error] };
|
|
245
|
+
}
|
|
246
|
+
await this.#writeOutputManifest(outputManifest.value);
|
|
247
|
+
}
|
|
248
|
+
await writeFingerprintPromise;
|
|
249
|
+
this._logger.log({
|
|
250
|
+
script: this._config,
|
|
251
|
+
type: 'success',
|
|
252
|
+
reason: 'cached',
|
|
253
|
+
});
|
|
254
|
+
return { ok: true, value: fingerprint };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Handle the outcome where the script was stale and we need to run it.
|
|
258
|
+
*/
|
|
259
|
+
async #handleNeedsRun(fingerprint) {
|
|
260
|
+
// Check if we should clean before we delete the fingerprint file, because
|
|
261
|
+
// we sometimes need to read the previous fingerprint file to determine
|
|
262
|
+
// this.
|
|
263
|
+
const shouldClean = await this.#shouldClean(fingerprint);
|
|
264
|
+
// Delete the fingerprint and other files. It's important we do this before
|
|
265
|
+
// starting the command, because we don't want to think that the previous
|
|
266
|
+
// fingerprint is still valid when it no longer is.
|
|
267
|
+
await this.#prepareDataDir();
|
|
268
|
+
if (shouldClean) {
|
|
269
|
+
const result = await this.#cleanOutput();
|
|
270
|
+
if (!result.ok) {
|
|
271
|
+
return { ok: false, error: [result.error] };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const childResult = await this.#workerPool.run(async () => {
|
|
275
|
+
// Significant time could have elapsed since we last checked because of
|
|
276
|
+
// parallelism limits.
|
|
277
|
+
if (this.#shouldNotStart) {
|
|
278
|
+
return { ok: false, error: this.#startCancelledEvent };
|
|
279
|
+
}
|
|
280
|
+
let earlyServiceTermination;
|
|
281
|
+
if (this._config.services.length > 0) {
|
|
282
|
+
const servicesStarted = await this._startServices();
|
|
283
|
+
if (!servicesStarted.ok) {
|
|
284
|
+
return servicesStarted;
|
|
285
|
+
}
|
|
286
|
+
void this._anyServiceTerminated.then(() => {
|
|
287
|
+
if (this.#state === 'after-running') {
|
|
288
|
+
// This is expected after we're done.
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
earlyServiceTermination = {
|
|
292
|
+
script: this._config,
|
|
293
|
+
type: 'failure',
|
|
294
|
+
reason: 'dependency-service-exited-unexpectedly',
|
|
295
|
+
};
|
|
296
|
+
// Stop running. If a service we depend on is down, then we know we're
|
|
297
|
+
// in an invalid state too.
|
|
298
|
+
child.kill();
|
|
299
|
+
this._executor.notifyFailure();
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
this.#state = 'running';
|
|
303
|
+
this._logger.log({
|
|
304
|
+
script: this._config,
|
|
305
|
+
type: 'info',
|
|
306
|
+
detail: 'running',
|
|
307
|
+
});
|
|
308
|
+
const child = new ScriptChildProcess(
|
|
309
|
+
// Unfortunately TypeScript doesn't automatically narrow this type
|
|
310
|
+
// based on the undefined-command check we did just above.
|
|
311
|
+
this._config);
|
|
312
|
+
void this._executor.shouldKillRunningScripts.then(() => {
|
|
313
|
+
child.kill();
|
|
314
|
+
});
|
|
315
|
+
child.stdout.on('data', (data) => {
|
|
316
|
+
this._logger.log({
|
|
317
|
+
script: this._config,
|
|
318
|
+
type: 'output',
|
|
319
|
+
stream: 'stdout',
|
|
320
|
+
data,
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
child.stderr.on('data', (data) => {
|
|
324
|
+
this._logger.log({
|
|
325
|
+
script: this._config,
|
|
326
|
+
type: 'output',
|
|
327
|
+
stream: 'stderr',
|
|
328
|
+
data,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
const result = await child.completed;
|
|
332
|
+
if (result.ok) {
|
|
333
|
+
if (earlyServiceTermination !== undefined) {
|
|
334
|
+
return { ok: false, error: earlyServiceTermination };
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this._logger.log({
|
|
338
|
+
script: this._config,
|
|
339
|
+
type: 'success',
|
|
340
|
+
reason: 'exit-zero',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
this._logger.log(result.error);
|
|
346
|
+
// This failure will propagate to the Executor eventually anyway, but
|
|
347
|
+
// asynchronously.
|
|
348
|
+
//
|
|
349
|
+
// The problem with that is that when parallelism is constrained, the
|
|
350
|
+
// next script waiting on this WorkerPool might start running before
|
|
351
|
+
// the failure information propagates, because returning from this
|
|
352
|
+
// function immediately unblocks the next worker.
|
|
353
|
+
//
|
|
354
|
+
// By directly notifying the Executor about the failure while we are
|
|
355
|
+
// still inside the WorkerPool callback, we prevent this race
|
|
356
|
+
// condition.
|
|
357
|
+
this._executor.notifyFailure();
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
});
|
|
361
|
+
this.#state = 'after-running';
|
|
362
|
+
if (!childResult.ok) {
|
|
363
|
+
this._executor.registerWatchIterationFailure(this._config, fingerprint);
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error: Array.isArray(childResult.error)
|
|
367
|
+
? childResult.error
|
|
368
|
+
: [childResult.error],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Optimization: early signal that services are no longer needed while we're
|
|
372
|
+
// still writing the fingerprint file etc.
|
|
373
|
+
this._servicesNotNeeded.resolve();
|
|
374
|
+
const writeFingerprintPromise = this.#writeFingerprintFile(fingerprint);
|
|
375
|
+
const outputFilesAfterRunning = await this.#globOutputFilesAfterRunning();
|
|
376
|
+
if (!outputFilesAfterRunning.ok) {
|
|
377
|
+
return { ok: false, error: [outputFilesAfterRunning.error] };
|
|
378
|
+
}
|
|
379
|
+
if (outputFilesAfterRunning.value !== undefined) {
|
|
380
|
+
const outputManifest = await this.#computeOutputManifest(outputFilesAfterRunning.value);
|
|
381
|
+
if (!outputManifest.ok) {
|
|
382
|
+
return { ok: false, error: [outputManifest.error] };
|
|
383
|
+
}
|
|
384
|
+
await this.#writeOutputManifest(outputManifest.value);
|
|
385
|
+
}
|
|
386
|
+
await writeFingerprintPromise;
|
|
387
|
+
if (fingerprint.data.fullyTracked) {
|
|
388
|
+
const result = await this.#saveToCacheIfPossible(fingerprint);
|
|
389
|
+
if (!result.ok) {
|
|
390
|
+
return { ok: false, error: [result.error] };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return { ok: true, value: fingerprint };
|
|
394
|
+
}
|
|
395
|
+
async #shouldClean(fingerprint) {
|
|
396
|
+
const cleanValue = this._config.clean;
|
|
397
|
+
switch (cleanValue) {
|
|
398
|
+
case true: {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
case false: {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
case 'if-file-deleted': {
|
|
405
|
+
const prevFingerprint = await this.#readPreviousFingerprint();
|
|
406
|
+
if (prevFingerprint === undefined) {
|
|
407
|
+
// If we don't know the previous fingerprint, then we can't know
|
|
408
|
+
// whether any input files were removed. It's safer to err on the
|
|
409
|
+
// side of cleaning.
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
return this.#anyInputFilesDeletedSinceLastRun(fingerprint, prevFingerprint);
|
|
413
|
+
}
|
|
414
|
+
default: {
|
|
415
|
+
throw new Error(`Unhandled clean setting: ${unreachable(cleanValue)}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Compares the current set of input file names to the previous set of input
|
|
421
|
+
* file names, and returns whether any files have been removed.
|
|
422
|
+
*/
|
|
423
|
+
#anyInputFilesDeletedSinceLastRun(curFingerprint, prevFingerprint) {
|
|
424
|
+
const curFiles = Object.keys(curFingerprint.data.files);
|
|
425
|
+
const prevFiles = Object.keys(prevFingerprint.data.files);
|
|
426
|
+
if (curFiles.length < prevFiles.length) {
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
const newFilesSet = new Set(curFiles);
|
|
430
|
+
for (const oldFile of prevFiles) {
|
|
431
|
+
if (!newFilesSet.has(oldFile)) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Save the current output files to the configured cache if possible.
|
|
439
|
+
*/
|
|
440
|
+
async #saveToCacheIfPossible(fingerprint) {
|
|
441
|
+
if (this.#cache === undefined) {
|
|
442
|
+
return { ok: true, value: undefined };
|
|
443
|
+
}
|
|
444
|
+
const paths = await this.#globOutputFilesAfterRunning();
|
|
445
|
+
if (!paths.ok) {
|
|
446
|
+
return paths;
|
|
447
|
+
}
|
|
448
|
+
if (paths.value === undefined) {
|
|
449
|
+
return { ok: true, value: undefined };
|
|
450
|
+
}
|
|
451
|
+
await this.#cache.set(this._config, fingerprint, paths.value);
|
|
452
|
+
return { ok: true, value: undefined };
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Glob the output files for this script and cache them, but throw unless the
|
|
456
|
+
* script has not yet started running or been restored from cache.
|
|
457
|
+
*/
|
|
458
|
+
#globOutputFilesBeforeRunning() {
|
|
459
|
+
this.#ensureState('before-running');
|
|
460
|
+
return (this.#cachedOutputFilesBeforeRunning ??= this.#globOutputFiles());
|
|
461
|
+
}
|
|
462
|
+
#cachedOutputFilesBeforeRunning;
|
|
463
|
+
/**
|
|
464
|
+
* Glob the output files for this script and cache them, but throw unless the
|
|
465
|
+
* script has finished running or been restored from cache.
|
|
466
|
+
*/
|
|
467
|
+
#globOutputFilesAfterRunning() {
|
|
468
|
+
this.#ensureState('after-running');
|
|
469
|
+
return (this.#cachedOutputFilesAfterRunning ??= this.#globOutputFiles());
|
|
470
|
+
}
|
|
471
|
+
#cachedOutputFilesAfterRunning;
|
|
472
|
+
/**
|
|
473
|
+
* Glob the output files for this script, or return undefined if output files
|
|
474
|
+
* are not defined.
|
|
475
|
+
*/
|
|
476
|
+
async #globOutputFiles() {
|
|
477
|
+
if (this._config.output === undefined) {
|
|
478
|
+
return { ok: true, value: undefined };
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
value: await glob(this._config.output.values, {
|
|
484
|
+
cwd: this._config.packageDir,
|
|
485
|
+
followSymlinks: false,
|
|
486
|
+
includeDirectories: true,
|
|
487
|
+
expandDirectories: true,
|
|
488
|
+
throwIfOutsideCwd: true,
|
|
489
|
+
}),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
catch (error) {
|
|
493
|
+
if (error instanceof GlobOutsideCwdError) {
|
|
494
|
+
// TODO(aomarks) It would be better to do this in the Analyzer by
|
|
495
|
+
// looking at the output glob patterns. See
|
|
496
|
+
// https://github.com/google/wireit/issues/64.
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
error: {
|
|
500
|
+
type: 'failure',
|
|
501
|
+
reason: 'invalid-config-syntax',
|
|
502
|
+
script: this._config,
|
|
503
|
+
diagnostic: {
|
|
504
|
+
severity: 'error',
|
|
505
|
+
message: `Output files must be within the package: ${error.message}`,
|
|
506
|
+
location: {
|
|
507
|
+
file: this._config.declaringFile,
|
|
508
|
+
range: {
|
|
509
|
+
offset: this._config.output.node.offset,
|
|
510
|
+
length: this._config.output.node.length,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Get the directory name where Wireit data can be saved for this script.
|
|
522
|
+
*/
|
|
523
|
+
get #dataDir() {
|
|
524
|
+
return getScriptDataDir(this._config);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get the path where the current fingerprint is saved for this script.
|
|
528
|
+
*/
|
|
529
|
+
get #fingerprintFilePath() {
|
|
530
|
+
return pathlib.join(this.#dataDir, 'fingerprint');
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Read this script's previous fingerprint from `fingerprint` file in the
|
|
534
|
+
* `.wireit` directory. Cached after first call.
|
|
535
|
+
*/
|
|
536
|
+
async #readPreviousFingerprint() {
|
|
537
|
+
if (this.#cachedPreviousFingerprint === undefined) {
|
|
538
|
+
this.#cachedPreviousFingerprint = (async () => {
|
|
539
|
+
try {
|
|
540
|
+
return Fingerprint.fromString((await fs.readFile(this.#fingerprintFilePath, 'utf8')));
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
if (error.code === 'ENOENT') {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
})();
|
|
549
|
+
}
|
|
550
|
+
return this.#cachedPreviousFingerprint;
|
|
551
|
+
}
|
|
552
|
+
#cachedPreviousFingerprint;
|
|
553
|
+
/**
|
|
554
|
+
* Write this script's fingerprint file.
|
|
555
|
+
*/
|
|
556
|
+
async #writeFingerprintFile(fingerprint) {
|
|
557
|
+
await fs.mkdir(this.#dataDir, { recursive: true });
|
|
558
|
+
await fs.writeFile(this.#fingerprintFilePath, fingerprint.string, 'utf8');
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Delete the fingerprint and other files for this script from the previous
|
|
562
|
+
* run, and ensure the data directory exists.
|
|
563
|
+
*/
|
|
564
|
+
async #prepareDataDir() {
|
|
565
|
+
await Promise.all([
|
|
566
|
+
fs.rm(this.#fingerprintFilePath, { force: true }),
|
|
567
|
+
fs.mkdir(this.#dataDir, { recursive: true }),
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Delete all files matched by this script's "output" glob patterns.
|
|
572
|
+
*/
|
|
573
|
+
async #cleanOutput() {
|
|
574
|
+
const files = await this.#globOutputFilesBeforeRunning();
|
|
575
|
+
if (!files.ok) {
|
|
576
|
+
return files;
|
|
577
|
+
}
|
|
578
|
+
if (files.value === undefined) {
|
|
579
|
+
return { ok: true, value: undefined };
|
|
580
|
+
}
|
|
581
|
+
await deleteEntries(files.value);
|
|
582
|
+
return { ok: true, value: undefined };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Compute the output manifest for this script, which is the sorted list of
|
|
586
|
+
* all output filenames, along with filesystem metadata that we assume is good
|
|
587
|
+
* enough for checking that a file hasn't changed: ctime, mtime, and bytes.
|
|
588
|
+
*/
|
|
589
|
+
async #computeOutputManifest(outputEntries) {
|
|
590
|
+
outputEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
591
|
+
const stats = [];
|
|
592
|
+
const deleted = [];
|
|
593
|
+
await Promise.all(outputEntries.map(async (entry, i) => {
|
|
594
|
+
try {
|
|
595
|
+
stats[i] = await fs.lstat(entry.path);
|
|
596
|
+
}
|
|
597
|
+
catch (e) {
|
|
598
|
+
if (e.code === 'ENOENT') {
|
|
599
|
+
deleted.push(entry.path);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
throw e;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}));
|
|
606
|
+
if (deleted.length > 0) {
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
error: {
|
|
610
|
+
type: 'failure',
|
|
611
|
+
reason: 'output-file-deleted-unexpectedly',
|
|
612
|
+
script: this._config,
|
|
613
|
+
filePaths: deleted.sort(),
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const manifest = {};
|
|
618
|
+
for (let i = 0; i < outputEntries.length; i++) {
|
|
619
|
+
manifest[outputEntries[i].path] = computeManifestEntry(stats[i]);
|
|
620
|
+
}
|
|
621
|
+
return { ok: true, value: JSON.stringify(manifest) };
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Check whether the current manifest of output files matches the one from the
|
|
625
|
+
* `.wireit` directory.
|
|
626
|
+
*/
|
|
627
|
+
async #outputManifestIsFresh() {
|
|
628
|
+
const oldManifestPromise = this.#readPreviousOutputManifest();
|
|
629
|
+
const outputFilesBeforeRunning = await this.#globOutputFilesBeforeRunning();
|
|
630
|
+
if (!outputFilesBeforeRunning.ok) {
|
|
631
|
+
return outputFilesBeforeRunning;
|
|
632
|
+
}
|
|
633
|
+
if (outputFilesBeforeRunning.value === undefined) {
|
|
634
|
+
return { ok: true, value: false };
|
|
635
|
+
}
|
|
636
|
+
const newManifest = await this.#computeOutputManifest(outputFilesBeforeRunning.value);
|
|
637
|
+
if (!newManifest.ok) {
|
|
638
|
+
return newManifest;
|
|
639
|
+
}
|
|
640
|
+
const oldManifest = await oldManifestPromise;
|
|
641
|
+
if (oldManifest === undefined) {
|
|
642
|
+
return { ok: true, value: false };
|
|
643
|
+
}
|
|
644
|
+
const equal = newManifest.value === oldManifest;
|
|
645
|
+
if (!equal) {
|
|
646
|
+
this._logger.log({
|
|
647
|
+
script: this._config,
|
|
648
|
+
type: 'info',
|
|
649
|
+
detail: 'output-modified',
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return { ok: true, value: equal };
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Read this script's previous output manifest file from the `manifest` file
|
|
656
|
+
* in the `.wireit` directory. Not cached.
|
|
657
|
+
*/
|
|
658
|
+
async #readPreviousOutputManifest() {
|
|
659
|
+
try {
|
|
660
|
+
return (await fs.readFile(this.#outputManifestFilePath, 'utf8'));
|
|
661
|
+
}
|
|
662
|
+
catch (error) {
|
|
663
|
+
if (error.code === 'ENOENT') {
|
|
664
|
+
return undefined;
|
|
665
|
+
}
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Write this script's output manifest file.
|
|
671
|
+
*/
|
|
672
|
+
async #writeOutputManifest(outputManifest) {
|
|
673
|
+
await fs.mkdir(this.#dataDir, { recursive: true });
|
|
674
|
+
await fs.writeFile(this.#outputManifestFilePath, outputManifest, 'utf8');
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Get the path where the current output manifest is saved for this script.
|
|
678
|
+
*/
|
|
679
|
+
get #outputManifestFilePath() {
|
|
680
|
+
return pathlib.join(this.#dataDir, 'manifest');
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
//# sourceMappingURL=standard.js.map
|