Squashed 'third_party/pi/' content from commit d981de122
git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
22
packages/tui/native/win32/README.md
Normal file
22
packages/tui/native/win32/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Windows native prebuilds
|
||||
|
||||
Build both Windows architectures from the repository root:
|
||||
|
||||
```sh
|
||||
npm --prefix packages/tui run build:native:win32
|
||||
```
|
||||
|
||||
On Windows, the build uses the Microsoft C++ Build Tools from Visual Studio. `build.mjs` locates `VsDevCmd.bat`, initializes a developer environment for `amd64` and `arm64`, and builds the addon with `cl.exe`/`link.exe`.
|
||||
|
||||
Install the "Desktop development with C++" workload, or at minimum the MSVC toolset and Windows SDK components. No Node headers are required; the addon resolves N-API symbols from the host process.
|
||||
|
||||
For non-Windows cross-builds, or for custom Windows toolchains, set `PI_TUI_WIN32_TOOLCHAIN=mingw` and provide MinGW-compatible compilers:
|
||||
|
||||
```sh
|
||||
PI_TUI_WIN32_TOOLCHAIN=mingw \
|
||||
CC_X64=/path/to/x86_64-w64-mingw32-gcc \
|
||||
CC_ARM64=/path/to/aarch64-w64-mingw32-gcc \
|
||||
npm --prefix packages/tui run build:native:win32
|
||||
```
|
||||
|
||||
The addon intentionally avoids the C runtime and links only against `kernel32`.
|
||||
229
packages/tui/native/win32/build.mjs
Normal file
229
packages/tui/native/win32/build.mjs
Normal file
@@ -0,0 +1,229 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const sourceFile = path.join(scriptDir, "src", "win32-console-mode.c");
|
||||
const temporaryDir = mkdtempSync(path.join(tmpdir(), "pi-tui-win32-"));
|
||||
|
||||
const targets = [
|
||||
{
|
||||
arch: "x64",
|
||||
msvcArch: "x64",
|
||||
mingwTarget: "x86_64-w64-windows-gnu",
|
||||
prefixedCompilers: ["x86_64-w64-mingw32-gcc", "x86_64-w64-mingw32-clang"],
|
||||
envCompiler: "CC_X64",
|
||||
},
|
||||
{
|
||||
arch: "arm64",
|
||||
msvcArch: "arm64",
|
||||
mingwTarget: "aarch64-w64-windows-gnu",
|
||||
prefixedCompilers: ["aarch64-w64-mingw32-gcc", "aarch64-w64-mingw32-clang"],
|
||||
envCompiler: "CC_ARM64",
|
||||
},
|
||||
];
|
||||
|
||||
function removeTemporaryDir() {
|
||||
rmSync(temporaryDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
|
||||
process.on("exit", removeTemporaryDir);
|
||||
process.on("SIGINT", () => {
|
||||
removeTemporaryDir();
|
||||
process.exit(130);
|
||||
});
|
||||
|
||||
function commandExists(command) {
|
||||
const result = spawnSync(command, ["--version"], { stdio: "ignore" });
|
||||
return !result.error || result.error.code !== "ENOENT";
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, { stdio: "inherit", ...options });
|
||||
if (result.error) throw result.error;
|
||||
if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status ?? 1}`);
|
||||
}
|
||||
|
||||
function quoteBatch(value) {
|
||||
return `"${value.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
function msvcHostArch() {
|
||||
return process.arch === "arm64" ? "arm64" : "x64";
|
||||
}
|
||||
|
||||
function findVisualStudioDevCmd() {
|
||||
const candidates = [];
|
||||
const programFilesX86 = process.env["ProgramFiles(x86)"];
|
||||
if (programFilesX86) {
|
||||
const vswhere = path.join(programFilesX86, "Microsoft Visual Studio", "Installer", "vswhere.exe");
|
||||
if (existsSync(vswhere)) {
|
||||
const result = spawnSync(
|
||||
vswhere,
|
||||
[
|
||||
"-latest",
|
||||
"-products",
|
||||
"*",
|
||||
"-requires",
|
||||
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"-property",
|
||||
"installationPath",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
for (const line of result.stdout.split(/\r?\n/)) {
|
||||
const installationPath = line.trim();
|
||||
if (installationPath) candidates.push(path.join(installationPath, "Common7", "Tools", "VsDevCmd.bat"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const base of [process.env.ProgramFiles, programFilesX86]) {
|
||||
if (!base) continue;
|
||||
for (const edition of ["BuildTools", "Community", "Professional", "Enterprise"]) {
|
||||
candidates.push(path.join(base, "Microsoft Visual Studio", "2022", edition, "Common7", "Tools", "VsDevCmd.bat"));
|
||||
}
|
||||
}
|
||||
|
||||
return candidates.find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
function canUseMsvc(vsDevCmd) {
|
||||
const probeDir = path.join(temporaryDir, "msvc-probe");
|
||||
mkdirSync(probeDir, { recursive: true });
|
||||
const batchFile = path.join(probeDir, "probe.cmd");
|
||||
writeFileSync(
|
||||
batchFile,
|
||||
[
|
||||
"@echo off",
|
||||
`call ${quoteBatch(vsDevCmd)} -no_logo -arch=x64 -host_arch=${msvcHostArch()} >nul`,
|
||||
"if errorlevel 1 exit /b %errorlevel%",
|
||||
"where cl.exe >nul 2>nul",
|
||||
].join("\r\n"),
|
||||
);
|
||||
const result = spawnSync("cmd.exe", ["/d", "/s", "/c", batchFile], { cwd: probeDir, stdio: "ignore" });
|
||||
return !result.error && result.status === 0;
|
||||
}
|
||||
|
||||
function resolveMingwCompiler(target) {
|
||||
const envCompiler = process.env[target.envCompiler];
|
||||
if (envCompiler) return { command: envCompiler, useTargetFlag: false };
|
||||
if (process.env.CC) return { command: process.env.CC, useTargetFlag: true };
|
||||
for (const command of target.prefixedCompilers) {
|
||||
if (commandExists(command)) return { command, useTargetFlag: false };
|
||||
}
|
||||
if (commandExists("clang")) return { command: "clang", useTargetFlag: true };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function installOutput(temporaryOutput, target) {
|
||||
const outputDir = path.join(scriptDir, "prebuilds", `win32-${target.arch}`);
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const output = path.join(outputDir, "win32-console-mode.node");
|
||||
copyFileSync(temporaryOutput, output);
|
||||
chmodSync(output, 0o755);
|
||||
console.log(`Built ${path.relative(process.cwd(), output)}`);
|
||||
}
|
||||
|
||||
function buildWithMsvc(target, vsDevCmd) {
|
||||
const buildDir = path.join(temporaryDir, `msvc-${target.arch}`);
|
||||
const temporaryOutput = path.join(buildDir, "win32-console-mode.node");
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
|
||||
const batchFile = path.join(buildDir, "build.cmd");
|
||||
writeFileSync(
|
||||
batchFile,
|
||||
[
|
||||
"@echo off",
|
||||
`call ${quoteBatch(vsDevCmd)} -no_logo -arch=${target.msvcArch} -host_arch=${msvcHostArch()}`,
|
||||
"if errorlevel 1 exit /b %errorlevel%",
|
||||
[
|
||||
"cl",
|
||||
"/nologo",
|
||||
"/TC",
|
||||
"/std:c11",
|
||||
"/W4",
|
||||
"/O1",
|
||||
"/GS-",
|
||||
"/LD",
|
||||
`/Fe:${quoteBatch(temporaryOutput)}`,
|
||||
quoteBatch(sourceFile),
|
||||
"/link",
|
||||
"/NOENTRY",
|
||||
"/NODEFAULTLIB",
|
||||
"kernel32.lib",
|
||||
"/OPT:REF",
|
||||
"/OPT:ICF",
|
||||
].join(" "),
|
||||
"if errorlevel 1 exit /b %errorlevel%",
|
||||
].join("\r\n"),
|
||||
);
|
||||
|
||||
run("cmd.exe", ["/d", "/s", "/c", batchFile], { cwd: buildDir });
|
||||
installOutput(temporaryOutput, target);
|
||||
}
|
||||
|
||||
function buildWithMingw(target) {
|
||||
const compiler = resolveMingwCompiler(target);
|
||||
if (!compiler) {
|
||||
throw new Error(
|
||||
`No Windows cross-compiler found for ${target.arch}. Install Microsoft C++ Build Tools on Windows, or set ${target.envCompiler} to a MinGW-compatible compiler.`,
|
||||
);
|
||||
}
|
||||
|
||||
const buildDir = path.join(temporaryDir, `mingw-${target.arch}`);
|
||||
const temporaryOutput = path.join(buildDir, "win32-console-mode.node");
|
||||
mkdirSync(buildDir, { recursive: true });
|
||||
|
||||
const args = [
|
||||
...(compiler.useTargetFlag ? [`--target=${target.mingwTarget}`] : []),
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Oz",
|
||||
"-shared",
|
||||
"-nostdlib",
|
||||
"-Wl,--no-entry",
|
||||
"-Wl,--strip-all",
|
||||
sourceFile,
|
||||
"-lkernel32",
|
||||
"-o",
|
||||
temporaryOutput,
|
||||
];
|
||||
|
||||
run(compiler.command, args, { cwd: buildDir });
|
||||
installOutput(temporaryOutput, target);
|
||||
}
|
||||
|
||||
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
||||
console.log(`Usage: npm --prefix packages/tui run build:native:win32
|
||||
|
||||
Builds win32-x64 and win32-arm64 native prebuilds.
|
||||
|
||||
Environment:
|
||||
PI_TUI_WIN32_TOOLCHAIN=msvc|mingw
|
||||
CC_X64=/path/to/x86_64-w64-mingw32-gcc
|
||||
CC_ARM64=/path/to/aarch64-w64-mingw32-gcc
|
||||
CC=/path/to/clang`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const requestedToolchain = process.env.PI_TUI_WIN32_TOOLCHAIN;
|
||||
if (requestedToolchain && requestedToolchain !== "msvc" && requestedToolchain !== "mingw") {
|
||||
throw new Error("PI_TUI_WIN32_TOOLCHAIN must be either 'msvc' or 'mingw'");
|
||||
}
|
||||
|
||||
const vsDevCmd = findVisualStudioDevCmd();
|
||||
const hasMsvc = vsDevCmd ? canUseMsvc(vsDevCmd) : false;
|
||||
const toolchain = requestedToolchain ?? (process.platform === "win32" && vsDevCmd ? "msvc" : "mingw");
|
||||
|
||||
if (toolchain === "msvc") {
|
||||
if (!vsDevCmd || !hasMsvc) {
|
||||
throw new Error("Microsoft C++ Build Tools not found. Install the Visual Studio C++ workload or set PI_TUI_WIN32_TOOLCHAIN=mingw.");
|
||||
}
|
||||
for (const target of targets) buildWithMsvc(target, vsDevCmd);
|
||||
} else {
|
||||
for (const target of targets) buildWithMingw(target);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
119
packages/tui/native/win32/src/win32-console-mode.c
Normal file
119
packages/tui/native/win32/src/win32-console-mode.c
Normal file
@@ -0,0 +1,119 @@
|
||||
#include <windows.h>
|
||||
|
||||
#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
|
||||
#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
|
||||
#endif
|
||||
|
||||
#define NAPI_AUTO_LENGTH ((unsigned long long)-1)
|
||||
#define KEY_PRESSED_MASK 0x8000
|
||||
|
||||
typedef void* napi_env;
|
||||
typedef void* napi_value;
|
||||
typedef void* napi_callback_info;
|
||||
typedef napi_value (__cdecl *napi_callback)(napi_env, napi_callback_info);
|
||||
typedef int (__cdecl *napi_create_function_fn)(napi_env, const char*, unsigned long long, napi_callback, void*, napi_value*);
|
||||
typedef int (__cdecl *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
|
||||
typedef int (__cdecl *napi_get_boolean_fn)(napi_env, int, napi_value*);
|
||||
typedef int (__cdecl *napi_get_cb_info_fn)(napi_env, napi_callback_info, unsigned long long*, napi_value*, napi_value*, void**);
|
||||
typedef int (__cdecl *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, unsigned long long, unsigned long long*);
|
||||
typedef SHORT (WINAPI *get_async_key_state_fn)(int);
|
||||
|
||||
static void* node_symbol(const char* name) {
|
||||
HMODULE module = GetModuleHandleA(0);
|
||||
void* proc = module ? (void*)GetProcAddress(module, name) : 0;
|
||||
if (proc) return proc;
|
||||
|
||||
module = GetModuleHandleA("node.dll");
|
||||
return module ? (void*)GetProcAddress(module, name) : 0;
|
||||
}
|
||||
|
||||
static int string_equals(const char* left, const char* right) {
|
||||
while (*left && *right && *left == *right) {
|
||||
left++;
|
||||
right++;
|
||||
}
|
||||
return *left == 0 && *right == 0;
|
||||
}
|
||||
|
||||
static get_async_key_state_fn get_async_key_state_symbol(void) {
|
||||
static int loaded = 0;
|
||||
static get_async_key_state_fn get_async_key_state = 0;
|
||||
|
||||
if (!loaded) {
|
||||
HMODULE module = GetModuleHandleA("user32.dll");
|
||||
if (!module) module = LoadLibraryA("user32.dll");
|
||||
get_async_key_state = module ? (get_async_key_state_fn)GetProcAddress(module, "GetAsyncKeyState") : 0;
|
||||
loaded = 1;
|
||||
}
|
||||
|
||||
return get_async_key_state;
|
||||
}
|
||||
|
||||
static int is_key_pressed(int virtual_key) {
|
||||
get_async_key_state_fn get_async_key_state = get_async_key_state_symbol();
|
||||
return get_async_key_state && (((unsigned short)get_async_key_state(virtual_key)) & KEY_PRESSED_MASK) != 0;
|
||||
}
|
||||
|
||||
static int is_modifier_name_pressed(const char* name) {
|
||||
if (string_equals(name, "shift")) return is_key_pressed(VK_SHIFT) || is_key_pressed(VK_LSHIFT) || is_key_pressed(VK_RSHIFT);
|
||||
if (string_equals(name, "control")) return is_key_pressed(VK_CONTROL) || is_key_pressed(VK_LCONTROL) || is_key_pressed(VK_RCONTROL);
|
||||
if (string_equals(name, "option") || string_equals(name, "alt")) return is_key_pressed(VK_MENU) || is_key_pressed(VK_LMENU) || is_key_pressed(VK_RMENU);
|
||||
if (string_equals(name, "command") || string_equals(name, "super") || string_equals(name, "win")) return is_key_pressed(VK_LWIN) || is_key_pressed(VK_RWIN);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static napi_value __cdecl enable_virtual_terminal_input(napi_env env, napi_callback_info info) {
|
||||
(void)info;
|
||||
|
||||
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
|
||||
DWORD mode = 0;
|
||||
int enabled = handle != INVALID_HANDLE_VALUE &&
|
||||
GetConsoleMode(handle, &mode) &&
|
||||
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_INPUT);
|
||||
|
||||
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
|
||||
napi_value result = 0;
|
||||
if (napi_get_boolean) napi_get_boolean(env, enabled, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value __cdecl is_modifier_pressed(napi_env env, napi_callback_info info) {
|
||||
napi_get_cb_info_fn napi_get_cb_info = (napi_get_cb_info_fn)node_symbol("napi_get_cb_info");
|
||||
napi_get_value_string_utf8_fn napi_get_value_string_utf8 = (napi_get_value_string_utf8_fn)node_symbol("napi_get_value_string_utf8");
|
||||
napi_get_boolean_fn napi_get_boolean = (napi_get_boolean_fn)node_symbol("napi_get_boolean");
|
||||
|
||||
int pressed = 0;
|
||||
if (napi_get_cb_info && napi_get_value_string_utf8) {
|
||||
unsigned long long argc = 1;
|
||||
napi_value args[1] = {0};
|
||||
if (napi_get_cb_info(env, info, &argc, args, 0, 0) == 0 && argc >= 1 && args[0]) {
|
||||
char name[16] = {0};
|
||||
unsigned long long copied = 0;
|
||||
if (napi_get_value_string_utf8(env, args[0], name, sizeof(name), &copied) == 0) {
|
||||
pressed = is_modifier_name_pressed(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
napi_value result = 0;
|
||||
if (napi_get_boolean) napi_get_boolean(env, pressed, &result);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void set_function_export(napi_env env, napi_value exports, const char* name, napi_callback callback) {
|
||||
napi_create_function_fn napi_create_function = (napi_create_function_fn)node_symbol("napi_create_function");
|
||||
napi_set_named_property_fn napi_set_named_property = (napi_set_named_property_fn)node_symbol("napi_set_named_property");
|
||||
|
||||
napi_value fn = 0;
|
||||
if (napi_create_function &&
|
||||
napi_set_named_property &&
|
||||
napi_create_function(env, name, NAPI_AUTO_LENGTH, callback, 0, &fn) == 0) {
|
||||
napi_set_named_property(env, exports, name, fn);
|
||||
}
|
||||
}
|
||||
|
||||
__declspec(dllexport) napi_value __cdecl napi_register_module_v1(napi_env env, napi_value exports) {
|
||||
set_function_export(env, exports, "enableVirtualTerminalInput", enable_virtual_terminal_input);
|
||||
set_function_export(env, exports, "isModifierPressed", is_modifier_pressed);
|
||||
return exports;
|
||||
}
|
||||
Reference in New Issue
Block a user