Skip to content

Commit 04ec48a

Browse files
m4airm4air
authored andcommitted
build: compile isolated Bun ConPTY provider candidate
1 parent c6075ea commit 04ec48a

4 files changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Bundled ConPTY Bun build proof
2+
on:
3+
push:
4+
branches: [OrcaWin/np-bun-conpty-build-proof]
5+
permissions:
6+
contents: read
7+
concurrency:
8+
group: bun-conpty-build-proof
9+
cancel-in-progress: false
10+
jobs:
11+
build:
12+
runs-on: ubuntu-24.04
13+
timeout-minutes: 240
14+
env:
15+
ORCA_BACKGROUND_LAUNCH: '1'
16+
BUN_CONPTY_BUILD_ROOT: ${{ runner.temp }}/bun-conpty-build
17+
steps:
18+
- uses: actions/checkout@v6
19+
with:
20+
persist-credentials: false
21+
- name: Free unused SDK space on disposable runner
22+
run: |
23+
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL
24+
df -h "$RUNNER_TEMP"
25+
free -h
26+
- name: Compile isolated Windows runtime candidates
27+
run: bash config/bun-conpty-build/build-disposable.sh
28+
- uses: actions/upload-artifact@v4
29+
if: always()
30+
with:
31+
name: bun-conpty-candidate
32+
retention-days: 7
33+
if-no-files-found: warn
34+
path: |
35+
${{ runner.temp }}/bun-conpty-build/bun/build/conpty-*/bun.exe
36+
${{ runner.temp }}/bun-conpty-build/bun/build/conpty-*/build.ninja

