mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
Initialize the packaged GStreamer runtime in the voice host (#42631)
## Why The voice helper previously supported only handshake and shutdown, without validating that its packaged native runtime could be initialized safely. ## What changed - Add an `initializeRuntime` protocol exchange that loads GStreamer and the required plugins from physical package paths without opening audio devices. - Restrict plugin discovery, registry access, and native library search paths, and keep loaded libraries alive until the helper exits. - Give initialization a dedicated deadline and terminate the owned helper when initialization is cancelled or fails. - Allow binary-only Rust targets to disable the default Bazel library target. ## Testing - Cover helper-only packages, cancellation, and environment filtering. - Add an ignored integration test for initialization from a relocated prepared runtime and rejection of duplicate initialization. GitOrigin-RevId: 8fff68fe26e52cb2e0722dc98fb5f124eddb0d4d
This commit is contained in:
committed by
copyberry
parent
280ae8b9fc
commit
d979df154c
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -4908,6 +4908,7 @@ dependencies = [
|
||||
"codex-process-hardening",
|
||||
"codex-realtime-webrtc",
|
||||
"codex-utils-cargo-bin",
|
||||
"libloading",
|
||||
"pretty_assertions",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
|
||||
@@ -17,6 +17,7 @@ use crate::encode_frame;
|
||||
use crate::message_reader::MessageReader;
|
||||
|
||||
const DEADLINE: Duration = Duration::from_secs(/*secs*/ 5);
|
||||
const RUNTIME_INITIALIZATION_DEADLINE: Duration = Duration::from_secs(/*secs*/ 30);
|
||||
|
||||
/// Owns one helper. Dropping it terminates the process and leaves its waiter to reap it.
|
||||
/// A successful handshake establishes compatibility only, not an active audio session.
|
||||
@@ -27,6 +28,17 @@ pub struct VoiceHost {
|
||||
}
|
||||
|
||||
impl VoiceHost {
|
||||
/// Initialize the packaged native runtime without opening devices or starting a session.
|
||||
pub async fn initialize_runtime(mut self) -> Result<Self> {
|
||||
self.exchange(
|
||||
Message::InitializeRuntime {},
|
||||
Message::RuntimeReady {},
|
||||
RUNTIME_INITIALIZATION_DEADLINE,
|
||||
)
|
||||
.await?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub async fn connect(package: &CodexPackageLayout, build_commit: &str) -> Result<Self> {
|
||||
let root = package.package_dir.as_path().canonicalize()?;
|
||||
let name = if cfg!(windows) {
|
||||
@@ -66,13 +78,16 @@ impl VoiceHost {
|
||||
build_commit: build_commit.to_owned(),
|
||||
},
|
||||
Message::Ready {},
|
||||
DEADLINE,
|
||||
)
|
||||
.await?;
|
||||
Ok(host)
|
||||
}
|
||||
|
||||
pub async fn close(mut self) -> Result<()> {
|
||||
let result = self.exchange(Message::Close {}, Message::Closed {}).await;
|
||||
let result = self
|
||||
.exchange(Message::Close {}, Message::Closed {}, DEADLINE)
|
||||
.await;
|
||||
if result.is_err() {
|
||||
self.process.terminate();
|
||||
}
|
||||
@@ -82,8 +97,13 @@ impl VoiceHost {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn exchange(&mut self, request: Message, expected: Message) -> Result<()> {
|
||||
timeout(DEADLINE, async {
|
||||
async fn exchange(
|
||||
&mut self,
|
||||
request: Message,
|
||||
expected: Message,
|
||||
deadline: Duration,
|
||||
) -> Result<()> {
|
||||
timeout(deadline, async {
|
||||
self.process
|
||||
.writer_sender()
|
||||
.send(encode_frame(&request)?)
|
||||
@@ -128,6 +148,11 @@ fn child_environment(vars: impl Iterator<Item = (OsString, OsString)>) -> HashMa
|
||||
.then(|| Some((key, value.into_string().ok()?)))
|
||||
.flatten()
|
||||
})
|
||||
.chain(
|
||||
crate::RUNTIME_ENVIRONMENT
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.to_owned(), value.to_owned())),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ fn forwards_only_explicit_device_network_and_os_inputs() {
|
||||
("DYLD_INSERT_LIBRARIES", "loader"),
|
||||
("PATH", "project"),
|
||||
("GST_PLUGIN_PATH", "plugins"),
|
||||
("GST_REGISTRY", "untrusted-registry"),
|
||||
("GST_REGISTRY_FORK", "yes"),
|
||||
("OPENAI_API_KEY", "secret"),
|
||||
];
|
||||
assert_eq!(
|
||||
@@ -25,6 +27,37 @@ fn forwards_only_explicit_device_network_and_os_inputs() {
|
||||
input[..3]
|
||||
.iter()
|
||||
.map(|(key, value)| (key.to_string(), value.to_string()))
|
||||
.chain(crate::RUNTIME_ENVIRONMENT.map(|(key, value)| (key.into(), value.into())))
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn cancelling_initialization_terminates_the_owned_helper() -> anyhow::Result<()> {
|
||||
let spawned = codex_utils_pty::spawn_pipe_process(
|
||||
std::path::Path::new("/bin/sleep"),
|
||||
&["30".to_owned()],
|
||||
std::path::Path::new("/"),
|
||||
&child_environment(std::iter::empty()),
|
||||
/*arg0*/ &None,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
drop(spawned.stderr_rx);
|
||||
let (_, unused_exit) = tokio::sync::oneshot::channel();
|
||||
let host = super::VoiceHost {
|
||||
process: spawned.session,
|
||||
output: crate::message_reader::MessageReader::new(spawned.stdout_rx),
|
||||
exit: unused_exit,
|
||||
};
|
||||
let mut initialization = Box::pin(host.initialize_runtime());
|
||||
std::future::poll_fn(|context| {
|
||||
assert!(std::future::Future::poll(initialization.as_mut(), context).is_pending());
|
||||
std::task::Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
drop(initialization);
|
||||
tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 1), spawned.exit_rx).await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ mod protocol;
|
||||
pub use client::VoiceHost;
|
||||
pub use protocol::MAX_FRAME_BYTES;
|
||||
pub use protocol::Message;
|
||||
pub use protocol::RUNTIME_ENVIRONMENT;
|
||||
pub use protocol::decode_frame;
|
||||
pub use protocol::encode_frame;
|
||||
pub use protocol::read_message;
|
||||
|
||||
@@ -8,6 +8,20 @@ use serde::Serialize;
|
||||
|
||||
pub const MAX_FRAME_BYTES: usize = 256;
|
||||
|
||||
/// Fixed child settings prevent native initialization from scanning system plugins or caches.
|
||||
pub const RUNTIME_ENVIRONMENT: [(&str, &str); 7] = [
|
||||
("GST_PLUGIN_PATH", ""),
|
||||
("GST_PLUGIN_PATH_1_0", ""),
|
||||
("GST_PLUGIN_SYSTEM_PATH", ""),
|
||||
("GST_PLUGIN_SYSTEM_PATH_1_0", ""),
|
||||
(
|
||||
"GST_REGISTRY",
|
||||
if cfg!(windows) { "NUL" } else { "/dev/null" },
|
||||
),
|
||||
("GST_REGISTRY_UPDATE", "no"),
|
||||
("GST_REGISTRY_FORK", "no"),
|
||||
];
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
@@ -18,6 +32,8 @@ pub const MAX_FRAME_BYTES: usize = 256;
|
||||
pub enum Message {
|
||||
Hello { protocol: u32, build_commit: String },
|
||||
Ready {},
|
||||
InitializeRuntime {},
|
||||
RuntimeReady {},
|
||||
Close {},
|
||||
Closed {},
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ load("//:defs.bzl", "codex_rust_crate")
|
||||
codex_rust_crate(
|
||||
name = "voice-host",
|
||||
crate_name = "codex_voice_host",
|
||||
crate_srcs = [],
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ workspace = true
|
||||
[dependencies]
|
||||
codex-process-hardening = { workspace = true }
|
||||
codex-realtime-webrtc = { workspace = true }
|
||||
libloading = "=0.8.9"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
# Private voice helper foundation
|
||||
|
||||
`codex-voice-host` establishes the inherited-pipe lifecycle for the proposed
|
||||
bundled voice process. It does not open devices, load native plugins, negotiate
|
||||
bundled voice process. It does not open devices, negotiate
|
||||
WebRTC, or enable voice in the TUI. The existing CLI is unchanged.
|
||||
|
||||
Frames are a big-endian u32 length followed by at most 256 bytes of JSON. The
|
||||
parent sends `hello` with protocol `1` and the helper's exact `buildCommit` before
|
||||
receiving `ready`. It then sends `close` and receives `closed` before process exit.
|
||||
After `ready`, the parent may send `initializeRuntime` once. `runtimeReady` means
|
||||
the physical package's GStreamer library and seven explicit plugins initialized;
|
||||
it does not mean an audio session started. Missing or invalid runtime files cause
|
||||
the helper to exit without a readiness response or raw native diagnostics. The
|
||||
client terminates the helper if initialization fails or exceeds its deadline.
|
||||
Unknown fields, incompatible builds, invalid order and oversized frames fail
|
||||
closed without echoing input. EOF exits even when the main worker cannot progress.
|
||||
|
||||
@@ -35,5 +40,20 @@ Omitting `--runtime` retains helper-only assembly.
|
||||
|
||||
This accepts a development runtime receipt, not an authenticated release. It
|
||||
does not repeat native loader inspection or establish trust in the build inputs.
|
||||
The helper still does not load these files; native loading, media/privacy controls,
|
||||
linking against the prepared SDK and actual audio proof remain integration stages.
|
||||
The helper opens only physical packaged paths. The parent fixes GStreamer search
|
||||
paths to empty, disables registry updates/forking, and points its registry to the
|
||||
OS null device. Windows loads use only the DLL's directory and System32. Native
|
||||
libraries remain loaded until helper exit, even after partial initialization,
|
||||
because GStreamer registers process-global callbacks. The small private C ABI
|
||||
bootstrap does not expose native pointers to the parent or link native libraries
|
||||
into ordinary Codex. The existing `libloading` dependency supplies OS loading.
|
||||
Media/privacy controls, a full media binding layer and actual audio proof remain
|
||||
integration stages; helper-only packages continue to support lifecycle calls.
|
||||
|
||||
The ignored `packaged_runtime` integration test uses real libraries prepared for
|
||||
the host platform. From `codex-rs`, run
|
||||
`CODEX_TEST_VOICE_RUNTIME=/absolute/prepared/runtime just test -p codex-voice-host --test packaged_runtime --run-ignored all`.
|
||||
It copies and relocates the runtime with the real helper, checks client
|
||||
initialization and close, and rejects duplicate initialization. This requires
|
||||
native inputs separately; ordinary CI does not run this ignored test. It tests
|
||||
helper loading, not microphone, speaker, backend, or release behavior.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Private helper lifecycle foundation. No devices, native plugins, or media are opened yet.
|
||||
//! Same-build helper lifecycle and opt-in private runtime initialization. No devices or media yet.
|
||||
|
||||
mod runtime;
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
@@ -64,12 +66,29 @@ fn run() -> io::Result<()> {
|
||||
let mut output = io::stdout().lock();
|
||||
output.write_all(&encode_frame(&Message::Ready {})?)?;
|
||||
output.flush()?;
|
||||
match receiver.recv() {
|
||||
Ok(Message::Close {}) => {
|
||||
output.write_all(&encode_frame(&Message::Closed {})?)?;
|
||||
output.flush()
|
||||
}
|
||||
Err(_) => Ok(()),
|
||||
Ok(_) => Err(io::Error::other("invalid voice control sequence")),
|
||||
let mut runtime = None;
|
||||
loop {
|
||||
let reply = match receiver.recv() {
|
||||
Ok(Message::InitializeRuntime {}) => {
|
||||
if runtime.is_some() {
|
||||
return Err(io::Error::other("runtime already initialized"));
|
||||
}
|
||||
runtime = Some(runtime::Runtime::initialize()?);
|
||||
Message::RuntimeReady {}
|
||||
}
|
||||
Ok(Message::Close {}) => {
|
||||
output.write_all(&encode_frame(&Message::Closed {})?)?;
|
||||
return output.flush();
|
||||
}
|
||||
Err(_) => return Ok(()),
|
||||
Ok(
|
||||
Message::Hello { .. }
|
||||
| Message::Ready {}
|
||||
| Message::RuntimeReady {}
|
||||
| Message::Closed {},
|
||||
) => return Err(io::Error::other("invalid voice control sequence")),
|
||||
};
|
||||
output.write_all(&encode_frame(&reply)?)?;
|
||||
output.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
161
codex-rs/voice-host/src/runtime.rs
Normal file
161
codex-rs/voice-host/src/runtime.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
//! Initialize only the physical package's native runtime on the helper worker.
|
||||
//!
|
||||
//! GStreamer registers process-global callbacks. Keep every opened library loaded
|
||||
//! through process exit, including partial initialization failures. This private
|
||||
//! bootstrap is not a media binding API and never opens an audio device.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::ffi::c_char;
|
||||
use std::ffi::c_void;
|
||||
use std::io;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr;
|
||||
|
||||
use codex_realtime_webrtc::RUNTIME_ENVIRONMENT;
|
||||
use libloading::Library;
|
||||
|
||||
type Init = unsafe extern "C" fn(*mut i32, *mut *mut *mut c_char, *mut *mut c_void) -> i32;
|
||||
type LoadPlugin = unsafe extern "C" fn(*const c_char, *mut *mut c_void) -> *mut c_void;
|
||||
type Unref = unsafe extern "C" fn(*mut c_void);
|
||||
|
||||
pub(super) struct Runtime {
|
||||
_libraries: Vec<ManuallyDrop<Library>>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
pub(super) fn initialize() -> io::Result<Self> {
|
||||
if RUNTIME_ENVIRONMENT.iter().any(|(key, value)| {
|
||||
std::env::var_os(key).as_deref() != Some(std::ffi::OsStr::new(value))
|
||||
}) {
|
||||
return Err(io::Error::other("private runtime environment is required"));
|
||||
}
|
||||
let executable = std::env::current_exe()?.canonicalize()?;
|
||||
let bin = executable.parent().ok_or_else(runtime_error)?;
|
||||
let root = bin.parent().ok_or_else(runtime_error)?;
|
||||
if bin.file_name().is_none_or(|name| name != "bin")
|
||||
|| root.file_name().is_none_or(|name| name != "voice")
|
||||
|| root
|
||||
.parent()
|
||||
.and_then(Path::file_name)
|
||||
.is_none_or(|name| name != "codex-resources")
|
||||
{
|
||||
return Err(runtime_error());
|
||||
}
|
||||
let (core, directory, prefix, suffix) = if cfg!(target_os = "macos") {
|
||||
(
|
||||
"lib/libgstreamer-1.0.0.dylib",
|
||||
"plugins",
|
||||
"libgst",
|
||||
".dylib",
|
||||
)
|
||||
} else if cfg!(all(target_os = "linux", target_env = "gnu")) {
|
||||
(
|
||||
"lib/libgstreamer-1.0.so.0",
|
||||
"lib/gstreamer-1.0",
|
||||
"libgst",
|
||||
".so",
|
||||
)
|
||||
} else if cfg!(all(windows, target_env = "msvc")) {
|
||||
("bin/gstreamer-1.0-0.dll", "bin", "gst", ".dll")
|
||||
} else {
|
||||
return Err(io::Error::other("unsupported native runtime platform"));
|
||||
};
|
||||
let core = private_file(root, core)?;
|
||||
let plugins = [
|
||||
"app",
|
||||
"audioconvert",
|
||||
"audioresample",
|
||||
"coreelements",
|
||||
"opus",
|
||||
"rtp",
|
||||
"rtpmanager",
|
||||
]
|
||||
.map(|name| private_file(root, &format!("{directory}/{prefix}{name}{suffix}")))
|
||||
.into_iter()
|
||||
.collect::<io::Result<Vec<_>>>()?;
|
||||
let library = load(&core)?;
|
||||
// SAFETY: These are stable GStreamer 1.x C signatures. The library stays
|
||||
// loaded through process exit, and all calls occur on this one worker.
|
||||
let (init, load_plugin, unref) = unsafe {
|
||||
(
|
||||
*library
|
||||
.get::<Init>(b"gst_init_check\0")
|
||||
.map_err(|_| runtime_error())?,
|
||||
*library
|
||||
.get::<LoadPlugin>(b"gst_plugin_load_file\0")
|
||||
.map_err(|_| runtime_error())?,
|
||||
*library
|
||||
.get::<Unref>(b"gst_object_unref\0")
|
||||
.map_err(|_| runtime_error())?,
|
||||
)
|
||||
};
|
||||
let mut libraries = vec![library];
|
||||
// SAFETY: GStreamer accepts null argc/argv and an omitted error output.
|
||||
// Fixed child settings disable registry reads, scans, writes and forking.
|
||||
if unsafe { init(ptr::null_mut(), ptr::null_mut(), ptr::null_mut()) } == 0 {
|
||||
return Err(runtime_error());
|
||||
}
|
||||
for plugin in plugins {
|
||||
// Preload with the restricted OS search policy before GStreamer
|
||||
// opens the same module, including its private dependencies on Windows.
|
||||
libraries.push(load(&plugin)?);
|
||||
#[cfg(unix)]
|
||||
let filename = {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
CString::new(plugin.as_os_str().as_bytes())
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let filename = CString::new(plugin.to_str().ok_or_else(runtime_error)?);
|
||||
let filename = filename.map_err(|_| runtime_error())?;
|
||||
// SAFETY: The NUL-terminated filename lives through the call. A
|
||||
// non-null result owns a plugin reference, released exactly once.
|
||||
unsafe {
|
||||
let plugin = load_plugin(filename.as_ptr(), ptr::null_mut());
|
||||
if plugin.is_null() {
|
||||
return Err(runtime_error());
|
||||
}
|
||||
unref(plugin);
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
_libraries: libraries,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn private_file(root: &Path, relative: &str) -> io::Result<PathBuf> {
|
||||
let path = root.join(relative);
|
||||
if path.canonicalize()? != path || !path.is_file() {
|
||||
return Err(runtime_error());
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn load(path: &Path) -> io::Result<ManuallyDrop<Library>> {
|
||||
// SAFETY: Callers supply only absolute, physical paths inside the installed
|
||||
// trusted package. Native code runs only in this expendable helper process.
|
||||
#[cfg(unix)]
|
||||
let library = unsafe {
|
||||
libloading::os::unix::Library::open(
|
||||
Some(path),
|
||||
libloading::os::unix::RTLD_NOW | libloading::os::unix::RTLD_LOCAL,
|
||||
)
|
||||
.map(Library::from)
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let library = unsafe {
|
||||
libloading::os::windows::Library::load_with_flags(
|
||||
path,
|
||||
libloading::os::windows::LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
|
||||
| libloading::os::windows::LOAD_LIBRARY_SEARCH_SYSTEM32,
|
||||
)
|
||||
.map(Library::from)
|
||||
};
|
||||
library.map(ManuallyDrop::new).map_err(|_| runtime_error())
|
||||
}
|
||||
|
||||
fn runtime_error() -> io::Error {
|
||||
io::Error::other("private audio runtime initialization failed")
|
||||
}
|
||||
@@ -151,6 +151,9 @@ async fn installed_client_rejects_mixed_builds_and_missing_helper() -> Result<()
|
||||
.close()
|
||||
.await?;
|
||||
assert!(VoiceHost::connect(&package, "wrong-build").await.is_err());
|
||||
// Helper-only installations still handshake, but cannot claim native readiness.
|
||||
let host = VoiceHost::connect(&package, &build_commit().await?).await?;
|
||||
assert!(host.initialize_runtime().await.is_err());
|
||||
// The same executable elsewhere in the package must not become a fallback.
|
||||
fs::rename(&helper, bin.join(source.file_name().unwrap()))?;
|
||||
assert!(
|
||||
|
||||
88
codex-rs/voice-host/tests/packaged_runtime.rs
Normal file
88
codex-rs/voice-host/tests/packaged_runtime.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
//! Exercise native initialization with real prepared libraries in a relocated package.
|
||||
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use codex_install_context::InstallContext;
|
||||
use codex_realtime_webrtc::VoiceHost;
|
||||
use codex_utils_cargo_bin::cargo_bin;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEADLINE: Duration = Duration::from_secs(/*secs*/ 10);
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires CODEX_TEST_VOICE_RUNTIME containing real prepared native libraries"]
|
||||
async fn relocated_runtime_initializes_closes_and_rejects_duplicate_initialization() -> Result<()> {
|
||||
let source = std::env::var_os("CODEX_TEST_VOICE_RUNTIME")
|
||||
.context("set CODEX_TEST_VOICE_RUNTIME to a matching prepared runtime")?;
|
||||
let source = fs::canonicalize(source)?;
|
||||
ensure!(
|
||||
source.join("runtime.json").is_file(),
|
||||
"prepared runtime receipt missing"
|
||||
);
|
||||
let directory = tempfile::Builder::new()
|
||||
.prefix("voice native package ")
|
||||
.tempdir()?;
|
||||
let root = directory.path().join("staging");
|
||||
let runtime = root.join("codex-resources/voice");
|
||||
fs::create_dir_all(&runtime)?;
|
||||
let mut pending = vec![(source, runtime.clone())];
|
||||
while let Some((source, destination)) = pending.pop() {
|
||||
for entry in fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
let target = destination.join(entry.file_name());
|
||||
let kind = entry.file_type()?;
|
||||
if kind.is_dir() {
|
||||
fs::create_dir(&target)?;
|
||||
pending.push((entry.path(), target));
|
||||
} else {
|
||||
ensure!(kind.is_file(), "runtime inputs must be physical files");
|
||||
fs::copy(entry.path(), target)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let helper_source = cargo_bin("codex-voice-host")?;
|
||||
let name = helper_source.file_name().context("helper filename")?;
|
||||
fs::create_dir_all(runtime.join("bin"))?;
|
||||
fs::copy(&helper_source, runtime.join("bin").join(name))?;
|
||||
fs::create_dir(root.join("bin"))?;
|
||||
let app_name = if cfg!(windows) { "codex.exe" } else { "codex" };
|
||||
fs::write(root.join("bin").join(app_name), [])?;
|
||||
fs::write(root.join("codex-package.json"), "{}")?;
|
||||
let moved = directory.path().join("relocated package");
|
||||
fs::rename(root, &moved)?;
|
||||
let package = InstallContext::from_exe(
|
||||
/*is_macos*/ cfg!(target_os = "macos"),
|
||||
Some(&moved.join("bin").join(app_name)),
|
||||
/*method_override*/ None,
|
||||
)
|
||||
.package_layout
|
||||
.context("package layout")?;
|
||||
let output = timeout(
|
||||
DEADLINE,
|
||||
Command::new(&helper_source)
|
||||
.arg("--build-commit")
|
||||
.kill_on_drop(true)
|
||||
.output(),
|
||||
)
|
||||
.await??;
|
||||
ensure!(output.status.success(), "helper build identity failed");
|
||||
let build_commit = String::from_utf8(output.stdout)?.trim().to_owned();
|
||||
VoiceHost::connect(&package, &build_commit)
|
||||
.await?
|
||||
.initialize_runtime()
|
||||
.await?
|
||||
.close()
|
||||
.await?;
|
||||
|
||||
let host = VoiceHost::connect(&package, &build_commit)
|
||||
.await?
|
||||
.initialize_runtime()
|
||||
.await?;
|
||||
assert!(host.initialize_runtime().await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
5
defs.bzl
5
defs.bzl
@@ -227,7 +227,8 @@ def codex_rust_crate(
|
||||
Crates are only compiled in a single configuration across the workspace, i.e.
|
||||
with all features in this list enabled. So use sparingly, and prefer to refactor
|
||||
optional functionality to a separate crate.
|
||||
crate_srcs: Optional explicit srcs; defaults to `src/**/*.rs`.
|
||||
crate_srcs: Optional explicit library srcs; [] disables the library target.
|
||||
Defaults to `src/**/*.rs` excluding binary entrypoints.
|
||||
crate_edition: Rust edition override, if not default.
|
||||
You probably don't want this, it's only here for a single caller.
|
||||
proc_macro: Whether this crate builds a proc-macro library.
|
||||
@@ -303,7 +304,7 @@ def codex_rust_crate(
|
||||
|
||||
binaries = DEP_DATA.get(native.package_name())["binaries"]
|
||||
|
||||
lib_srcs = crate_srcs or native.glob(["src/**/*.rs"], exclude = binaries.values(), allow_empty = True)
|
||||
lib_srcs = crate_srcs if crate_srcs != None else native.glob(["src/**/*.rs"], exclude = binaries.values(), allow_empty = True)
|
||||
|
||||
maybe_deps = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user