Skip to content

Commit e778994

Browse files
Wirasmleex279
andauthored
fix(build): use build-time constants for binary detection and pretty stream logger (#982)
* fix(build): use build-time constants for binary detection and pretty stream logger Replaces runtime detection of compiled binaries (env sniffing via import.meta.dir / process.execPath) with a build-time BUNDLED_IS_BINARY constant in @archon/paths/bundled-build.ts, rewritten by scripts/build-binaries.sh and restored on EXIT via a trap. Also rewrites @archon/paths/logger.ts to use pino-pretty as a destination stream instead of a worker-thread transport. The formatter now runs on the main thread, eliminating the require.resolve('pino-pretty') lookup that crashes inside Bun's /\$bunfs/ virtual filesystem in compiled binaries. The same code path runs in dev and binaries — no environment detection in the logger at all. isBinaryBuild() in @archon/workflows is kept as a one-line wrapper around BUNDLED_IS_BINARY so existing spyOn-based test mocking in loader.test.ts continues to work without modification. Closes #960 Closes #961 Closes #979 Supersedes #962 Supersedes #963 Co-Authored-By: leex279 <leex279@users.noreply.github.com> * style(workflows): hoist BUNDLED_IS_BINARY import to top of file * fix(build,logger): harden pretty init and trap restore - logger: wrap pino-pretty init in try/catch and fall back to JSON so a broken TTY or missing peer can't crash module load. - build-binaries.sh: drop '2>/dev/null || true' from the EXIT trap so a failed bundled-build.ts restore is visible instead of silently leaving the dev tree with BUNDLED_IS_BINARY=true. - bundled-defaults: unmark isBinaryBuild() @deprecated and document why the wrapper is the intentional test seam (mock.module pollution in Bun). --------- Co-authored-by: leex279 <leex279@users.noreply.github.com>
1 parent 472fd07 commit e778994

11 files changed

Lines changed: 114 additions & 97 deletions

File tree

bun.lock

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/src/commands/bundled-version.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

packages/cli/src/commands/version.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
/**
22
* Version command - displays version info
33
*
4-
* For compiled binaries, version and git commit are embedded via bundled-version.ts
5-
* For development (Bun), reads from package.json and retrieves git commit at runtime
4+
* For compiled binaries, version and git commit are embedded via `@archon/paths`
5+
* build-time constants (rewritten by `scripts/build-binaries.sh`).
6+
* For development (Bun), reads from package.json and retrieves git commit at runtime.
67
*/
78
import { readFile } from 'fs/promises';
89
import { join, dirname } from 'path';
910
import { fileURLToPath } from 'url';
1011
import { execFileAsync } from '@archon/git';
11-
import { createLogger } from '@archon/paths';
12+
import {
13+
BUNDLED_GIT_COMMIT,
14+
BUNDLED_IS_BINARY,
15+
BUNDLED_VERSION,
16+
createLogger,
17+
} from '@archon/paths';
1218
import { getDatabaseType } from '@archon/core';
13-
import { isBinaryBuild } from '@archon/workflows/defaults';
14-
import { BUNDLED_VERSION, BUNDLED_GIT_COMMIT } from './bundled-version';
1519

1620
const log = createLogger('cli:version');
1721

@@ -72,7 +76,7 @@ export async function versionCommand(): Promise<void> {
7276
let version: string;
7377
let gitCommit: string;
7478

75-
if (isBinaryBuild()) {
79+
if (BUNDLED_IS_BINARY) {
7680
// Compiled binary: use embedded version and commit
7781
version = BUNDLED_VERSION;
7882
gitCommit = BUNDLED_GIT_COMMIT;
@@ -86,7 +90,7 @@ export async function versionCommand(): Promise<void> {
8690
const platform = process.platform;
8791
const arch = process.arch;
8892
const dbType = getDatabaseType();
89-
const buildType = isBinaryBuild() ? 'binary' : 'source (bun)';
93+
const buildType = BUNDLED_IS_BINARY ? 'binary' : 'source (bun)';
9094

9195
console.log(`Archon CLI v${version}`);
9296
console.log(` Platform: ${platform}-${arch}`);

packages/paths/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212
"type-check": "bun x tsc --noEmit"
1313
},
1414
"dependencies": {
15-
"pino": "^9"
16-
},
17-
"optionalDependencies": {
15+
"pino": "^9",
1816
"pino-pretty": "^13"
1917
},
2018
"peerDependencies": {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it } from 'bun:test';
2+
import { BUNDLED_GIT_COMMIT, BUNDLED_IS_BINARY, BUNDLED_VERSION } from './bundled-build';
3+
4+
describe('bundled-build', () => {
5+
// In dev/test mode the placeholders must be the dev defaults.
6+
// `scripts/build-binaries.sh` rewrites this file only during binary
7+
// compilation and restores it afterwards via an EXIT trap.
8+
it('BUNDLED_IS_BINARY is false in dev mode', () => {
9+
expect(BUNDLED_IS_BINARY).toBe(false);
10+
});
11+
12+
it('BUNDLED_VERSION is the dev placeholder', () => {
13+
expect(BUNDLED_VERSION).toBe('dev');
14+
});
15+
16+
it('BUNDLED_GIT_COMMIT is the dev placeholder', () => {
17+
expect(BUNDLED_GIT_COMMIT).toBe('unknown');
18+
});
19+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Build-time constants embedded into compiled binaries.
3+
*
4+
* In dev/test mode, the placeholders below are used and BUNDLED_IS_BINARY
5+
* is `false`. Compiled binaries get this file overwritten by
6+
* `scripts/build-binaries.sh` before `bun build --compile` is invoked,
7+
* and restored afterwards via an EXIT trap.
8+
*
9+
* Lives in `@archon/paths` (the bottom of the dep graph) so any package
10+
* can import these constants without creating dependency cycles.
11+
*
12+
* See GitHub issue #979 for the rationale (replaces runtime detection
13+
* heuristics that were brittle across Bun's ESM/CJS compile modes).
14+
*/
15+
16+
export const BUNDLED_IS_BINARY = false;
17+
export const BUNDLED_VERSION = 'dev';
18+
export const BUNDLED_GIT_COMMIT = 'unknown';

packages/paths/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,6 @@ export {
3030
// Logger
3131
export { createLogger, setLogLevel, getLogLevel, rootLogger } from './logger';
3232
export type { Logger } from './logger';
33+
34+
// Build-time constants (rewritten by scripts/build-binaries.sh)
35+
export { BUNDLED_IS_BINARY, BUNDLED_VERSION, BUNDLED_GIT_COMMIT } from './bundled-build';

packages/paths/src/logger.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import pino from 'pino';
2525
import type { Logger } from 'pino';
26+
import pretty from 'pino-pretty';
2627

2728
export type { Logger } from 'pino';
2829

@@ -44,36 +45,48 @@ function getInitialLevel(): string {
4445
}
4546

4647
/**
47-
* Uses pino-pretty when stdout is a TTY and NODE_ENV !== 'production';
48-
* outputs newline-delimited JSON otherwise.
48+
* Build the root Pino logger.
49+
*
50+
* Uses `pino-pretty` as a **destination stream** (not a worker-thread transport)
51+
* when stdout is a TTY and NODE_ENV !== 'production'. Running pino-pretty as a
52+
* destination stream keeps the formatter on the main thread, which avoids the
53+
* `require.resolve('pino-pretty')` lookup that crashes inside Bun's `/$bunfs/`
54+
* virtual filesystem in compiled binaries (see GitHub issue #960 / #979).
55+
*
56+
* The same code path runs in dev and compiled binaries — no environment
57+
* detection required.
4958
*/
50-
function buildLoggerOptions(): pino.LoggerOptions {
59+
function buildLogger(): Logger {
5160
const level = getInitialLevel();
5261
const usePretty = process.stdout.isTTY && process.env.NODE_ENV !== 'production';
5362

5463
if (usePretty) {
55-
return {
56-
level,
57-
transport: {
58-
target: 'pino-pretty',
59-
options: {
60-
colorize: true,
61-
levelFirst: true,
62-
translateTime: 'SYS:standard',
63-
ignore: 'pid,hostname',
64-
},
65-
},
66-
};
64+
try {
65+
const stream = pretty({
66+
colorize: true,
67+
levelFirst: true,
68+
translateTime: 'SYS:standard',
69+
ignore: 'pid,hostname',
70+
});
71+
return pino({ level }, stream);
72+
} catch (err) {
73+
// pino-pretty failed to initialize (missing peer, broken TTY descriptor,
74+
// or incompatible runtime). Fall back to plain JSON so logging keeps
75+
// working instead of crashing the entire process at module import time.
76+
console.warn(
77+
`[logger] pino-pretty failed to initialize, falling back to JSON output: ${(err as Error).message}`
78+
);
79+
}
6780
}
6881

69-
return { level };
82+
return pino({ level });
7083
}
7184

7285
/**
7386
* Root Pino logger instance.
7487
* Children inherit the root's level at creation time (not dynamically updated).
7588
*/
76-
export const rootLogger: Logger = pino(buildLoggerOptions());
89+
export const rootLogger: Logger = buildLogger();
7790

7891
/**
7992
* Create a child logger with a module binding.

packages/workflows/src/defaults/bundled-defaults.test.ts

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,13 @@
11
import { describe, it, expect } from 'bun:test';
2-
import {
3-
isBinaryBuild,
4-
isBunVirtualFs,
5-
BUNDLED_COMMANDS,
6-
BUNDLED_WORKFLOWS,
7-
} from './bundled-defaults';
2+
import { isBinaryBuild, BUNDLED_COMMANDS, BUNDLED_WORKFLOWS } from './bundled-defaults';
83

94
describe('bundled-defaults', () => {
10-
describe('isBunVirtualFs', () => {
11-
it('should detect Linux/macOS virtual filesystem paths', () => {
12-
expect(isBunVirtualFs('/$bunfs/root/bundled-defaults')).toBe(true);
13-
expect(isBunVirtualFs('/$bunfs/root/')).toBe(true);
14-
});
15-
16-
it('should detect Windows virtual filesystem paths (backslash)', () => {
17-
expect(isBunVirtualFs('B:\\~BUN\\root\\bundled-defaults')).toBe(true);
18-
expect(isBunVirtualFs('B:\\~BUN\\root')).toBe(true);
19-
});
20-
21-
it('should detect Windows virtual filesystem paths (forward slash)', () => {
22-
expect(isBunVirtualFs('B:/~BUN/root/bundled-defaults')).toBe(true);
23-
expect(isBunVirtualFs('B:/~BUN/root')).toBe(true);
24-
});
25-
26-
it('should return false for real filesystem paths', () => {
27-
expect(isBunVirtualFs('/home/user/project/src')).toBe(false);
28-
expect(isBunVirtualFs('C:\\Users\\user\\project\\src')).toBe(false);
29-
expect(isBunVirtualFs('/tmp/test')).toBe(false);
30-
});
31-
});
32-
335
describe('isBinaryBuild', () => {
34-
it('should return false when running in test environment (not compiled)', () => {
35-
// The true path requires an actual compiled binary (import.meta.dir points to
36-
// Bun's virtual FS only inside compiled binaries). Coverage of the true branch
37-
// relies on isBunVirtualFs tests above + manual binary smoke testing in CI.
6+
it('should return false in dev/test mode', () => {
7+
// `isBinaryBuild()` reads the build-time constant `BUNDLED_IS_BINARY` from
8+
// `@archon/paths`. In dev/test mode it is `false`. It is only rewritten to
9+
// `true` by `scripts/build-binaries.sh` before `bun build --compile`.
10+
// Coverage of the `true` branch is via local binary smoke testing (see #979).
3811
expect(isBinaryBuild()).toBe(false);
3912
});
4013
});

packages/workflows/src/defaults/bundled-defaults.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* Import syntax uses `with { type: 'text' }` to import file contents as strings.
99
*/
1010

11+
import { BUNDLED_IS_BINARY } from '@archon/paths';
12+
1113
// =============================================================================
1214
// Default Commands (21 total)
1315
// =============================================================================
@@ -102,23 +104,19 @@ export const BUNDLED_WORKFLOWS: Record<string, string> = {
102104
'archon-workflow-builder': archonWorkflowBuilderWf,
103105
};
104106

105-
/**
106-
* Check if a given module directory path belongs to a compiled Bun binary.
107-
*
108-
* Compiled Bun binaries use a virtual filesystem for bundled modules:
109-
* - Linux/macOS: `/$bunfs/root/`
110-
* - Windows: `B:\~BUN\root\` or `B:/~BUN/root/`
111-
*/
112-
export function isBunVirtualFs(dir: string): boolean {
113-
return dir.startsWith('/$bunfs/') || dir.startsWith('B:\\~BUN\\') || dir.startsWith('B:/~BUN/');
114-
}
115-
116107
/**
117108
* Check if the current process is running as a compiled binary (not via Bun CLI).
118109
*
119-
* Note: `process.versions.bun` is still set in compiled binaries as of Bun 1.3.5,
120-
* so we use the virtual filesystem path prefix for detection instead.
110+
* Reads the build-time constant `BUNDLED_IS_BINARY` from `@archon/paths`.
111+
* `scripts/build-binaries.sh` rewrites that file to set it to `true` before
112+
* `bun build --compile` and restores it afterwards. See GitHub issue #979.
113+
*
114+
* Kept as a function (rather than a direct re-export of `BUNDLED_IS_BINARY`)
115+
* so tests can use `spyOn(bundledDefaults, 'isBinaryBuild').mockReturnValue(...)`
116+
* without resorting to `mock.module('@archon/paths', ...)` — which is
117+
* process-global and irreversible in Bun and would pollute other test files.
118+
* See `.claude/rules/dx-quirks.md` and `loader.test.ts` for context.
121119
*/
122120
export function isBinaryBuild(): boolean {
123-
return isBunVirtualFs(import.meta.dir);
121+
return BUNDLED_IS_BINARY;
124122
}

0 commit comments

Comments
 (0)