@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.
Files changed (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1062 -0
  3. package/bin/wireit.js +9 -0
  4. package/lib/analyzer.js +1600 -0
  5. package/lib/caching/cache.js +7 -0
  6. package/lib/caching/github-actions-cache.js +832 -0
  7. package/lib/caching/local-cache.js +78 -0
  8. package/lib/caching/shared-cache.js +256 -0
  9. package/lib/cli-options.js +495 -0
  10. package/lib/cli.js +177 -0
  11. package/lib/config.js +18 -0
  12. package/lib/error.js +160 -0
  13. package/lib/event.js +7 -0
  14. package/lib/execution/base.js +108 -0
  15. package/lib/execution/no-command.js +32 -0
  16. package/lib/execution/service.js +1017 -0
  17. package/lib/execution/standard.js +683 -0
  18. package/lib/executor.js +249 -0
  19. package/lib/fingerprint.js +164 -0
  20. package/lib/ide.js +583 -0
  21. package/lib/language-server.js +135 -0
  22. package/lib/logging/combination-logger.js +41 -0
  23. package/lib/logging/debug-logger.js +43 -0
  24. package/lib/logging/logger.js +38 -0
  25. package/lib/logging/metrics-logger.js +108 -0
  26. package/lib/logging/quiet/run-tracker.js +597 -0
  27. package/lib/logging/quiet/stack-map.js +41 -0
  28. package/lib/logging/quiet/writeover-line.js +197 -0
  29. package/lib/logging/quiet-logger.js +78 -0
  30. package/lib/logging/simple-logger.js +296 -0
  31. package/lib/logging/watch-logger.js +81 -0
  32. package/lib/script-child-process.js +270 -0
  33. package/lib/util/ast.js +71 -0
  34. package/lib/util/async-cache.js +24 -0
  35. package/lib/util/copy.js +120 -0
  36. package/lib/util/deferred.js +35 -0
  37. package/lib/util/delete.js +120 -0
  38. package/lib/util/dispose.js +16 -0
  39. package/lib/util/fs.js +258 -0
  40. package/lib/util/glob.js +255 -0
  41. package/lib/util/line-monitor.js +69 -0
  42. package/lib/util/manifest.js +31 -0
  43. package/lib/util/optimize-mkdirs.js +55 -0
  44. package/lib/util/package-json-reader.js +61 -0
  45. package/lib/util/package-json.js +179 -0
  46. package/lib/util/script-data-dir.js +19 -0
  47. package/lib/util/shuffle.js +16 -0
  48. package/lib/util/unreachable.js +12 -0
  49. package/lib/util/windows.js +87 -0
  50. package/lib/util/worker-pool.js +61 -0
  51. package/lib/watcher.js +396 -0
  52. package/package.json +470 -0
  53. package/schema.json +132 -0
  54. package/wireit.svg +1 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { NoCommandScriptExecution } from './execution/no-command.js';
7
+ import { StandardScriptExecution } from './execution/standard.js';
8
+ import { ServiceScriptExecution } from './execution/service.js';
9
+ import { scriptReferenceToString, } from './config.js';
10
+ import { Deferred } from './util/deferred.js';
11
+ import { convertExceptionToFailure } from './error.js';
12
+ let executorConstructorHook;
13
+ /**
14
+ * For GC testing only. A function that is called whenever an Executor is
15
+ * constructed.
16
+ */
17
+ export function registerExecutorConstructorHook(fn) {
18
+ executorConstructorHook = fn;
19
+ }
20
+ /**
21
+ * Executes a script that has been analyzed and validated by the Analyzer.
22
+ */
23
+ export class Executor {
24
+ #rootConfig;
25
+ #executions = new Map();
26
+ #persistentServices = new Map();
27
+ #ephemeralServices = [];
28
+ #previousIterationServices;
29
+ #logger;
30
+ #workerPool;
31
+ #cache;
32
+ #isWatchMode;
33
+ #previousWatchIterationFailures;
34
+ /** Resolves when the first failure occurs in any script. */
35
+ #failureOccured = new Deferred();
36
+ /** Resolves when we decide that new scripts should not be started. */
37
+ #stopStartingNewScripts = new Deferred();
38
+ /** Resolves when we decide that running scripts should be killed. */
39
+ #killRunningScripts = new Deferred();
40
+ /** Resolves when we decide that services should be stopped. */
41
+ #stopServices = new Deferred();
42
+ constructor(rootConfig, logger, workerPool, cache, failureMode, previousIterationServices, isWatchMode, previousWatchIterationFailures) {
43
+ executorConstructorHook?.(this);
44
+ this.#rootConfig = rootConfig;
45
+ this.#logger = logger;
46
+ this.#workerPool = workerPool;
47
+ this.#cache = cache;
48
+ this.#previousIterationServices = previousIterationServices;
49
+ this.#isWatchMode = isWatchMode;
50
+ this.#previousWatchIterationFailures = previousWatchIterationFailures;
51
+ // If a failure occurs, then whether we stop starting new scripts or kill
52
+ // running ones depends on the failure mode setting.
53
+ void this.#failureOccured.promise.then(() => {
54
+ switch (failureMode) {
55
+ case 'continue': {
56
+ if (!this.#isWatchMode) {
57
+ this.#stopServices.resolve();
58
+ }
59
+ break;
60
+ }
61
+ case 'no-new': {
62
+ this.#stopStartingNewScripts.resolve();
63
+ if (!this.#isWatchMode) {
64
+ this.#stopServices.resolve();
65
+ }
66
+ break;
67
+ }
68
+ case 'kill': {
69
+ this.#stopStartingNewScripts.resolve();
70
+ this.#killRunningScripts.resolve();
71
+ this.#stopServices.resolve();
72
+ break;
73
+ }
74
+ default: {
75
+ const never = failureMode;
76
+ throw new Error(`Internal error: unexpected failure mode: ${String(never)}`);
77
+ }
78
+ }
79
+ });
80
+ }
81
+ /**
82
+ * If this entire execution is aborted because e.g. the user sent a SIGINT to
83
+ * the Wireit process, then dont start new scripts, and kill running ones.
84
+ */
85
+ abort() {
86
+ this.#stopStartingNewScripts.resolve();
87
+ this.#killRunningScripts.resolve();
88
+ this.#stopServices.resolve();
89
+ if (this.#previousIterationServices !== undefined) {
90
+ for (const service of this.#previousIterationServices.values()) {
91
+ void service.abort();
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Execute the root script.
97
+ */
98
+ async execute() {
99
+ if (this.#previousIterationServices !== undefined &&
100
+ this.#previousIterationServices.size > 0) {
101
+ // If any services were removed from the graph entirely, or used to be
102
+ // persistent but are no longer, then stop them now.
103
+ const currentPersistentServices = new Set();
104
+ for (const script of findAllScripts(this.#rootConfig)) {
105
+ if (script.service && script.isPersistent) {
106
+ currentPersistentServices.add(scriptReferenceToString(script));
107
+ }
108
+ }
109
+ const abortPromises = [];
110
+ for (const [key, service] of this.#previousIterationServices) {
111
+ if (!currentPersistentServices.has(key)) {
112
+ abortPromises.push(service.abort());
113
+ this.#previousIterationServices.delete(key);
114
+ }
115
+ }
116
+ await Promise.all(abortPromises);
117
+ }
118
+ const errors = [];
119
+ let rootExecutionResult;
120
+ try {
121
+ rootExecutionResult = await this.getExecution(this.#rootConfig).execute();
122
+ }
123
+ catch (error) {
124
+ rootExecutionResult = convertExceptionToFailure(error, this.#rootConfig);
125
+ }
126
+ if (!rootExecutionResult.ok) {
127
+ errors.push(...rootExecutionResult.error);
128
+ }
129
+ // Wait for all persistent services to start.
130
+ for (const service of this.#persistentServices.values()) {
131
+ // Persistent services start automatically, so calling start() here should
132
+ // be a no-op, but it lets us get the started promise.
133
+ const result = await service.start();
134
+ if (!result.ok) {
135
+ errors.push(result.error);
136
+ }
137
+ }
138
+ // Wait for all ephemeral services to have terminated (either started and
139
+ // stopped, or never needed to start).
140
+ const ephemeralServiceResults = await Promise.all(this.#ephemeralServices.map((service) => service.terminated));
141
+ for (const result of ephemeralServiceResults) {
142
+ if (!result.ok) {
143
+ errors.push(result.error);
144
+ }
145
+ }
146
+ // All previous services are either now adopted or stopped. Remove the
147
+ // reference to this map to allow for garbage collection, otherwise in watch
148
+ // mode we'll have a chain of references all the way back through every
149
+ // iteration.
150
+ this.#previousIterationServices = undefined;
151
+ return {
152
+ persistentServices: this.#persistentServices,
153
+ errors,
154
+ };
155
+ }
156
+ /**
157
+ * Signal that a script has failed, which will potentially stop starting or
158
+ * kill other scripts depending on the {@link FailureMode}.
159
+ *
160
+ * This method will be called automatically in the normal flow of execution,
161
+ * but scripts can also call it directly to synchronously signal a failure.
162
+ */
163
+ notifyFailure() {
164
+ this.#failureOccured.resolve();
165
+ }
166
+ /**
167
+ * Synchronously check if new scripts should stop being started.
168
+ */
169
+ get shouldStopStartingNewScripts() {
170
+ return this.#stopStartingNewScripts.settled;
171
+ }
172
+ /**
173
+ * A promise which resolves if we should kill running scripts.
174
+ */
175
+ get shouldKillRunningScripts() {
176
+ return this.#killRunningScripts.promise;
177
+ }
178
+ /**
179
+ * Get the execution instance for a script config, creating one if it doesn't
180
+ * already exist.
181
+ */
182
+ getExecution(config) {
183
+ const key = scriptReferenceToString(config);
184
+ let execution = this.#executions.get(key);
185
+ if (execution === undefined) {
186
+ if (config.command === undefined) {
187
+ execution = new NoCommandScriptExecution(config, this, this.#logger);
188
+ }
189
+ else if (config.service !== undefined) {
190
+ execution = new ServiceScriptExecution(config, this, this.#logger, this.#stopServices.promise, this.#previousIterationServices?.get(key), this.#isWatchMode);
191
+ if (config.isPersistent) {
192
+ this.#persistentServices.set(key, execution);
193
+ }
194
+ else {
195
+ this.#ephemeralServices.push(execution);
196
+ }
197
+ }
198
+ else {
199
+ execution = new StandardScriptExecution(config, this, this.#workerPool, this.#cache, this.#logger);
200
+ }
201
+ this.#executions.set(key, execution);
202
+ }
203
+ // Cast needed because our Map type doesn't know about the config ->
204
+ // execution type guarantees. We could make a smarter Map type, but not
205
+ // really worth it here.
206
+ return execution;
207
+ }
208
+ /**
209
+ * If we're in watch mode, check whether in the previous watch iteration the
210
+ * given script failed with the given fingerprint.
211
+ */
212
+ failedInPreviousWatchIteration(script, fingerprint) {
213
+ if (this.#previousWatchIterationFailures === undefined) {
214
+ return false;
215
+ }
216
+ const previous = this.#previousWatchIterationFailures.get(scriptReferenceToString(script));
217
+ if (previous === undefined) {
218
+ return false;
219
+ }
220
+ return previous.equal(fingerprint);
221
+ }
222
+ /**
223
+ * If we're in watch mode, record that a script failed for the purpose of
224
+ * preventing it from running unless its fingerprint changes in the next watch
225
+ * iteration.
226
+ */
227
+ registerWatchIterationFailure(script, fingerprint) {
228
+ this.#previousWatchIterationFailures?.set(scriptReferenceToString(script), fingerprint);
229
+ }
230
+ }
231
+ /**
232
+ * Walk the dependencies of the given root script and return all scripts in the
233
+ * graph (including the root itself).
234
+ */
235
+ function findAllScripts(root) {
236
+ const visited = new Set();
237
+ const stack = [root];
238
+ while (stack.length > 0) {
239
+ const next = stack.pop();
240
+ visited.add(next);
241
+ for (const dep of next.dependencies) {
242
+ if (!visited.has(dep.config)) {
243
+ stack.push(dep.config);
244
+ }
245
+ }
246
+ }
247
+ return visited;
248
+ }
249
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { createHash } from 'crypto';
7
+ import { createReadStream } from './util/fs.js';
8
+ import { glob } from './util/glob.js';
9
+ import { scriptReferenceToString } from './config.js';
10
+ /**
11
+ * The fingerprint of a script. Converts lazily between string and data object
12
+ * forms.
13
+ */
14
+ export class Fingerprint {
15
+ static fromString(string) {
16
+ const fingerprint = new Fingerprint();
17
+ fingerprint.#str = string;
18
+ return fingerprint;
19
+ }
20
+ /**
21
+ * Generate the fingerprint data object for a script based on its current
22
+ * configuration, input files, and the fingerprints of its dependencies.
23
+ */
24
+ static async compute(script, dependencyFingerprints) {
25
+ let allDependenciesAreFullyTracked = true;
26
+ const filteredDependencyFingerprints = [];
27
+ for (const [dep, depFingerprint] of dependencyFingerprints) {
28
+ if (!dep.cascade) {
29
+ // cascade: false means the fingerprint of the dependency isn't
30
+ // directly inherited.
31
+ continue;
32
+ }
33
+ if (!depFingerprint.data.fullyTracked) {
34
+ allDependenciesAreFullyTracked = false;
35
+ }
36
+ filteredDependencyFingerprints.push([
37
+ scriptReferenceToString(dep.config),
38
+ depFingerprint.hash,
39
+ ]);
40
+ }
41
+ let fileHashes;
42
+ if (script.files?.values.length) {
43
+ const files = await glob(script.files.values, {
44
+ cwd: script.packageDir,
45
+ followSymlinks: true,
46
+ // TODO(aomarks) This means that empty directories are not reflected in
47
+ // the fingerprint, however an empty directory could modify the behavior
48
+ // of a script. We should probably include empty directories; we'll just
49
+ // need special handling when we compute the fingerprint, because there
50
+ // is no hash we can compute.
51
+ includeDirectories: false,
52
+ // We must expand directories here, because we need the complete
53
+ // explicit list of files to hash.
54
+ expandDirectories: true,
55
+ throwIfOutsideCwd: false,
56
+ });
57
+ // TODO(aomarks) Instead of reading and hashing every input file on every
58
+ // build, use inode/mtime/ctime/size metadata (which is much faster to
59
+ // read) as a heuristic to detect files that have likely changed, and
60
+ // otherwise re-use cached hashes that we store in e.g.
61
+ // ".wireit/<script>/hashes".
62
+ const erroredFilePaths = [];
63
+ fileHashes = await Promise.all(files.map(async (file) => {
64
+ const absolutePath = file.path;
65
+ const hash = createHash('sha256');
66
+ try {
67
+ const stream = await createReadStream(absolutePath);
68
+ for await (const chunk of stream) {
69
+ hash.update(chunk);
70
+ }
71
+ }
72
+ catch (error) {
73
+ // It's possible for a file to be deleted between the
74
+ // time it is globbed and the time it is fingerprinted.
75
+ const { code } = error;
76
+ if (code !== /* does not exist */ 'ENOENT') {
77
+ throw error;
78
+ }
79
+ erroredFilePaths.push(absolutePath);
80
+ }
81
+ return [file.path, hash.digest('hex')];
82
+ }));
83
+ if (erroredFilePaths.length > 0) {
84
+ return {
85
+ ok: false,
86
+ error: {
87
+ type: 'failure',
88
+ reason: 'input-file-deleted-unexpectedly',
89
+ script: script,
90
+ filePaths: erroredFilePaths,
91
+ },
92
+ };
93
+ }
94
+ }
95
+ else {
96
+ fileHashes = [];
97
+ }
98
+ const fullyTracked =
99
+ // If any any dependency is not fully tracked, then we can't be either,
100
+ // because we can't know if there was an undeclared input that this script
101
+ // depends on.
102
+ allDependenciesAreFullyTracked &&
103
+ // A no-command script. Doesn't ever do anything itsef, so always fully
104
+ // tracked.
105
+ (script.command === undefined ||
106
+ // A service. Fully tracked if we know its inputs. Can't produce output.
107
+ (script.service !== undefined && script.files !== undefined) ||
108
+ // A standard script. Fully tracked if we know both its inputs and
109
+ // outputs.
110
+ (script.files !== undefined && script.output !== undefined));
111
+ const fingerprint = new Fingerprint();
112
+ // Note: The order of all fields is important so that we can do fast string
113
+ // comparison.
114
+ const data = {
115
+ fullyTracked,
116
+ platform: process.platform,
117
+ arch: process.arch,
118
+ nodeVersion: process.version,
119
+ command: script.command?.value,
120
+ extraArgs: script.extraArgs ?? [],
121
+ clean: script.clean,
122
+ files: Object.fromEntries(fileHashes.sort(([aFile], [bFile]) => aFile.localeCompare(bFile))),
123
+ output: script.output?.values ?? [],
124
+ dependencies: Object.fromEntries(filteredDependencyFingerprints.sort(([aRef], [bRef]) => aRef.localeCompare(bRef))),
125
+ service: script.service === undefined
126
+ ? undefined
127
+ : {
128
+ readyWhen: {
129
+ lineMatches: script.service.readyWhen.lineMatches?.toString(),
130
+ },
131
+ },
132
+ env: script.env,
133
+ };
134
+ fingerprint.#data = data;
135
+ return { ok: true, value: fingerprint };
136
+ }
137
+ #str;
138
+ #data;
139
+ #hash;
140
+ get string() {
141
+ if (this.#str === undefined) {
142
+ this.#str = JSON.stringify(this.#data);
143
+ }
144
+ return this.#str;
145
+ }
146
+ get data() {
147
+ if (this.#data === undefined) {
148
+ this.#data = JSON.parse(this.#str);
149
+ }
150
+ return this.#data;
151
+ }
152
+ get hash() {
153
+ if (this.#hash === undefined) {
154
+ this.#hash = createHash('sha256')
155
+ .update(this.string)
156
+ .digest('hex');
157
+ }
158
+ return this.#hash;
159
+ }
160
+ equal(other) {
161
+ return this.string === other.string;
162
+ }
163
+ }
164
+ //# sourceMappingURL=fingerprint.js.map