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.
This commit is contained in:
can1357
2026-02-01 12:55:34 +01:00
parent a8bfd2ebe1
commit 66da3e8759
7 changed files with 95 additions and 85 deletions
+9 -18
View File
@@ -246,8 +246,8 @@ fn parse_key_id(key_id: &str) -> Option<ParsedKeyId<'_>> {
// 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<ParsedKeyId<'_>> {
}
#[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<u8> {
const fn ctrl_symbol_to_byte(symbol: u8) -> Option<u8> {
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,
}
}
+1
View File
@@ -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",
+5 -1
View File
@@ -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
+1
View File
@@ -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",
+60 -55
View File
@@ -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<void> {
try {
const entries = await fs.readdir(dir);
@@ -111,14 +59,71 @@ async function installBinary(src: string, dest: string): Promise<void> {
}
}
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);
+12 -4
View File
@@ -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}`);
}
+7 -7
View File
@@ -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;
}