From 66da3e87599f987ba90bb22a3a5f34c1331bcfd3 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 1 Feb 2026 12:55:34 +0100 Subject: [PATCH] feat(natives): added development mode support for native binary builds with debug profiles - Added development mode support for native binary builds via `--dev` flag and `OMP_DEV` environment variable. - Added `dev:native` npm script for building debug native binaries in development environments. - Changed native binary build process to conditionally use debug or release profiles based on development mode flag. - Changed native addon loader to prioritize debug builds when `OMP_DEV` environment variable is set. - Refactored keyboard parsing functions in pi-natives to use const functions and guard clauses for improved code clarity. --- crates/pi-natives/src/keys.rs | 27 ++---- package.json | 1 + packages/natives/CHANGELOG.md | 6 +- packages/natives/package.json | 1 + packages/natives/scripts/build-native.ts | 115 ++++++++++++----------- packages/natives/src/native.ts | 16 +++- packages/tui/src/keys.ts | 14 +-- 7 files changed, 95 insertions(+), 85 deletions(-) diff --git a/crates/pi-natives/src/keys.rs b/crates/pi-natives/src/keys.rs index fcc221ea1..27fdfcd0d 100644 --- a/crates/pi-natives/src/keys.rs +++ b/crates/pi-natives/src/keys.rs @@ -246,8 +246,8 @@ fn parse_key_id(key_id: &str) -> Option> { // In this case the trailing "++" means: delimiter '+' + key '+' let (prefix, forced_key_plus): (&str, bool) = if s == "+" { ("", true) - } else if s.ends_with("++") { - (&s[..s.len() - 2], true) + } else if let Some(stripped) = s.strip_suffix("++") { + (stripped, true) } else { (s, false) }; @@ -291,12 +291,12 @@ fn parse_key_id(key_id: &str) -> Option> { } #[inline] -fn raw_ctrl_char(letter: u8) -> u8 { +const fn raw_ctrl_char(letter: u8) -> u8 { (letter.to_ascii_lowercase() - b'a') + 1 } /// CTRL+symbol legacy mappings -fn ctrl_symbol_to_byte(symbol: u8) -> Option { +const fn ctrl_symbol_to_byte(symbol: u8) -> Option { match symbol { b'@' => Some(0x00), b'[' => Some(0x1b), @@ -347,12 +347,6 @@ fn parse_modify_other_keys(bytes: &[u8]) -> Option<(u32, i32)> { Some((modifier, keycode)) } -#[inline] -fn matches_modify_other_keys(bytes: &[u8], expected_keycode: i32, expected_modifier: u32) -> bool { - parse_modify_other_keys(bytes) - .is_some_and(|(m, k)| k == expected_keycode && m == expected_modifier) -} - fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> bool { let Some(parsed_id) = parse_key_id(key_id) else { return false; @@ -670,10 +664,11 @@ fn matches_key_inner(bytes: &[u8], key_id: &str, kitty_protocol_active: bool) -> } // ctrl+symbol legacy mapping (layout dependent) - if let Some(legacy_ctrl) = ctrl_symbol_to_byte(ch) { - if bytes.len() == 1 && bytes[0] == legacy_ctrl { - return true; - } + if let Some(legacy_ctrl) = ctrl_symbol_to_byte(ch) + && bytes.len() == 1 + && bytes[0] == legacy_ctrl + { + return true; } return mok_matches(codepoint, MOD_CTRL) || kitty_matches(codepoint, MOD_CTRL); @@ -1120,10 +1115,6 @@ fn format_key_name(codepoint: i32) -> Option<&'static str> { // Any printable ASCII can be represented without allocation via the static table. 33..=126 => Some(ASCII_PRINTABLE[(codepoint - 33) as usize]), - - // Keep lowercase letters as a fast/consistent path (optional): - 97..=122 => Some(LETTERS[(codepoint - 97) as usize]), - _ => None, } } diff --git a/package.json b/package.json index 600f60307..4acbec86d 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "fix:ts": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts", "fix:rs": "cargo clippy --fix --allow-dirty --all-targets --no-deps --allow-staged --broken-code --allow-no-vcs && cargo fmt --all", "build:native": "bun --cwd=packages/natives run build:native", + "dev:native": "bun --cwd=packages/natives run dev:native", "bench:gen-fixtures": "bun run packages/react-edit-benchmark/generate.ts --react-dir /tmp/react-source --count-per-type 3", "bench:edit": "bun --cwd=packages/react-edit-benchmark run start", "version:patch": "npm version patch -ws --no-git-tag-version && bun scripts/sync-versions.ts && rm -rf node_modules packages/*/node_modules bun.lock && bun install", diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index c9d7cf2b3..30ad47b29 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -1,9 +1,10 @@ # Changelog ## [Unreleased] - ### Added +- Added `dev:native` npm script to build debug native binaries with `--dev` flag +- Added `OMP_DEV` environment variable support for loading and debugging development native builds - Exported keyboard parsing and matching functions: `parseKey`, `parseKittySequence`, `matchesLegacySequence`, and `matchesKey` for terminal input handling - Exported `KeyEventType` enum and `ParsedKittyResult` type for Kitty keyboard protocol support - Added `parseKey` function to parse terminal input and return normalized key identifiers (e.g., "ctrl+c", "shift+tab") @@ -13,6 +14,9 @@ ### Changed +- Modified native binary build process to support both debug and release builds via `--dev` flag +- Updated native binary search to prioritize platform-tagged builds and separate debug/release candidates +- Changed debug builds to output to `pi_natives.dev.node` instead of mixing with release artifacts - Improved native binary installation to use atomic rename operations and better fallback handling for Windows DLLs - Reordered native binary search candidates to prioritize platform-tagged builds and avoid loading stale cross-compiled binaries - Enhanced cross-compilation detection to prevent installing wrong-platform fallback binaries during cross-compilation builds diff --git a/packages/natives/package.json b/packages/natives/package.json index 19f4186f3..f6bbe415b 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -17,6 +17,7 @@ ], "scripts": { "build:native": "bun scripts/build-native.ts", + "dev:native": "bun scripts/build-native.ts --dev", "check": "biome check . && tsgo -p tsconfig.json", "fix": "biome check --write --unsafe .", "test": "bun run build:native && bun test", diff --git a/packages/natives/scripts/build-native.ts b/packages/natives/scripts/build-native.ts index c08aad385..68d22ada2 100644 --- a/packages/natives/scripts/build-native.ts +++ b/packages/natives/scripts/build-native.ts @@ -6,6 +6,7 @@ const repoRoot = path.join(import.meta.dir, "../../.."); const rustDir = path.join(repoRoot, "crates/pi-natives"); const nativeDir = path.join(import.meta.dir, "../native"); +const isDev = process.argv.includes("--dev"); const crossTarget = process.env.CROSS_TARGET; const targetPlatform = process.env.TARGET_PLATFORM || process.platform; const targetArch = process.env.TARGET_ARCH || process.arch; @@ -14,59 +15,6 @@ const isCrossCompile = targetPlatform !== process.platform || targetArch !== process.arch; -const cargoArgs = ["build", "--release"]; -if (crossTarget) cargoArgs.push("--target", crossTarget); - -console.log(`Building pi-natives for ${targetPlatform}-${targetArch}...`); -const buildResult = await $`cargo ${cargoArgs}`.cwd(rustDir).nothrow(); -if (buildResult.exitCode !== 0) { - const stderr = - typeof buildResult.stderr === "string" - ? buildResult.stderr - : buildResult.stderr?.length - ? new TextDecoder().decode(buildResult.stderr) - : ""; - throw new Error(`cargo build --release failed${stderr ? `:\n${stderr}` : ""}`); -} - -const targetRoots = [ - process.env.CARGO_TARGET_DIR ? path.resolve(process.env.CARGO_TARGET_DIR) : undefined, - path.join(repoRoot, "target"), - path.join(rustDir, "target"), -].filter((v): v is string => Boolean(v)); - -const releaseDirs = targetRoots.flatMap((root) => { - if (crossTarget) { - return [path.join(root, crossTarget, "release"), path.join(root, "release")]; - } - return [path.join(root, "release")]; -}); - -const libraryNames = ["libpi_natives.so", "libpi_natives.dylib", "pi_natives.dll", "libpi_natives.dll"]; - -let sourcePath: string | null = null; -for (const dir of releaseDirs) { - for (const name of libraryNames) { - const fullPath = path.join(dir, name); - try { - await fs.stat(fullPath); - sourcePath = fullPath; - break; - } catch (err) { - if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; - } - } - if (sourcePath) break; -} - -if (!sourcePath) { - const checked = releaseDirs.map((d) => ` - ${d}`).join("\n"); - throw new Error(`Built library not found. Checked:\n${checked}`); -} - -console.log(`Found: ${sourcePath}`); -await fs.mkdir(nativeDir, { recursive: true }); - async function cleanupStaleTemps(dir: string): Promise { try { const entries = await fs.readdir(dir); @@ -111,14 +59,71 @@ async function installBinary(src: string, dest: string): Promise { } } + + +const cargoArgs = ["build"]; +if (!isDev) cargoArgs.push("--release"); +if (crossTarget) cargoArgs.push("--target", crossTarget); + +console.log(`Building pi-natives for ${targetPlatform}-${targetArch}${isDev ? " (debug)" : ""}...`); +const buildResult = await $`cargo ${cargoArgs}`.cwd(rustDir).nothrow(); +if (buildResult.exitCode !== 0) { + const stderr = + typeof buildResult.stderr === "string" + ? buildResult.stderr + : buildResult.stderr?.length + ? new TextDecoder().decode(buildResult.stderr) + : ""; + throw new Error(`cargo build --release failed${stderr ? `:\n${stderr}` : ""}`); +} + +const profile = isDev ? "debug" : "release"; +const targetRoots = [ + process.env.CARGO_TARGET_DIR ? path.resolve(process.env.CARGO_TARGET_DIR) : undefined, + path.join(repoRoot, "target"), + path.join(rustDir, "target"), +].filter((v): v is string => Boolean(v)); + +const profileDirs = targetRoots.flatMap((root) => { + if (crossTarget) { + return [path.join(root, crossTarget, profile), path.join(root, profile)]; + } + return [path.join(root, profile)]; +}); + +const libraryNames = ["libpi_natives.so", "libpi_natives.dylib", "pi_natives.dll", "libpi_natives.dll"]; + +let sourcePath: string | null = null; +for (const dir of profileDirs) { + for (const name of libraryNames) { + const fullPath = path.join(dir, name); + try { + await fs.stat(fullPath); + sourcePath = fullPath; + break; + } catch (err) { + if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err; + } + } + if (sourcePath) break; +} + +await fs.mkdir(nativeDir, { recursive: true }); await cleanupStaleTemps(nativeDir); -const taggedPath = path.join(nativeDir, `pi_natives.${targetPlatform}-${targetArch}.node`); +if (!sourcePath) { + const checked = profileDirs.map((d) => ` - ${d}`).join("\n"); + throw new Error(`Built library not found. Checked:\n${checked}`); +} + + +console.log(`Found: ${sourcePath}`); +const taggedPath = isDev ? path.join(nativeDir, `pi_natives.dev.node`) : path.join(nativeDir, `pi_natives.${targetPlatform}-${targetArch}.node`); console.log(`Installing: ${taggedPath}`); await installBinary(sourcePath, taggedPath); // Only create fallback for native (non-cross) builds to avoid overwriting with wrong-platform binaries -if (!isCrossCompile) { +if (!isCrossCompile && !isDev) { const fallbackPath = path.join(nativeDir, "pi_natives.node"); console.log(`Installing: ${fallbackPath}`); await installBinary(sourcePath, fallbackPath); diff --git a/packages/natives/src/native.ts b/packages/natives/src/native.ts index 4f8d7fd62..d5e796f00 100644 --- a/packages/natives/src/native.ts +++ b/packages/natives/src/native.ts @@ -100,18 +100,19 @@ const execDir = path.dirname(process.execPath); const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"]; -const candidates = [ +const debugCandidates = [path.join(nativeDir, "pi_natives.dev.node"), path.join(execDir, "pi_natives.dev.node")]; + +const releaseCandidates = [ // Platform-tagged builds (preferred - always correct platform) path.join(nativeDir, `pi_natives.${platformTag}.node`), path.join(execDir, `pi_natives.${platformTag}.node`), // Fallback untagged (only created for native builds, not cross-compilation) path.join(nativeDir, "pi_natives.node"), path.join(execDir, "pi_natives.node"), - // Dev builds (cargo build --release output, may be stale after cross-compilation) - path.join(repoRoot, "target", "release", "pi_natives.node"), - path.join(repoRoot, "crates", "pi-natives", "target", "release", "pi_natives.node"), ]; +const candidates = process.env.OMP_DEV ? [...debugCandidates, ...releaseCandidates] : releaseCandidates; + function loadNative(): NativeBindings { const errors: string[] = []; @@ -119,8 +120,15 @@ function loadNative(): NativeBindings { try { const bindings = require(candidate) as NativeBindings; validateNative(bindings, candidate); + if (process.env.OMP_DEV) { + console.log(`Loaded native addon from ${candidate}`); + console.log(` - Root: ${repoRoot}`); + } return bindings; } catch (err) { + if (process.env.OMP_DEV) { + console.error(`Error loading native addon from ${candidate}:`, err); + } const message = err instanceof Error ? err.message : String(err); errors.push(`${candidate}: ${message}`); } diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index 5c430a979..797126f5f 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -29,21 +29,21 @@ import { // Global Kitty Protocol State // ============================================================================= -let _kittyProtocolActive = false; +let kittyProtocolActive = false; /** * Set the global Kitty keyboard protocol state. * Called by ProcessTerminal after detecting protocol support. */ export function setKittyProtocolActive(active: boolean): void { - _kittyProtocolActive = active; + kittyProtocolActive = active; } /** * Query whether Kitty keyboard protocol is currently active. */ export function isKittyProtocolActive(): boolean { - return _kittyProtocolActive; + return kittyProtocolActive; } // ============================================================================= @@ -192,7 +192,7 @@ const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDHF]$/; */ export function isKeyRelease(data: string): boolean { // Only detect release events when Kitty protocol is active - if (!_kittyProtocolActive) { + if (!kittyProtocolActive) { return false; } @@ -212,7 +212,7 @@ export function isKeyRelease(data: string): boolean { */ export function isKeyRepeat(data: string): boolean { // Only detect repeat events when Kitty protocol is active - if (!_kittyProtocolActive) { + if (!kittyProtocolActive) { return false; } @@ -254,7 +254,7 @@ export function parseKittySequence(data: string): ParsedKittySequence | null { * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c")) */ export function matchesKey(data: string, keyId: KeyId): boolean { - return matchesKeyNative(data, keyId, _kittyProtocolActive); + return matchesKeyNative(data, keyId, kittyProtocolActive); } /** @@ -266,5 +266,5 @@ export function matchesKey(data: string, keyId: KeyId): boolean { * @param data - Raw input data from terminal */ export function parseKey(data: string): string | undefined { - return parseKeyNative(data, _kittyProtocolActive) ?? undefined; + return parseKeyNative(data, kittyProtocolActive) ?? undefined; }