@andersbakken/fisk 5.0.4 → 5.0.7

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.
@@ -58,88 +58,191 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
58
58
  };
59
59
 
60
60
  const execFileAsync = require$$4.promisify(child_process.execFile);
61
- // Prefixes removed from a compiler's `-v` output before hashing / parsing.
62
- // Must match filter() in src/client/Client.cpp (lines 234-257).
63
- const FILTER_PREFIXES = [
64
- "COLLECT_",
65
- "InstalledDir: ",
66
- "Found candidate GCC installation: ",
67
- "Selected GCC installation: "
61
+ // Fingerprinting strategy:
62
+ //
63
+ // A compiler's "identity" for distributed-compile purposes is the set of
64
+ // behaviours that determine what code the frontend accepts and what the
65
+ // backend produces. It is NOT the bytes of the driver executable, because
66
+ // GCC and Clang bake absolute install paths into the driver at build time
67
+ // (STANDARD_EXEC_PREFIX / GCC_INSTALL_PREFIX / CLANG_RESOURCE_DIR /
68
+ // DEFAULT_SYSROOT / ...). Two machines that installed the same conan
69
+ // package for llvm end up with byte-different driver binaries whose paths
70
+ // point into per-machine conan caches, but the compilers are functionally
71
+ // identical. A file hash would say they are different; the scheduler would
72
+ // then be unable to match clients to builders.
73
+ //
74
+ // Instead, we hash the compiler's answers to a small set of probes that
75
+ // (a) are switch-independent, (b) do not embed absolute paths, and
76
+ // (c) fully determine frontend behaviour:
77
+ //
78
+ // -dumpmachine default target triple
79
+ // -dumpversion version number
80
+ // -x c -E -dM /dev/null all builtin macros for C
81
+ // -x c++ -E -dM /dev/null all builtin macros for C++
82
+ //
83
+ // The macro dumps include __clang_version__ / __GNUC__ / __GNUC_MINOR__ /
84
+ // __GNUC_PATCHLEVEL__ / __VERSION__ / target width macros / feature-test
85
+ // macros. Those strings are frozen at compiler-build time, not install
86
+ // time, so they are identical across machines that installed the same
87
+ // compiler package.
88
+ const PROBE_TIMEOUT_MS = 10000;
89
+ const PROBE_MAX_BUFFER = 4 * 1024 * 1024;
90
+ const PROBES = [
91
+ { label: "dumpmachine", args: ["-dumpmachine"], required: true },
92
+ { label: "dumpversion", args: ["-dumpversion"], required: true },
93
+ { label: "dumpfullversion", args: ["-dumpfullversion"], required: false },
94
+ { label: "builtins-c", args: ["-x", "c", "-E", "-dM", "/dev/null"], required: true },
95
+ { label: "builtins-cxx", args: ["-x", "c++", "-E", "-dM", "/dev/null"], required: true }
68
96
  ];
69
- // Remove any line that starts with one of the filter prefixes.
70
- // Equivalent to the C++ filter() loop that removes needle from line-starts only.
71
- function filterOutput(output) {
72
- let result = output;
73
- for (const needle of FILTER_PREFIXES) {
74
- const lines = result.split("\n");
75
- const kept = [];
76
- for (const line of lines) {
77
- if (!line.startsWith(needle)) {
78
- kept.push(line);
97
+ function runProbe(exec, probe) {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ try {
100
+ const { stdout, stderr } = yield execFileAsync(exec, [...probe.args], {
101
+ timeout: PROBE_TIMEOUT_MS,
102
+ maxBuffer: PROBE_MAX_BUFFER
103
+ });
104
+ return `${stdout}${stderr}`;
105
+ }
106
+ catch (err) {
107
+ if (probe.required) {
108
+ throw new Error(`Probe '${probe.label}' failed for ${exec}: ${err instanceof Error ? err.message : String(err)}`);
79
109
  }
110
+ return null;
80
111
  }
81
- result = kept.join("\n");
82
- }
83
- return result;
112
+ });
84
113
  }
85
- // Emulate sscanf cascade "%d.%d.%d" -> "%d.%d" -> "%d".
86
- function parseVersion(suffix) {
87
- const three = /^(\d+)\.(\d+)\.(\d+)/.exec(suffix);
114
+ // Emulate the C++ sscanf cascade "%d.%d.%d" -> "%d.%d" -> "%d".
115
+ function parseVersion(text) {
116
+ const three = /^(\d+)\.(\d+)\.(\d+)/.exec(text);
88
117
  if (three) {
89
- return {
90
- major: parseInt(three[1], 10),
91
- minor: parseInt(three[2], 10),
92
- patch: parseInt(three[3], 10)
93
- };
118
+ return { major: parseInt(three[1], 10), minor: parseInt(three[2], 10), patch: parseInt(three[3], 10) };
94
119
  }
95
- const two = /^(\d+)\.(\d+)/.exec(suffix);
120
+ const two = /^(\d+)\.(\d+)/.exec(text);
96
121
  if (two) {
97
122
  return { major: parseInt(two[1], 10), minor: parseInt(two[2], 10), patch: 0 };
98
123
  }
99
- const one = /^(\d+)/.exec(suffix);
124
+ const one = /^(\d+)/.exec(text);
100
125
  if (one) {
101
126
  return { major: parseInt(one[1], 10), minor: 0, patch: 0 };
102
127
  }
103
128
  return { major: 0, minor: 0, patch: 0 };
104
129
  }
105
- // Byte-for-byte port of createCompilerInfo() in src/client/Client.cpp (lines 290-341).
106
- function createCompilerInfo(exec, versionInfo) {
107
- let type = "unknown";
108
- let input = "";
109
- let version = { major: 0, minor: 0, patch: 0 };
110
- let foundVersion = false;
111
- const lines = versionInfo.split("\n");
112
- for (const line of lines) {
113
- if (line.startsWith("gcc version ")) {
114
- type = "gcc";
115
- const suffix = line.substring(12);
116
- input += suffix;
117
- version = parseVersion(suffix);
118
- foundVersion = true;
119
- }
120
- else if (line.startsWith("clang version ")) {
121
- type = "clang";
122
- const suffix = line.substring(14);
123
- input += suffix;
124
- version = parseVersion(suffix);
125
- foundVersion = true;
130
+ function detectTypeFromMacros(macros) {
131
+ // clang defines __clang__ even under GCC compatibility mode.
132
+ if (/^#define __clang__ /m.test(macros)) {
133
+ return "clang";
134
+ }
135
+ // GCC defines __GNUC__ but so does clang; require __GNUC__ *without* __clang__.
136
+ if (/^#define __GNUC__ /m.test(macros)) {
137
+ return "gcc";
138
+ }
139
+ return "unknown";
140
+ }
141
+ function macroValue(macros, name) {
142
+ const m = new RegExp(`^#define ${name} (.*)$`, "m").exec(macros);
143
+ return m ? m[1].trim() : null;
144
+ }
145
+ function stripQuotes(s) {
146
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
147
+ return s.substring(1, s.length - 1);
148
+ }
149
+ return s;
150
+ }
151
+ // Extract a version tuple from the compiler's own macros. This is stable
152
+ // across install locations because these macros are frozen at compiler
153
+ // build time.
154
+ function versionFromMacros(macros, type) {
155
+ if (type === "clang") {
156
+ const v = macroValue(macros, "__clang_version__");
157
+ if (v) {
158
+ return parseVersion(stripQuotes(v));
126
159
  }
127
- else if (line.startsWith("Target: ")) {
128
- const suffix = line.substring(8);
129
- input += suffix;
160
+ const major = macroValue(macros, "__clang_major__");
161
+ const minor = macroValue(macros, "__clang_minor__");
162
+ const patch = macroValue(macros, "__clang_patchlevel__");
163
+ if (major !== null) {
164
+ return {
165
+ major: parseInt(major, 10) || 0,
166
+ minor: minor !== null ? parseInt(minor, 10) || 0 : 0,
167
+ patch: patch !== null ? parseInt(patch, 10) || 0 : 0
168
+ };
130
169
  }
131
170
  }
132
- if (!foundVersion) {
133
- const lower = exec.toLowerCase();
134
- if (lower.indexOf("clang") !== -1) {
135
- type = "clang";
136
- }
137
- else if (lower.indexOf("gcc") !== -1) {
138
- type = "gcc";
171
+ if (type === "gcc") {
172
+ const major = macroValue(macros, "__GNUC__");
173
+ const minor = macroValue(macros, "__GNUC_MINOR__");
174
+ const patch = macroValue(macros, "__GNUC_PATCHLEVEL__");
175
+ if (major !== null) {
176
+ return {
177
+ major: parseInt(major, 10) || 0,
178
+ minor: minor !== null ? parseInt(minor, 10) || 0 : 0,
179
+ patch: patch !== null ? parseInt(patch, 10) || 0 : 0
180
+ };
139
181
  }
140
182
  }
141
- const hash = crypto.createHash("sha1").update(input).digest("hex").toUpperCase();
142
- return { hash, input, type, version };
183
+ return { major: 0, minor: 0, patch: 0 };
184
+ }
185
+ function gatherProbes(exec) {
186
+ return __awaiter(this, void 0, void 0, function* () {
187
+ const results = yield Promise.all(PROBES.map((p) => runProbe(exec, p)));
188
+ const [dumpmachine, dumpversion, dumpfullversion, builtinsC, builtinsCxx] = results;
189
+ // The required probes cannot be null because runProbe would have thrown.
190
+ return {
191
+ dumpmachine: (dumpmachine !== null && dumpmachine !== void 0 ? dumpmachine : "").trim(),
192
+ dumpversion: (dumpversion !== null && dumpversion !== void 0 ? dumpversion : "").trim(),
193
+ dumpfullversion: dumpfullversion === null ? null : dumpfullversion.trim(),
194
+ builtinsC: builtinsC !== null && builtinsC !== void 0 ? builtinsC : "",
195
+ builtinsCxx: builtinsCxx !== null && builtinsCxx !== void 0 ? builtinsCxx : ""
196
+ };
197
+ });
198
+ }
199
+ // Build the canonical fingerprint blob whose SHA becomes the compiler hash.
200
+ // Fields are separated by NUL to avoid ambiguity if any probe output
201
+ // contains our field label as a substring. Field labels are included so
202
+ // that adding a new probe in a later version deterministically changes the
203
+ // hash for the same compiler (the label acts as a schema version bump).
204
+ function canonicalFingerprint(p) {
205
+ var _a;
206
+ const parts = [
207
+ "fisk-compiler-fingerprint-v1",
208
+ "dumpmachine",
209
+ p.dumpmachine,
210
+ "dumpversion",
211
+ p.dumpversion,
212
+ "dumpfullversion",
213
+ (_a = p.dumpfullversion) !== null && _a !== void 0 ? _a : "",
214
+ "builtins-c",
215
+ p.builtinsC,
216
+ "builtins-cxx",
217
+ p.builtinsCxx
218
+ ];
219
+ return Buffer.from(parts.join("\0"), "utf8");
220
+ }
221
+ function createCompilerInfo(exec) {
222
+ var _a;
223
+ return __awaiter(this, void 0, void 0, function* () {
224
+ const probes = yield gatherProbes(exec);
225
+ const type = detectTypeFromMacros(probes.builtinsC);
226
+ const versionFromMac = versionFromMacros(probes.builtinsC, type);
227
+ const version = versionFromMac.major !== 0
228
+ ? versionFromMac
229
+ : parseVersion((_a = probes.dumpfullversion) !== null && _a !== void 0 ? _a : probes.dumpversion);
230
+ const blob = canonicalFingerprint(probes);
231
+ const hash = crypto.createHash("sha1").update(blob).digest("hex").toUpperCase();
232
+ // `input` is retained for debug/traceability: it lets a human see what
233
+ // went into the hash without needing to re-probe the compiler. Keep it
234
+ // small: just the identifying strings, not the full macro dumps.
235
+ const input = [
236
+ `type=${type}`,
237
+ `target=${probes.dumpmachine}`,
238
+ `version=${version.major}.${version.minor}.${version.patch}`,
239
+ `dumpversion=${probes.dumpversion}`,
240
+ probes.dumpfullversion ? `dumpfullversion=${probes.dumpfullversion}` : ""
241
+ ]
242
+ .filter((s) => s.length > 0)
243
+ .join("\n");
244
+ return { hash, input, type, version };
245
+ });
143
246
  }
144
247
  class CompilerInfoCache {
145
248
  constructor() {
@@ -151,7 +254,9 @@ class CompilerInfoCache {
151
254
  if (typeof compilerPath !== "string" || compilerPath.length === 0) {
152
255
  throw new Error("CompilerInfoCache.get: compilerPath must be a non-empty string");
153
256
  }
154
- const absPath = path__default["default"].resolve(compilerPath);
257
+ // Resolve symlinks so that /usr/bin/clang and /usr/bin/clang-18
258
+ // (when the former is a symlink to the latter) share a cache entry.
259
+ const absPath = yield require$$1.promises.realpath(path__default["default"].resolve(compilerPath));
155
260
  const stat = yield require$$1.promises.stat(absPath);
156
261
  const key = `${absPath}:${stat.mtimeMs}`;
157
262
  const cached = this.cache.get(key);
@@ -168,28 +273,20 @@ class CompilerInfoCache {
168
273
  });
169
274
  this.pending.set(key, compute);
170
275
  // Clean up the pending map on both success and failure so a failed
171
- // lookup doesn't wedge the key forever. We attach a no-op catch on
172
- // the cleanup chain because the original rejection is already
173
- // surfaced through the returned `compute` promise.
276
+ // lookup doesn't wedge the key forever.
174
277
  compute
175
278
  .finally(() => {
176
279
  this.pending.delete(key);
177
280
  })
178
281
  .catch(() => {
179
- /* rejection observed by caller via the returned `compute` */
282
+ /* rejection observed by caller via the returned promise */
180
283
  });
181
284
  return compute;
182
285
  });
183
286
  }
184
287
  static compute(absPath) {
185
288
  return __awaiter(this, void 0, void 0, function* () {
186
- const { stdout, stderr } = yield execFileAsync(absPath, ["-v"], {
187
- timeout: 30000,
188
- maxBuffer: 4 * 1024 * 1024
189
- });
190
- const combined = `${stdout}${stderr}`;
191
- const filtered = filterOutput(combined);
192
- return createCompilerInfo(absPath, filtered);
289
+ return createCompilerInfo(absPath);
193
290
  });
194
291
  }
195
292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andersbakken/fisk",
3
- "version": "5.0.4",
3
+ "version": "5.0.7",
4
4
  "description": "Fisk, a distributed compile system",
5
5
  "scripts": {
6
6
  "lint": "eslint . --ext .ts",
@@ -57995,7 +57995,7 @@ const option = options({
57995
57995
  const common = common$1(option);
57996
57996
  let nextCommandId = 0;
57997
57997
  const server = new Server(option, common.Version);
57998
- const clientMinimumVersion = "4.0.69";
57998
+ const clientMinimumVersion = "5.0.6";
57999
57999
  const serverStartTime = Date.now();
58000
58000
  process.on("unhandledRejection", (reason, p) => {
58001
58001
  console.error("Unhandled Rejection at: Promise", p, "reason:", reason === null || reason === void 0 ? void 0 : reason.stack);