From d979df154cf60e13eafb5453e75b6d84f21c67bf Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Thu, 3 Sep 2026 22:41:26 +0000 Subject: [PATCH] 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 --- codex-rs/Cargo.lock | 1 + codex-rs/realtime-webrtc/src/client.rs | 31 +++- codex-rs/realtime-webrtc/src/client_tests.rs | 33 ++++ codex-rs/realtime-webrtc/src/lib.rs | 1 + codex-rs/realtime-webrtc/src/protocol.rs | 16 ++ codex-rs/voice-host/BUILD.bazel | 1 + codex-rs/voice-host/Cargo.toml | 1 + codex-rs/voice-host/README.md | 26 ++- codex-rs/voice-host/src/main.rs | 35 +++- codex-rs/voice-host/src/runtime.rs | 161 ++++++++++++++++++ codex-rs/voice-host/tests/lifecycle.rs | 3 + codex-rs/voice-host/tests/packaged_runtime.rs | 88 ++++++++++ defs.bzl | 5 +- 13 files changed, 386 insertions(+), 16 deletions(-) create mode 100644 codex-rs/voice-host/src/runtime.rs create mode 100644 codex-rs/voice-host/tests/packaged_runtime.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 782f8d2535..963bd95750 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4908,6 +4908,7 @@ dependencies = [ "codex-process-hardening", "codex-realtime-webrtc", "codex-utils-cargo-bin", + "libloading", "pretty_assertions", "tempfile", "tokio", diff --git a/codex-rs/realtime-webrtc/src/client.rs b/codex-rs/realtime-webrtc/src/client.rs index 3073ca4dd2..81dc9c741a 100644 --- a/codex-rs/realtime-webrtc/src/client.rs +++ b/codex-rs/realtime-webrtc/src/client.rs @@ -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.exchange( + Message::InitializeRuntime {}, + Message::RuntimeReady {}, + RUNTIME_INITIALIZATION_DEADLINE, + ) + .await?; + Ok(self) + } + pub async fn connect(package: &CodexPackageLayout, build_commit: &str) -> Result { 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) -> 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() } diff --git a/codex-rs/realtime-webrtc/src/client_tests.rs b/codex-rs/realtime-webrtc/src/client_tests.rs index fe992a5632..b5256bee47 100644 --- a/codex-rs/realtime-webrtc/src/client_tests.rs +++ b/codex-rs/realtime-webrtc/src/client_tests.rs @@ -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(()) +} diff --git a/codex-rs/realtime-webrtc/src/lib.rs b/codex-rs/realtime-webrtc/src/lib.rs index 281808152b..5a15b00c1c 100644 --- a/codex-rs/realtime-webrtc/src/lib.rs +++ b/codex-rs/realtime-webrtc/src/lib.rs @@ -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; diff --git a/codex-rs/realtime-webrtc/src/protocol.rs b/codex-rs/realtime-webrtc/src/protocol.rs index 847529facb..a494d27e61 100644 --- a/codex-rs/realtime-webrtc/src/protocol.rs +++ b/codex-rs/realtime-webrtc/src/protocol.rs @@ -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 {}, } diff --git a/codex-rs/voice-host/BUILD.bazel b/codex-rs/voice-host/BUILD.bazel index fca93d945d..ad24a4a1bf 100644 --- a/codex-rs/voice-host/BUILD.bazel +++ b/codex-rs/voice-host/BUILD.bazel @@ -3,4 +3,5 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "voice-host", crate_name = "codex_voice_host", + crate_srcs = [], ) diff --git a/codex-rs/voice-host/Cargo.toml b/codex-rs/voice-host/Cargo.toml index c3da4ed7b3..f5ce0f8356 100644 --- a/codex-rs/voice-host/Cargo.toml +++ b/codex-rs/voice-host/Cargo.toml @@ -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 } diff --git a/codex-rs/voice-host/README.md b/codex-rs/voice-host/README.md index 17d0f3e130..847014bd22 100644 --- a/codex-rs/voice-host/README.md +++ b/codex-rs/voice-host/README.md @@ -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. diff --git a/codex-rs/voice-host/src/main.rs b/codex-rs/voice-host/src/main.rs index b4760859d0..e562bb4295 100644 --- a/codex-rs/voice-host/src/main.rs +++ b/codex-rs/voice-host/src/main.rs @@ -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()?; } } diff --git a/codex-rs/voice-host/src/runtime.rs b/codex-rs/voice-host/src/runtime.rs new file mode 100644 index 0000000000..0c795188d4 --- /dev/null +++ b/codex-rs/voice-host/src/runtime.rs @@ -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>, +} + +impl Runtime { + pub(super) fn initialize() -> io::Result { + 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::>>()?; + 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::(b"gst_init_check\0") + .map_err(|_| runtime_error())?, + *library + .get::(b"gst_plugin_load_file\0") + .map_err(|_| runtime_error())?, + *library + .get::(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 { + let path = root.join(relative); + if path.canonicalize()? != path || !path.is_file() { + return Err(runtime_error()); + } + Ok(path) +} + +fn load(path: &Path) -> io::Result> { + // 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") +} diff --git a/codex-rs/voice-host/tests/lifecycle.rs b/codex-rs/voice-host/tests/lifecycle.rs index 76baa13a34..efa4a62cf7 100644 --- a/codex-rs/voice-host/tests/lifecycle.rs +++ b/codex-rs/voice-host/tests/lifecycle.rs @@ -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!( diff --git a/codex-rs/voice-host/tests/packaged_runtime.rs b/codex-rs/voice-host/tests/packaged_runtime.rs new file mode 100644 index 0000000000..91c9daa526 --- /dev/null +++ b/codex-rs/voice-host/tests/packaged_runtime.rs @@ -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(()) +} diff --git a/defs.bzl b/defs.bzl index 48922cd696..7a18fd096e 100644 --- a/defs.bzl +++ b/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 = []