‎config/bun-conpty-build/Dockerfile‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM bun-conpty-toolchain-base
2+
ENV CARGO_HOME=/opt/bun-cargo RUSTUP_HOME=/opt/bun-rustup
3+
ENV PATH=/opt/bun-cargo/bin:$PATH
4+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh \
5+
&& sh /tmp/rustup.sh -y --profile minimal --default-toolchain nightly-2026-07-20 \
6+
&& rustup target add --toolchain nightly-2026-07-20 x86_64-pc-windows-msvc aarch64-pc-windows-msvc
7+
WORKDIR /work/bun
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
# Run on a disposable Linux Docker host. Only the selected working directory is mounted.
4+
fixture_dir="$(cd "$(dirname "$0")" && pwd)"
5+
bun_build_root="${BUN_CONPTY_BUILD_ROOT:?Set a new disposable build directory}"
6+
mkdir -p "$bun_build_root"
7+
git clone --filter=blob:none --no-checkout https://github.com/oven-sh/bun.git "$bun_build_root/bun"
8+
git -C "$bun_build_root/bun" checkout 744846f844374847c902b5e7fd59b4342a51ef99
9+
git -C "$bun_build_root/bun" submodule update --init --recursive --depth 1
10+
git -C "$bun_build_root/bun" apply --check "$fixture_dir/bundled-conpty.patch"
11+
git -C "$bun_build_root/bun" apply "$fixture_dir/bundled-conpty.patch"
12+
docker build --target base --tag bun-conpty-toolchain-base --file "$bun_build_root/bun/.buildkite/Dockerfile" "$bun_build_root/bun"
13+
docker build --tag bun-conpty-toolchain --file "$fixture_dir/Dockerfile" "$fixture_dir"
14+
for bun_target_arch in x64 arm64; do
15+
bun_baseline_arg=""
16+
if [[ "$bun_target_arch" == x64 ]]; then bun_baseline_arg="--baseline=true"; fi
17+
docker run --rm --mount "type=bind,source=$bun_build_root/bun,target=/work/bun" \
18+
--env CI=true --env ORCA_BACKGROUND_LAUNCH=1 --env RUSTUP_TOOLCHAIN=nightly-2026-07-20 \
19+
bun-conpty-toolchain bash -lc \
20+
"bun install --frozen-lockfile && bun scripts/build.ts --profile=release --os=windows --arch=$bun_target_arch $bun_baseline_arg --lto=off --build-dir=build/conpty-$bun_target_arch -j2"
21+
done
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
--- a/src/runtime/api/bun/Terminal.rs
2+
+++ b/src/runtime/api/bun/Terminal.rs
3+
@@ -121,6 +121,8 @@
4+
/// uv_process_options_t.pseudoconsole.
5+
#[cfg(windows)]
6+
hpcon: Cell<Option<windows::HPCON>>,
7+
+ #[cfg(windows)]
8+
+ conpty_api: ConptyApi,
9+
10+
/// Current terminal size
11+
cols: Cell<u16>,
12+
@@ -419,6 +421,8 @@
13+
slave_fd: Cell::new(pty_result.slave),
14+
#[cfg(windows)]
15+
hpcon: Cell::new(Some(pty_result.hpcon)),
16+
+ #[cfg(windows)]
17+
+ conpty_api: pty_result.conpty_api,
18+
cols: Cell::new(if cfg!(windows) {
19+
u16::try_from(clamp_to_coord(options.cols)).expect("int cast")
20+
} else {
21+
@@ -716,6 +720,11 @@
22+
let Some(hpcon) = self.hpcon.get() else {
23+
return;
24+
};
25+
+ if let Some(release) = self.conpty_api.release {
26+
+ // The bundled provider owns this opaque HPCON; never inspect its fields.
27+
+ unsafe { release(hpcon) };
28+
+ return;
29+
+ }
30+
// SAFETY: `hpcon` was returned from inbox `CreatePseudoConsole`, which
31+
// heap-allocates a `PseudoConsole` and returns it as HPCON.
32+
// `ClosePseudoConsole` later skips the field when it reads null.
33+
@@ -740,10 +749,11 @@
34+
// PORTING.md bans std::process but not std::thread; a raw detached OS
35+
// thread is intentional here (no event-loop integration needed).
36+
let hpcon_addr = hpcon as usize;
37+
+ let close = self.conpty_api.close;
38+
match std::thread::Builder::new().spawn(move || {
39+
// SAFETY: hpcon was a valid HPCON when taken; ClosePseudoConsole is
40+
// safe to call from any thread per Win32 docs.
41+
- unsafe { windows::ClosePseudoConsole(hpcon_addr as windows::HPCON) };
42+
+ unsafe { close(hpcon_addr as windows::HPCON) };
43+
}) {
44+
Ok(_t) => {
45+
// detached: JoinHandle dropped without join → thread runs to completion.
46+
@@ -771,6 +781,8 @@
47+
pub(crate) slave: Fd,
48+
#[cfg(windows)]
49+
pub(crate) hpcon: windows::HPCON,
50+
+ #[cfg(windows)]
51+
+ conpty_api: ConptyApi,
52+
}
53+
54+
#[derive(thiserror::Error, Debug, strum::IntoStaticStr)]
55+
@@ -1152,6 +1164,7 @@
56+
57+
#[cfg(windows)]
58+
fn create_pty_windows(cols: u16, rows: u16) -> Result<PtyResult, CreatePtyError> {
59+
+ let conpty_api = ConptyApi::selected()?;
60+
// Track ownership explicitly: handles are nulled out as they are closed or
61+
// transferred so the errdefer cleanup never double-closes.
62+
let mut out_server: Option<windows::HANDLE> = None;
63+
@@ -1171,7 +1184,7 @@
64+
// safe on those values.
65+
unsafe {
66+
if let Some(h) = hpcon {
67+
- windows::ClosePseudoConsole(h);
68+
+ (conpty_api.close)(h);
69+
}
70+
if let Some(h) = out_server {
71+
let _ = windows::CloseHandle(h);
72+
@@ -1223,7 +1236,7 @@
73+
let mut pc: windows::HPCON = core::ptr::null_mut();
74+
// SAFETY: in_client/out_client are valid open HANDLEs; pc is a valid out-ptr.
75+
if unsafe {
76+
- windows::CreatePseudoConsole(size, in_client.unwrap(), out_client.unwrap(), 0, &mut pc)
77+
+ (conpty_api.create)(size, in_client.unwrap(), out_client.unwrap(), 0, &mut pc)
78+
} < 0
79+
{
80+
cleanup!();
81+
@@ -1274,6 +1287,7 @@
82+
write_fd,
83+
slave: Fd::INVALID,
84+
hpcon: result_hpcon,
85+
+ conpty_api,
86+
})
87+
}
88+
89+
@@ -1549,7 +1563,7 @@
90+
Y: clamp_to_coord(new_rows),
91+
};
92+
// SAFETY: hpcon is a valid open HPCON.
93+
- let hr = unsafe { windows::ResizePseudoConsole(hpcon, size) };
94+
+ let hr = unsafe { (self.conpty_api.resize)(hpcon, size) };
95+
if hr < 0 && hr != HR_NO_DATA && hr != HR_BROKEN_PIPE {
96+
return Err(global_object.throw(format_args!("Failed to resize terminal")));
97+
}
98+
@@ -2060,3 +2074,75 @@
99+
Self::from_parent_ptr(this).on_writer_close();
100+
}
101+
}
102+
+
103+
+// Local prototype: not compiled or ready to ship.
104+
+#[cfg(windows)]
105+
+#[derive(Clone, Copy)]
106+
+struct ConptyApi {
107+
+ create: unsafe extern "system" fn(windows::COORD, windows::HANDLE, windows::HANDLE, u32, *mut windows::HPCON) -> i32,
108+
+ resize: unsafe extern "system" fn(windows::HPCON, windows::COORD) -> i32,
109+
+ close: unsafe extern "system" fn(windows::HPCON),
110+
+ release: Option<unsafe extern "system" fn(windows::HPCON)>,
111+
+}
112+
+
113+
+#[cfg(windows)]
114+
+impl ConptyApi {
115+
+ fn selected() -> Result<Self, CreatePtyError> {
116+
+ static API: std::sync::OnceLock<Result<ConptyApi, ()>> = std::sync::OnceLock::new();
117+
+ API.get_or_init(Self::load).as_ref().copied().map_err(|_| CreatePtyError::OpenPtyFailed)
118+
+ }
119+
+
120+
+ fn load() -> Result<Self, ()> {
121+
+ let Some(bytes) = bun_core::env_var::BUN_CONPTY_LIBRARY::get() else {
122+
+ return Ok(Self {
123+
+ create: windows::CreatePseudoConsole,
124+
+ resize: windows::ResizePseudoConsole,
125+
+ close: windows::ClosePseudoConsole,
126+
+ release: None,
127+
+ });
128+
+ };
129+
+ let path = core::str::from_utf8(bytes).map_err(|_| ())?;
130+
+ if path.contains('\0') || !std::path::Path::new(path).is_absolute() {
131+
+ return Err(());
132+
+ }
133+
+ use std::os::windows::ffi::OsStrExt;
134+
+ let wide: Vec<u16> = std::ffi::OsStr::new(path).encode_wide().chain(Some(0)).collect();
135+
+ // Limit dependency lookup to the selected DLL's directory and System32.
136+
+ let library = unsafe { windows::LoadLibraryExW(wide.as_ptr(), core::ptr::null_mut(), 0x100 | 0x800) };
137+
+ if library.is_null() {
138+
+ return Err(());
139+
+ }
140+
+ struct LibraryGuard(windows::HMODULE);
141+
+ impl Drop for LibraryGuard {
142+
+ fn drop(&mut self) {
143+
+ unsafe { FreeLibrary(self.0); }
144+
+ }
145+
+ }
146+
+ let guard = LibraryGuard(library);
147+
+ macro_rules! symbol {
148+
+ ($name:literal, $ty:ty) => {{
149+
+ let address = unsafe { windows::GetProcAddress(library, concat!($name, "\0").as_ptr().cast()) };
150+
+ if address.is_null() {
151+
+ return Err(());
152+
+ }
153+
+ // All names use the pinned Microsoft ConPTY ABI; the module stays resident.
154+
+ unsafe { core::mem::transmute::<*mut core::ffi::c_void, $ty>(address) }
155+
+ }};
156+
+ }
157+
+ // Keep one module resident for process lifetime, including off-thread closes.
158+
+ let api = Self {
159+
+ create: symbol!("ConptyCreatePseudoConsole", unsafe extern "system" fn(windows::COORD, windows::HANDLE, windows::HANDLE, u32, *mut windows::HPCON) -> i32),
160+
+ resize: symbol!("ConptyResizePseudoConsole", unsafe extern "system" fn(windows::HPCON, windows::COORD) -> i32),
161+
+ close: symbol!("ConptyClosePseudoConsole", unsafe extern "system" fn(windows::HPCON)),
162+
+ release: Some(symbol!("ConptyReleasePseudoConsole", unsafe extern "system" fn(windows::HPCON))),
163+
+ };
164+
+ core::mem::forget(guard);
165+
+ Ok(api)
166+
+ }
167+
+}
168+
+
169+
+#[cfg(windows)]
170+
+#[link(name = "kernel32")]
171+
+unsafe extern "system" {
172+
+ fn FreeLibrary(module: windows::HMODULE) -> i32;
173+
+}
174+
--- a/src/bun_core/env_var.rs
175+
+++ b/src/bun_core/env_var.rs
176+
@@ -58,6 +58,7 @@
177+
// A peer that accepts the TCP connection but never answers the upgrade fails
178+
// with error + close(1006). 0 disables; uSockets' 4 s sweep rounds small values up.
179+
new!(pub BUN_CONFIG_WS_HANDSHAKE_TIMEOUT: unsigned, "BUN_CONFIG_WS_HANDSHAKE_TIMEOUT", { default: 120 });
180+
+new!(pub BUN_CONPTY_LIBRARY: string, "BUN_CONPTY_LIBRARY", {});
181+
new!(pub BUN_CRASH_REPORT_URL: string, "BUN_CRASH_REPORT_URL", {});
182+
new!(pub BUN_DEBUG: string, "BUN_DEBUG", {});
183+
new!(pub BUN_DEBUG_ALL: boolean, "BUN_DEBUG_ALL", {});

0 commit comments

Comments
 (0)