diff --git a/packages/builder/src/Compiler.ts b/packages/builder/src/Compiler.ts index c0b23a3..a390ad2 100755 --- a/packages/builder/src/Compiler.ts +++ b/packages/builder/src/Compiler.ts @@ -335,9 +335,13 @@ export class CompactCompiler { UIService.printOutput(summary, chalk.cyan); } - const cleanStderr = cleanForDisplay(execError.stderr); - if (cleanStderr) { - UIService.printOutput(cleanStderr, chalk.red); + // Under `script` the compiler's stderr is merged into the captured + // PTY output, so stdout is the only place the error text lives. + const details = + cleanForDisplay(execError.stderr) || + cleanForDisplay(execError.stdout); + if (details) { + UIService.printOutput(details, chalk.red); } } diff --git a/packages/builder/src/services/CompilerService.ts b/packages/builder/src/services/CompilerService.ts index 6477c3f..1ea9b3c 100644 --- a/packages/builder/src/services/CompilerService.ts +++ b/packages/builder/src/services/CompilerService.ts @@ -41,7 +41,10 @@ function tokenizeFlags(flags: string): string[] { * * Handles both macOS and Linux `script` syntax: * - macOS: `script -q [args...]` - * - Linux: `script -qc " [args...]" ` + * - Linux: `script -qec " [args...]" ` + * + * util-linux `script` exits 0 whatever the child did unless `-e` is given. + * BSD `script` on macOS already returns the child's status. * * Note: Circuit constraint output is only available when compiling WITHOUT * `--skip-zk`, as the k/rows values come from the ZK proving pass. When @@ -61,11 +64,11 @@ async function spawnWithPty( // macOS: script -q [args...] scriptArgs = ['-q', tmpFile, 'compact', ...args]; } else { - // Linux: script -qc "" + // Linux: script -qec "" const cmd = ['compact', ...args] .map((a) => (a.includes(' ') ? `'${a}'` : a)) .join(' '); - scriptArgs = ['-qc', cmd, tmpFile]; + scriptArgs = ['-qec', cmd, tmpFile]; } const proc = spawn('script', scriptArgs, { diff --git a/packages/builder/test/Compiler.test.ts b/packages/builder/test/Compiler.test.ts index 4287acf..d7bec5c 100644 --- a/packages/builder/test/Compiler.test.ts +++ b/packages/builder/test/Compiler.test.ts @@ -1153,6 +1153,45 @@ describe('CompactCompiler', () => { ); expect(testMockExec).toHaveBeenCalledTimes(4); }); + + it('prints the captured output when the failure carries no stderr', async () => { + mockReaddir.mockResolvedValue([ + { + name: 'MockElGamal.compact', + isFile: () => true, + isDirectory: () => false, + }, + ] as any); + mockExistsSync.mockReturnValue(true); + + const ptyError = new Error('compact exited with code 1') as Error & { + stdout: string; + stderr: string; + }; + ptyError.stdout = + 'Exception in thread "main" Unsupported test_eq: JubjubScalar == JubjubScalar\n'; + ptyError.stderr = ''; + + const testMockExec = vi + .fn() + .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: 'Compactc 0.26.0', stderr: '' }) + .mockRejectedValueOnce(ptyError); + + const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + compiler = new CompactCompiler({}, testMockExec); + + await expect(compiler.compile()).rejects.toThrow(CompilationError); + + expect(logSpy).toHaveBeenCalledWith( + expect.stringContaining( + 'Unsupported test_eq: JubjubScalar == JubjubScalar', + ), + ); + + logSpy.mockRestore(); + }); }); describe('Real-world scenarios', () => {