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:
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user