diff --git a/src/build.js b/src/build.js index cc5ae5c..cb4c558 100644 --- a/src/build.js +++ b/src/build.js @@ -33,9 +33,18 @@ function runTrace(config, flowName, args, opts) { LOOPFINDER_ALLOW_NETWORK: opts.allowNetwork ? '1' : '', LOOPFINDER_ALLOW_SUBPROCESS: opts.allowSubprocess ? '1' : '', }; + // A record left by an earlier build must not pass for this run's: if the script fails to start, + // the old record would be used as if it were new, and declared loops would look broken. + fs.rmSync(cache, { force: true }); const r = spawnSync(py ? config.python : process.execPath, py ? [TRACE_PY, script, ...args.slice(1)] : ['-r', TRACE_JS, script, ...args.slice(1)], { cwd: config.root, env, encoding: 'utf8', timeout: opts.timeout || 600000 }); + // Windows ships a `python` that only points to the Microsoft Store (exit 9009), so a missing + // interpreter shows up either as a spawn error or as that message. + if (py && !fs.existsSync(cache) && (r.error?.code === 'ENOENT' || r.status === 9009 || /Python was not found/.test(r.stderr || ''))) { + throw new Error(`could not start Python to trace ${args[0]} ("python": ${JSON.stringify(config.python)} in the config).\n` + + 'Install Python 3, or set "python" in loopfinder/config.json to the interpreter (for example "py" or a full path).'); + } if (!fs.existsSync(cache)) { throw new Error(`trace did not finish: ${args.join(' ')} (exit ${r.status})\n${(r.stderr || '').slice(-2000)}`); } diff --git a/test/core.test.js b/test/core.test.js index af35f32..f8b9c6c 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -274,3 +274,23 @@ test('demo: builds the expected loops and leaves the workspace untouched', () => const feedNode = graph.nodes.find(n => n.id === 'web:Garden feed'); assert.ok(feedNode, 'the blocked fetch is still recorded and labelled'); }); + +test('build: a Python that cannot start is named, and an old record is not used in its place', () => { + const src = path.join(__dirname, '..', 'examples', 'demo'); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'loopfinder-nopy-')); + fs.cpSync(src, dir, { recursive: true }); + const cfgFile = path.join(dir, 'loopfinder', 'config.json'); + const cfg = JSON.parse(fs.readFileSync(cfgFile, 'utf8')); + const out = fs.mkdtempSync(path.join(os.tmpdir(), 'lf-nopy-out-')); + cfg.output = path.join(out, 'flow.json'); + cfg.cache = path.join(out, 'traces'); + cfg.python = 'loopfinder-no-such-python'; + fs.writeFileSync(cfgFile, JSON.stringify(cfg)); + // A record from an earlier build, as if Python had been there last time. + fs.mkdirSync(cfg.cache, { recursive: true }); + const stale = path.join(cfg.cache, 'reading__weekly_summary.py.json'); + fs.writeFileSync(stale, JSON.stringify({ reads: [], writes: [], network: [], subprocess: [] })); + assert.throws(() => build(loadConfig(cfgFile), { warn: () => {} }), + e => /could not start Python/.test(e.message) && e.message.includes('loopfinder-no-such-python')); + assert.ok(!fs.existsSync(stale), 'the old record was left to pass for this run'); +});