diff --git a/MODULE.bazel b/MODULE.bazel index b42936fde5..8a82e09d3b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -278,6 +278,74 @@ http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "ht http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") new_local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository") +http_archive( + name = "ninja_1.11.0_linux", + urls = [ + "https://github.com/ninja-build/ninja/releases/download/v1.11.0/ninja-linux.zip", + ], + sha256 = "9726e730d5b8599f82654dc80265e64a10a8a817552c34153361ed0c017f9f02", + strip_prefix = "", + build_file_content = """\ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "ninja_bin", + srcs = ["ninja"], +) +""", +) + +http_archive( + name = "ninja_1.12.1_linux_aarch64", + urls = [ + "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux-aarch64.zip", + ], + sha256 = "5c25c6570b0155e95fce5918cb95f1ad9870df5768653afe128db822301a05a1", + strip_prefix = "", + build_file_content = """\ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "ninja_bin", + srcs = ["ninja"], +) +""", +) + +http_archive( + name = "ninja_1.11.0_mac", + urls = [ + "https://github.com/ninja-build/ninja/releases/download/v1.11.0/ninja-mac.zip", + ], + sha256 = "21915277db59756bfc61f6f281c1f5e3897760b63776fd3d360f77dd7364137f", + strip_prefix = "", + build_file_content = """\ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "ninja_bin", + srcs = ["ninja"], +) +""", +) + +http_archive( + name = "ninja_1.11.0_win", + urls = [ + "https://github.com/ninja-build/ninja/releases/download/v1.11.0/ninja-win.zip", + ], + sha256 = "d0ee3da143211aa447e750085876c9b9d7bcdd637ab5b2c5b41349c617f22f3b", + strip_prefix = "", + build_file_content = """\ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "ninja_bin", + srcs = ["ninja.exe"], +) +""", +) + new_local_repository( name = "v8_targets", build_file = "//third_party/v8:BUILD.bazel", @@ -345,6 +413,41 @@ crate.annotation( inject_repo(crate, "alsa_lib") +bazel_dep(name = "meson", version = "1.5.1.bcr.1") + +crate.annotation( + build_script_tools = [ + "@meson//:meson", + "@ninja_1.11.0_linux//:ninja_bin", + "@ninja_1.12.1_linux_aarch64//:ninja_bin", + "@ninja_1.11.0_mac//:ninja_bin", + "@ninja_1.11.0_win//:ninja_bin", + ], + build_script_env = { + "MESON": "$(execpath @meson//:meson)", + }, + build_script_env_select = { + "aarch64-apple-darwin": "{\"NINJA_REAL\":\"$(execpath @ninja_1.12.1_linux_aarch64//:ninja_bin)\"}", + "x86_64-apple-darwin": "{\"NINJA_REAL\":\"$(execpath @ninja_1.12.1_linux_aarch64//:ninja_bin)\"}", + "aarch64-unknown-linux-gnu": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}", + "x86_64-unknown-linux-gnu": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}", + "aarch64-unknown-linux-musl": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}", + "x86_64-unknown-linux-musl": "{\"NINJA_REAL\":\"$(execpath @ninja_1.11.0_linux//:ninja_bin)\"}", + "aarch64-pc-windows-msvc": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}", + "x86_64-pc-windows-msvc": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}", + "aarch64-pc-windows-gnullvm": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}", + "x86_64-pc-windows-gnullvm": "{\"NINJA\":\"$(execpath @ninja_1.11.0_win//:ninja_bin)\"}", + }, + crate = "webrtc-audio-processing-sys", + gen_build_script = "on", + patch_args = ["-p1"], + patches = [ + "//patches:webrtc-audio-processing-sys_meson_env.patch", + "//patches:webrtc-audio-processing-sys_logging_macos_sdk.patch", + ], +) + +inject_repo(crate, "meson", "ninja_1.11.0_linux", "ninja_1.12.1_linux_aarch64", "ninja_1.11.0_mac", "ninja_1.11.0_win") bazel_dep(name = "v8", version = "14.6.202.9") archive_override( module_name = "v8", diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index f420a7e664..acc0a78f81 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -88,6 +88,8 @@ "https://bcr.bazel.build/modules/llvm/0.6.7/MODULE.bazel": "d37a2e10571864dc6a5bb53c29216d90b9400bbcadb422337f49107fd2eaf0d2", "https://bcr.bazel.build/modules/llvm/0.6.8/MODULE.bazel": "53468e4a4be409c2d34e5b7331d2e1fef982151b777655ca3c0047225b333629", "https://bcr.bazel.build/modules/llvm/0.6.8/source.json": "b673af466f716e01d6243f59e47729e99f37dc5e17026d2bf18c98206f09b6c5", + "https://bcr.bazel.build/modules/meson/1.5.1.bcr.1/MODULE.bazel": "fd48110cf099881cb57d7bd98f81d4fba231956b96df3ab5352c03950371701c", + "https://bcr.bazel.build/modules/meson/1.5.1.bcr.1/source.json": "98f39eef2bac9db11b898013f748668ccb7f94cb072519d754d2c1c8a89261e7", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", "https://bcr.bazel.build/modules/openssl/3.5.4.bcr.0/MODULE.bazel": "0f6b8f20b192b9ff0781406256150bcd46f19e66d807dcb0c540548439d6fc35", @@ -105,6 +107,8 @@ "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/23.1/MODULE.bazel": "88b393b3eb4101d18129e5db51847cd40a5517a53e81216144a8c32dfeeca52a", + "https://bcr.bazel.build/modules/protobuf/24.4/MODULE.bazel": "7bc7ce5f2abf36b3b7b7c8218d3acdebb9426aeb35c2257c96445756f970eb12", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", @@ -151,6 +155,7 @@ "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.1.0/MODULE.bazel": "30d9135a2b6561c761bd67bd4990da591e6bdc128790ce3e7afd6a3558b2fb64", "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", @@ -194,6 +199,7 @@ "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", + "https://bcr.bazel.build/modules/rules_python/0.35.0/MODULE.bazel": "c3657951764cdcdb5a7370d5e885fad5e8c1583320aad18d46f9f110d2c22755", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", "https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43", "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", @@ -226,6 +232,7 @@ "https://bcr.bazel.build/modules/tar.bzl/0.9.0/MODULE.bazel": "452a22d7f02b1c9d7a22ab25edf20f46f3e1101f0f67dc4bfbf9a474ddf02445", "https://bcr.bazel.build/modules/tar.bzl/0.9.0/source.json": "c732760a374831a2cf5b08839e4be75017196b4d796a5aa55235272ee17cd839", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/upb/0.0.0-20230516-61a97ef/MODULE.bazel": "c0df5e35ad55e264160417fd0875932ee3c9dda63d9fccace35ac62f45e1b6f9", "https://bcr.bazel.build/modules/with_cfg.bzl/0.12.0/MODULE.bazel": "b573395fe63aef4299ba095173e2f62ccfee5ad9bbf7acaa95dba73af9fc2b38", "https://bcr.bazel.build/modules/with_cfg.bzl/0.12.0/source.json": "3f3fbaeafecaf629877ad152a2c9def21f8d330d91aa94c5dc75bbb98c10b8b8", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", @@ -646,6 +653,7 @@ "atoi_2.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.14\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"num-traits/std\"]}}", "atomic-waker_1.1.2": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.5\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.7.0\"}],\"features\":{}}", "autocfg_1.5.0": "{\"dependencies\":[],\"features\":{}}", + "autotools_0.2.7": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.0.3\"}],\"features\":{}}", "aws-lc-rs_1.16.2": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.39.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", "aws-lc-sys_0.39.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"fips\":[\"dep:bindgen\"],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}", "axum-core_0.5.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", @@ -1553,6 +1561,9 @@ "webpki-root-certs_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"}],\"features\":{}}", "webpki-roots_0.26.11": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"parent\",\"package\":\"webpki-roots\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.102\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", "webpki-roots_1.0.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"percent-encoding\",\"req\":\"^2.3\"},{\"default_features\":false,\"name\":\"pki-types\",\"package\":\"rustls-pki-types\",\"req\":\"^1.8\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"ring\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"rustls\",\"req\":\"^0.23\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"webpki\",\"package\":\"rustls-webpki\",\"req\":\"^0.103\"},{\"kind\":\"dev\",\"name\":\"x509-parser\",\"req\":\"^0.17.0\"},{\"kind\":\"dev\",\"name\":\"yasna\",\"req\":\"^0.5.2\"}],\"features\":{}}", + "webrtc-audio-processing-config_2.0.3": "{\"dependencies\":[{\"name\":\"schemars\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"strum\",\"optional\":true,\"req\":\"^0.27\"}],\"features\":{\"schemars\":[\"dep:schemars\"],\"serde\":[\"dep:serde\"],\"strum\":[\"dep:strum\"]}}", + "webrtc-audio-processing-sys_2.0.3": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"autotools\",\"req\":\"^0.2\"},{\"kind\":\"build\",\"name\":\"bindgen\",\"req\":\"^0.72\"},{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3\"},{\"kind\":\"build\",\"name\":\"regex\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"bundled\":[],\"experimental-aec3-config\":[\"bundled\"],\"serde\":[\"dep:serde\"]}}", + "webrtc-audio-processing_2.0.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-channel\",\"req\":\"^0.5\"},{\"features\":[\"termination\"],\"kind\":\"dev\",\"name\":\"ctrlc\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"hound\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"json5\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"portaudio\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_ignored\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"structopt\",\"req\":\"^0.3\"},{\"name\":\"webrtc-audio-processing-config\",\"req\":\"^2.0.3\"},{\"name\":\"webrtc-audio-processing-sys\",\"req\":\"^2.0.3\"}],\"features\":{\"bundled\":[\"webrtc-audio-processing-sys/bundled\"],\"experimental-aec3-config\":[\"webrtc-audio-processing-sys/experimental-aec3-config\"],\"schemars\":[\"webrtc-audio-processing-config/schemars\"],\"serde\":[\"webrtc-audio-processing-config/serde\",\"webrtc-audio-processing-sys/serde\",\"dep:serde\"],\"strum\":[\"webrtc-audio-processing-config/strum\"]}}", "webrtc-data_0.17.1": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.28\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"portable-atomic\",\"req\":\"^1.6\"},{\"name\":\"sctp\",\"package\":\"webrtc-sctp\",\"req\":\"^0.17.1\"},{\"name\":\"thiserror\",\"req\":\"^1\"},{\"features\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"rt\",\"rt-multi-thread\",\"sync\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.32.0\"},{\"default_features\":false,\"features\":[\"conn\",\"marshal\"],\"name\":\"util\",\"package\":\"webrtc-util\",\"req\":\"^0.17.1\"}],\"features\":{}}", "webrtc-ice_0.17.1": "{\"dependencies\":[{\"name\":\"arc-swap\",\"req\":\"^1\"},{\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.28\"},{\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3\"},{\"name\":\"crc\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.3\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14.27\"},{\"kind\":\"dev\",\"name\":\"ipnet\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"mdns\",\"package\":\"webrtc-mdns\",\"req\":\"^0.17.1\"},{\"name\":\"portable-atomic\",\"req\":\"^1.6\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.9.5\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.10\"},{\"name\":\"stun\",\"req\":\"^0.17.0\"},{\"name\":\"thiserror\",\"req\":\"^1\"},{\"features\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"rt\",\"rt-multi-thread\",\"sync\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.32.0\"},{\"name\":\"turn\",\"req\":\"^0.17.1\"},{\"name\":\"url\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"conn\",\"vnet\",\"sync\"],\"name\":\"util\",\"package\":\"webrtc-util\",\"req\":\"^0.17.1\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"req\":\"^1\"},{\"name\":\"waitgroup\",\"req\":\"^0.1\"}],\"features\":{}}", "webrtc-mdns_0.17.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4.28\"},{\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"req\":\"^0.5\"},{\"name\":\"thiserror\",\"req\":\"^1\"},{\"features\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"rt\",\"rt-multi-thread\",\"sync\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.32.0\"},{\"default_features\":false,\"features\":[\"ifaces\"],\"name\":\"util\",\"package\":\"webrtc-util\",\"req\":\"^0.17.1\"}],\"features\":{\"default\":[\"reuse_port\"],\"reuse_port\":[]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 87818789a8..f44545577f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -790,6 +790,15 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "autotools" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" +dependencies = [ + "cc", +] + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -2943,6 +2952,7 @@ dependencies = [ "uuid", "vt100", "webbrowser", + "webrtc-audio-processing", "which 8.0.0", "windows-sys 0.52.0", "winsplit", @@ -11879,6 +11889,37 @@ dependencies = [ "webrtc-util", ] +[[package]] +name = "webrtc-audio-processing" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31cc7cbf143d9c3de985b113ddc3fddb4a60c7746635a69114f94a5b5af8f42" +dependencies = [ + "webrtc-audio-processing-config", + "webrtc-audio-processing-sys", +] + +[[package]] +name = "webrtc-audio-processing-config" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5a1fcf911c54bf3d0e022020f1626ae99add5728c65d345bacfbf3adbf2ef2" + +[[package]] +name = "webrtc-audio-processing-sys" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd9fcfbedf79b0c7bc0f627d4e9cd5efcb648dcfc271c15503194482b39b0e8" +dependencies = [ + "anyhow", + "autotools", + "bindgen", + "cc", + "fs_extra", + "pkg-config", + "regex", +] + [[package]] name = "webrtc-data" version = "0.17.1" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index a02dcb1c6e..f19018a922 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -352,6 +352,7 @@ vt100 = "0.16.2" walkdir = "2.5.0" webbrowser = "1.0" webrtc = "0.17.1" +webrtc-audio-processing = { version = "~2.0", features = ["bundled"] } which = "8" wildmatch = "2.6.1" zip = "2.4.2" diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 696ab503b3..e4e2b23071 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -113,6 +113,7 @@ tokio-util = { workspace = true, features = ["time"] } [target.'cfg(not(target_os = "linux"))'.dependencies] cpal = "0.15" +webrtc-audio-processing = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 08e8d237b5..67aeade3a3 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -155,11 +155,6 @@ impl AppServerSession { } } - pub(crate) fn with_remote_cwd_override(mut self, remote_cwd_override: Option) -> Self { - self.remote_cwd_override = remote_cwd_override; - self - } - pub(crate) fn remote_cwd_override(&self) -> Option<&std::path::Path> { self.remote_cwd_override.as_deref() } diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index 1d350f0669..adfd42b37b 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -1,4 +1,6 @@ use super::*; +#[cfg(not(target_os = "linux"))] +use crate::realtime_audio_processing::RealtimeAudioProcessor; use codex_protocol::protocol::ConversationStartParams; use codex_protocol::protocol::RealtimeAudioFrame; use codex_protocol::protocol::RealtimeConversationClosedEvent; @@ -30,6 +32,8 @@ pub(super) struct RealtimeConversationUiState { #[cfg(not(target_os = "linux"))] capture_stop_flag: Option>, #[cfg(not(target_os = "linux"))] + audio_processor: Option, + #[cfg(not(target_os = "linux"))] capture: Option, #[cfg(not(target_os = "linux"))] audio_player: Option, @@ -331,9 +335,26 @@ impl ChatWidget { fn enqueue_realtime_audio_out(&mut self, frame: &RealtimeAudioFrame) { #[cfg(not(target_os = "linux"))] { + if !self.realtime_conversation.is_active() { + return; + } if self.realtime_conversation.audio_player.is_none() { - self.realtime_conversation.audio_player = - crate::voice::RealtimeAudioPlayer::start(&self.config).ok(); + let Some(audio_processor) = self.realtime_conversation.audio_processor.clone() + else { + self.fail_realtime_conversation( + "Realtime audio processor was unavailable".to_string(), + ); + return; + }; + match crate::voice::RealtimeAudioPlayer::start(&self.config, audio_processor) { + Ok(player) => self.realtime_conversation.audio_player = Some(player), + Err(err) => { + self.fail_realtime_conversation(format!( + "Failed to start speaker output: {err}" + )); + return; + } + } } if let Some(player) = &self.realtime_conversation.audio_player && let Err(err) = player.enqueue_frame(frame) @@ -367,12 +388,42 @@ impl ChatWidget { self.realtime_conversation.meter_placeholder_id = Some(placeholder_id.clone()); self.request_redraw(); + let audio_processor = match RealtimeAudioProcessor::new() { + Ok(audio_processor) => audio_processor, + Err(err) => { + self.realtime_conversation.meter_placeholder_id = None; + self.remove_recording_meter_placeholder(&placeholder_id); + self.fail_realtime_conversation(format!( + "Failed to start realtime audio processor: {err}" + )); + return; + } + }; + self.realtime_conversation.audio_processor = Some(audio_processor.clone()); + + let audio_player = + match crate::voice::RealtimeAudioPlayer::start(&self.config, audio_processor.clone()) { + Ok(player) => player, + Err(err) => { + self.realtime_conversation.audio_processor = None; + self.realtime_conversation.meter_placeholder_id = None; + self.remove_recording_meter_placeholder(&placeholder_id); + self.fail_realtime_conversation(format!( + "Failed to start speaker output: {err}" + )); + return; + } + }; + let capture = match crate::voice::VoiceCapture::start_realtime( &self.config, self.app_event_tx.clone(), + audio_processor, ) { Ok(capture) => capture, Err(err) => { + drop(audio_player); + self.realtime_conversation.audio_processor = None; self.realtime_conversation.meter_placeholder_id = None; self.remove_recording_meter_placeholder(&placeholder_id); self.fail_realtime_conversation(format!( @@ -389,10 +440,7 @@ impl ChatWidget { self.realtime_conversation.capture_stop_flag = Some(stop_flag.clone()); self.realtime_conversation.capture = Some(capture); - if self.realtime_conversation.audio_player.is_none() { - self.realtime_conversation.audio_player = - crate::voice::RealtimeAudioPlayer::start(&self.config).ok(); - } + self.realtime_conversation.audio_player = Some(audio_player); std::thread::spawn(move || { let mut meter = crate::voice::RecordingMeterState::new(); @@ -423,23 +471,10 @@ impl ChatWidget { } match kind { - RealtimeAudioDeviceKind::Microphone => { - self.stop_realtime_microphone(); + RealtimeAudioDeviceKind::Microphone | RealtimeAudioDeviceKind::Speaker => { + self.stop_realtime_local_audio(); self.start_realtime_local_audio(); } - RealtimeAudioDeviceKind::Speaker => { - self.stop_realtime_speaker(); - match crate::voice::RealtimeAudioPlayer::start(&self.config) { - Ok(player) => { - self.realtime_conversation.audio_player = Some(player); - } - Err(err) => { - self.fail_realtime_conversation(format!( - "Failed to start speaker output: {err}" - )); - } - } - } } self.request_redraw(); } @@ -453,6 +488,7 @@ impl ChatWidget { fn stop_realtime_local_audio(&mut self) { self.stop_realtime_microphone(); self.stop_realtime_speaker(); + self.realtime_conversation.audio_processor = None; } #[cfg(target_os = "linux")] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 991c91c550..298ea6e081 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -130,6 +130,8 @@ pub mod onboarding; mod oss_selection; mod pager_overlay; pub mod public_widgets; +#[cfg(not(target_os = "linux"))] +mod realtime_audio_processing; mod render; mod resume_picker; mod selection_list; @@ -577,42 +579,15 @@ fn latest_session_lookup_params( source_kinds: (!include_non_interactive) .then_some(vec![ThreadSourceKind::Cli, ThreadSourceKind::VsCode]), archived: Some(false), - cwd: cwd_filter.map(|cwd| cwd.to_string_lossy().to_string()), + cwd: if is_remote { + None + } else { + cwd_filter.map(|cwd| cwd.to_string_lossy().to_string()) + }, search_term: None, } } -fn config_cwd_for_app_server_target( - cwd: Option<&Path>, - app_server_target: &AppServerTarget, -) -> std::io::Result { - if matches!(app_server_target, AppServerTarget::Remote { .. }) { - return AbsolutePathBuf::current_dir(); - } - - match cwd { - Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?), - None => AbsolutePathBuf::current_dir(), - } -} - -fn latest_session_cwd_filter<'a>( - remote_mode: bool, - remote_cwd_override: Option<&'a Path>, - config: &'a Config, - show_all: bool, -) -> Option<&'a Path> { - if show_all { - return None; - } - - if remote_mode { - remote_cwd_override - } else { - Some(config.cwd.as_path()) - } -} - pub async fn run_main( mut cli: Cli, arg0_paths: Arg0DispatchPaths, @@ -631,10 +606,6 @@ pub async fn run_main( auth_token: remote_auth_token.clone(), }) .unwrap_or(AppServerTarget::Embedded); - let remote_cwd_override = cli - .cwd - .clone() - .filter(|_| matches!(app_server_target, AppServerTarget::Remote { .. })); let (sandbox_mode, approval_policy) = if cli.full_auto { ( Some(SandboxMode::WorkspaceWrite), @@ -685,7 +656,10 @@ pub async fn run_main( }; let cwd = cli.cwd.clone(); - let config_cwd = config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target)?; + let config_cwd = match cwd.as_deref() { + Some(path) => AbsolutePathBuf::from_absolute_path(path.canonicalize()?)?, + None => AbsolutePathBuf::current_dir()?, + }; #[allow(clippy::print_stderr)] let config_toml = match load_config_as_toml_with_cli_overrides( @@ -773,11 +747,7 @@ pub async fn run_main( model, approval_policy, sandbox_mode, - cwd: if matches!(app_server_target, AppServerTarget::Remote { .. }) { - None - } else { - cwd - }, + cwd, model_provider: model_provider_override.clone(), config_profile: cli.config_profile.clone(), codex_self_exe: arg0_paths.codex_self_exe.clone(), @@ -939,7 +909,6 @@ pub async fn run_main( arg0_paths, loader_overrides, app_server_target, - remote_cwd_override, config, overrides, cli_kv_overrides, @@ -958,7 +927,6 @@ async fn run_ratatui_app( arg0_paths: Arg0DispatchPaths, loader_overrides: LoaderOverrides, app_server_target: AppServerTarget, - remote_cwd_override: Option, initial_config: Config, overrides: ConfigOverrides, cli_kv_overrides: Vec<(String, toml::Value)>, @@ -1017,21 +985,18 @@ async fn run_ratatui_app( let needs_onboarding_app_server = should_show_trust_screen_flag || initial_config.model_provider.requires_openai_auth; let mut onboarding_app_server = if needs_onboarding_app_server { - Some( - AppServerSession::new( - start_app_server( - &app_server_target, - arg0_paths.clone(), - initial_config.clone(), - cli_kv_overrides.clone(), - loader_overrides.clone(), - cloud_requirements.clone(), - feedback.clone(), - ) - .await?, + Some(AppServerSession::new( + start_app_server( + &app_server_target, + arg0_paths.clone(), + initial_config.clone(), + cli_kv_overrides.clone(), + loader_overrides.clone(), + cloud_requirements.clone(), + feedback.clone(), ) - .with_remote_cwd_override(remote_cwd_override.clone()), - ) + .await?, + )) } else { None }; @@ -1134,21 +1099,18 @@ async fn run_ratatui_app( || cli.resume_picker || cli.fork_picker; let mut session_lookup_app_server = if needs_app_server_session_lookup { - Some( - AppServerSession::new( - start_app_server( - &app_server_target, - arg0_paths.clone(), - config.clone(), - cli_kv_overrides.clone(), - loader_overrides.clone(), - cloud_requirements.clone(), - feedback.clone(), - ) - .await?, + Some(AppServerSession::new( + start_app_server( + &app_server_target, + arg0_paths.clone(), + config.clone(), + cli_kv_overrides.clone(), + loader_overrides.clone(), + cloud_requirements.clone(), + feedback.clone(), ) - .with_remote_cwd_override(remote_cwd_override.clone()), - ) + .await?, + )) } else { None }; @@ -1167,21 +1129,12 @@ async fn run_ratatui_app( } } } else if cli.fork_last { - let filter_cwd = if remote_mode { - latest_session_cwd_filter( - remote_mode, - remote_cwd_override.as_deref(), - &config, - cli.fork_show_all, - ) - } else { - None - }; let Some(app_server) = session_lookup_app_server.as_mut() else { unreachable!("session lookup app server should be initialized for --fork --last"); }; match lookup_latest_session_target_with_app_server( - app_server, &config, filter_cwd, /*include_non_interactive*/ false, + app_server, &config, /*cwd_filter*/ None, + /*include_non_interactive*/ false, ) .await? { @@ -1228,12 +1181,11 @@ async fn run_ratatui_app( } } } else if cli.resume_last { - let filter_cwd = latest_session_cwd_filter( - remote_mode, - remote_cwd_override.as_deref(), - &config, - cli.resume_show_all, - ); + let filter_cwd = if cli.resume_show_all { + None + } else { + Some(config.cwd.as_path()) + }; let Some(app_server) = session_lookup_app_server.as_mut() else { unreachable!("session lookup app server should be initialized for --resume --last"); }; @@ -1384,7 +1336,7 @@ async fn run_ratatui_app( let app_result = App::run( &mut tui, - AppServerSession::new(app_server).with_remote_cwd_override(remote_cwd_override), + AppServerSession::new(app_server), config, cli_kv_overrides.clone(), overrides.clone(), @@ -1845,9 +1797,12 @@ mod tests { -> std::io::Result<()> { let temp_dir = TempDir::new()?; let config = build_config(&temp_dir).await?; + let cwd = temp_dir.path().join("project"); let params = latest_session_lookup_params( - /*is_remote*/ true, &config, /*cwd_filter*/ None, + /*is_remote*/ true, + &config, + Some(cwd.as_path()), /*include_non_interactive*/ false, ); @@ -1856,58 +1811,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions() - -> std::io::Result<()> { - let temp_dir = TempDir::new()?; - let config = build_config(&temp_dir).await?; - let cwd = Path::new("repo/on/server"); - - let params = latest_session_lookup_params( - /*is_remote*/ true, - &config, - Some(cwd), - /*include_non_interactive*/ false, - ); - - assert_eq!(params.model_providers, None); - assert_eq!(params.cwd.as_deref(), Some("repo/on/server")); - Ok(()) - } - - #[test] - fn config_cwd_for_app_server_target_uses_current_dir_for_remote_sessions() -> std::io::Result<()> - { - let remote_only_cwd = if cfg!(windows) { - Path::new(r"C:\definitely\not\local\to\this\test") - } else { - Path::new("/definitely/not/local/to/this/test") - }; - let target = AppServerTarget::Remote { - websocket_url: "ws://127.0.0.1:1234/".to_string(), - auth_token: None, - }; - - let config_cwd = config_cwd_for_app_server_target(Some(remote_only_cwd), &target)?; - - assert_eq!(config_cwd, AbsolutePathBuf::current_dir()?); - Ok(()) - } - - #[test] - fn config_cwd_for_app_server_target_canonicalizes_embedded_cli_cwd() -> std::io::Result<()> { - let temp_dir = TempDir::new()?; - let target = AppServerTarget::Embedded; - - let config_cwd = config_cwd_for_app_server_target(Some(temp_dir.path()), &target)?; - - assert_eq!( - config_cwd, - AbsolutePathBuf::from_absolute_path(temp_dir.path().canonicalize()?)? - ); - Ok(()) - } - #[tokio::test] async fn read_session_cwd_returns_none_without_sqlite_or_rollout_path() -> std::io::Result<()> { let temp_dir = TempDir::new()?; diff --git a/codex-rs/tui/src/realtime_audio_processing.rs b/codex-rs/tui/src/realtime_audio_processing.rs new file mode 100644 index 0000000000..5d9e0f55c1 --- /dev/null +++ b/codex-rs/tui/src/realtime_audio_processing.rs @@ -0,0 +1,237 @@ +use std::collections::VecDeque; +use std::sync::Arc; +use tracing::warn; +use webrtc_audio_processing::Config as AudioProcessingConfig; +use webrtc_audio_processing::Processor; +use webrtc_audio_processing::config::EchoCanceller; +use webrtc_audio_processing::config::GainController; +use webrtc_audio_processing::config::HighPassFilter; +use webrtc_audio_processing::config::NoiseSuppression; +use webrtc_audio_processing::config::NoiseSuppressionLevel; +use webrtc_audio_processing::config::Pipeline; + +pub(crate) const AUDIO_PROCESSING_SAMPLE_RATE: u32 = 24_000; +pub(crate) const AUDIO_PROCESSING_CHANNELS: u16 = 1; + +#[derive(Clone)] +pub(crate) struct RealtimeAudioProcessor { + processor: Arc, +} + +impl RealtimeAudioProcessor { + pub(crate) fn new() -> Result { + let processor = Processor::new(AUDIO_PROCESSING_SAMPLE_RATE) + .map_err(|err| format!("failed to initialize realtime audio processor: {err}"))?; + processor.set_config(AudioProcessingConfig { + pipeline: Pipeline { + multi_channel_capture: false, + multi_channel_render: false, + ..Default::default() + }, + echo_canceller: Some(EchoCanceller::Full { + stream_delay_ms: None, + }), + noise_suppression: Some(NoiseSuppression { + level: NoiseSuppressionLevel::High, + ..Default::default() + }), + gain_controller: Some(GainController::GainController2(Default::default())), + high_pass_filter: Some(HighPassFilter::default()), + ..Default::default() + }); + processor.set_output_will_be_muted(true); + Ok(Self { + processor: Arc::new(processor), + }) + } + + pub(crate) fn capture_stage( + &self, + input_sample_rate: u32, + input_channels: u16, + ) -> RealtimeCaptureAudioProcessor { + RealtimeCaptureAudioProcessor { + processor: self.processor.clone(), + input_sample_rate, + input_channels, + pending_samples: VecDeque::new(), + } + } + + pub(crate) fn render_stage( + &self, + output_sample_rate: u32, + output_channels: u16, + ) -> RealtimeRenderAudioProcessor { + RealtimeRenderAudioProcessor { + processor: self.processor.clone(), + output_sample_rate, + output_channels, + pending_samples: VecDeque::new(), + } + } + + pub(crate) fn set_output_will_be_muted(&self, muted: bool) { + self.processor.set_output_will_be_muted(muted); + } +} + +pub(crate) struct RealtimeCaptureAudioProcessor { + processor: Arc, + input_sample_rate: u32, + input_channels: u16, + pending_samples: VecDeque, +} + +impl RealtimeCaptureAudioProcessor { + pub(crate) fn process_samples(&mut self, samples: &[i16]) -> Vec { + let converted = convert_pcm16( + samples, + self.input_sample_rate, + self.input_channels, + AUDIO_PROCESSING_SAMPLE_RATE, + AUDIO_PROCESSING_CHANNELS, + ); + self.pending_samples.extend(converted); + + let mut processed = Vec::new(); + while self.pending_samples.len() >= self.processor.num_samples_per_frame() { + let mut frame = self.pop_pending_frame(); + if let Err(err) = self.processor.process_capture_frame([frame.as_mut_slice()]) { + warn!("failed to process realtime capture audio: {err}"); + continue; + } + processed.extend(frame.into_iter().map(f32_to_i16)); + } + processed + } + + fn pop_pending_frame(&mut self) -> Vec { + self.pending_samples + .drain(..self.processor.num_samples_per_frame()) + .map(i16_to_f32) + .collect() + } +} + +pub(crate) struct RealtimeRenderAudioProcessor { + processor: Arc, + output_sample_rate: u32, + output_channels: u16, + pending_samples: VecDeque, +} + +impl RealtimeRenderAudioProcessor { + pub(crate) fn process_samples(&mut self, samples: &[i16]) { + self.processor + .set_output_will_be_muted(samples.iter().all(|sample| *sample == 0)); + + let converted = convert_pcm16( + samples, + self.output_sample_rate, + self.output_channels, + AUDIO_PROCESSING_SAMPLE_RATE, + AUDIO_PROCESSING_CHANNELS, + ); + self.pending_samples.extend(converted); + + while self.pending_samples.len() >= self.processor.num_samples_per_frame() { + let mut frame = self.pop_pending_frame(); + if let Err(err) = self.processor.process_render_frame([frame.as_mut_slice()]) { + warn!("failed to process realtime render audio: {err}"); + } + } + } + + fn pop_pending_frame(&mut self) -> Vec { + self.pending_samples + .drain(..self.processor.num_samples_per_frame()) + .map(i16_to_f32) + .collect() + } +} + +pub(crate) fn convert_pcm16( + input: &[i16], + input_sample_rate: u32, + input_channels: u16, + output_sample_rate: u32, + output_channels: u16, +) -> Vec { + if input.is_empty() || input_channels == 0 || output_channels == 0 { + return Vec::new(); + } + + let in_channels = input_channels as usize; + let out_channels = output_channels as usize; + let in_frames = input.len() / in_channels; + if in_frames == 0 { + return Vec::new(); + } + + let out_frames = if input_sample_rate == output_sample_rate { + in_frames + } else { + (((in_frames as u64) * (output_sample_rate as u64)) / (input_sample_rate as u64)).max(1) + as usize + }; + + let mut out = Vec::with_capacity(out_frames.saturating_mul(out_channels)); + for out_frame_idx in 0..out_frames { + let src_frame_idx = if out_frames <= 1 || in_frames <= 1 { + 0 + } else { + ((out_frame_idx as u64) * ((in_frames - 1) as u64) / ((out_frames - 1) as u64)) as usize + }; + let src_start = src_frame_idx.saturating_mul(in_channels); + let src = &input[src_start..src_start + in_channels]; + match (in_channels, out_channels) { + (1, 1) => out.push(src[0]), + (1, n) => { + for _ in 0..n { + out.push(src[0]); + } + } + (n, 1) if n >= 2 => { + let sum: i32 = src.iter().map(|s| *s as i32).sum(); + out.push((sum / (n as i32)) as i16); + } + (n, m) if n == m => out.extend_from_slice(src), + (n, m) if n > m => out.extend_from_slice(&src[..m]), + (n, m) => { + out.extend_from_slice(src); + let last = *src.last().unwrap_or(&0); + for _ in n..m { + out.push(last); + } + } + } + } + out +} + +#[inline] +fn i16_to_f32(sample: i16) -> f32 { + (sample as f32) / (i16::MAX as f32) +} + +#[inline] +fn f32_to_i16(sample: f32) -> i16 { + (sample.clamp(-1.0, 1.0) * i16::MAX as f32) as i16 +} + +#[cfg(test)] +mod tests { + use super::convert_pcm16; + use pretty_assertions::assert_eq; + + #[test] + fn convert_pcm16_downmixes_and_resamples_for_model_input() { + let input = vec![100, 300, 200, 400, 500, 700, 600, 800]; + let converted = convert_pcm16( + &input, /*input_sample_rate*/ 48_000, /*input_channels*/ 2, + /*output_sample_rate*/ 24_000, /*output_channels*/ 1, + ); + assert_eq!(converted, vec![200, 700]); + } +} diff --git a/codex-rs/tui/src/voice.rs b/codex-rs/tui/src/voice.rs index cd135870f8..b1680308ca 100644 --- a/codex-rs/tui/src/voice.rs +++ b/codex-rs/tui/src/voice.rs @@ -1,4 +1,10 @@ use crate::app_event_sender::AppEventSender; +use crate::realtime_audio_processing::AUDIO_PROCESSING_CHANNELS; +use crate::realtime_audio_processing::AUDIO_PROCESSING_SAMPLE_RATE; +use crate::realtime_audio_processing::RealtimeAudioProcessor; +use crate::realtime_audio_processing::RealtimeCaptureAudioProcessor; +use crate::realtime_audio_processing::RealtimeRenderAudioProcessor; +use crate::realtime_audio_processing::convert_pcm16; use base64::Engine; use codex_core::config::Config; use codex_protocol::protocol::ConversationAudioParams; @@ -23,7 +29,11 @@ pub struct VoiceCapture { } impl VoiceCapture { - pub fn start_realtime(config: &Config, tx: AppEventSender) -> Result { + pub fn start_realtime( + config: &Config, + tx: AppEventSender, + audio_processor: RealtimeAudioProcessor, + ) -> Result { let (device, config) = select_realtime_input_device_and_config(config)?; let sample_rate = config.sample_rate().0; @@ -34,9 +44,8 @@ impl VoiceCapture { let stream = build_realtime_input_stream( &device, &config, - sample_rate, - channels, tx, + audio_processor.capture_stage(sample_rate, channels), last_peak.clone(), )?; stream @@ -138,50 +147,76 @@ fn select_realtime_input_device_and_config( fn build_realtime_input_stream( device: &cpal::Device, config: &cpal::SupportedStreamConfig, - sample_rate: u32, - channels: u16, tx: AppEventSender, + capture_processor: RealtimeCaptureAudioProcessor, last_peak: Arc, ) -> Result { match config.sample_format() { - cpal::SampleFormat::F32 => device - .build_input_stream( - &config.clone().into(), - move |input: &[f32], _| { - let peak = peak_f32(input); - last_peak.store(peak, Ordering::Relaxed); - let samples = input.iter().copied().map(f32_to_i16).collect::>(); - send_realtime_audio_chunk(&tx, samples, sample_rate, channels); - }, - move |err| error!("audio input error: {err}"), - None, - ) - .map_err(|e| format!("failed to build input stream: {e}")), - cpal::SampleFormat::I16 => device - .build_input_stream( - &config.clone().into(), - move |input: &[i16], _| { - let peak = peak_i16(input); - last_peak.store(peak, Ordering::Relaxed); - send_realtime_audio_chunk(&tx, input.to_vec(), sample_rate, channels); - }, - move |err| error!("audio input error: {err}"), - None, - ) - .map_err(|e| format!("failed to build input stream: {e}")), - cpal::SampleFormat::U16 => device - .build_input_stream( - &config.clone().into(), - move |input: &[u16], _| { - let mut samples = Vec::with_capacity(input.len()); - let peak = convert_u16_to_i16_and_peak(input, &mut samples); - last_peak.store(peak, Ordering::Relaxed); - send_realtime_audio_chunk(&tx, samples, sample_rate, channels); - }, - move |err| error!("audio input error: {err}"), - None, - ) - .map_err(|e| format!("failed to build input stream: {e}")), + cpal::SampleFormat::F32 => { + let mut capture_processor = capture_processor; + device + .build_input_stream( + &config.clone().into(), + move |input: &[f32], _| { + let peak = peak_f32(input); + last_peak.store(peak, Ordering::Relaxed); + let samples = input.iter().copied().map(f32_to_i16).collect::>(); + let samples = capture_processor.process_samples(&samples); + send_realtime_audio_chunk( + &tx, + samples, + AUDIO_PROCESSING_SAMPLE_RATE, + AUDIO_PROCESSING_CHANNELS, + ); + }, + move |err| error!("audio input error: {err}"), + None, + ) + .map_err(|e| format!("failed to build input stream: {e}")) + } + cpal::SampleFormat::I16 => { + let mut capture_processor = capture_processor; + device + .build_input_stream( + &config.clone().into(), + move |input: &[i16], _| { + let peak = peak_i16(input); + last_peak.store(peak, Ordering::Relaxed); + let samples = capture_processor.process_samples(input); + send_realtime_audio_chunk( + &tx, + samples, + AUDIO_PROCESSING_SAMPLE_RATE, + AUDIO_PROCESSING_CHANNELS, + ); + }, + move |err| error!("audio input error: {err}"), + None, + ) + .map_err(|e| format!("failed to build input stream: {e}")) + } + cpal::SampleFormat::U16 => { + let mut capture_processor = capture_processor; + device + .build_input_stream( + &config.clone().into(), + move |input: &[u16], _| { + let mut samples = Vec::with_capacity(input.len()); + let peak = convert_u16_to_i16_and_peak(input, &mut samples); + last_peak.store(peak, Ordering::Relaxed); + let samples = capture_processor.process_samples(&samples); + send_realtime_audio_chunk( + &tx, + samples, + AUDIO_PROCESSING_SAMPLE_RATE, + AUDIO_PROCESSING_CHANNELS, + ); + }, + move |err| error!("audio input error: {err}"), + None, + ) + .map_err(|e| format!("failed to build input stream: {e}")) + } _ => Err("unsupported input sample format".to_string()), } } @@ -283,24 +318,34 @@ fn convert_u16_to_i16_and_peak(input: &[u16], out: &mut Vec) -> u16 { pub(crate) struct RealtimeAudioPlayer { _stream: cpal::Stream, queue: Arc>>, + audio_processor: RealtimeAudioProcessor, output_sample_rate: u32, output_channels: u16, } impl RealtimeAudioPlayer { - pub(crate) fn start(config: &Config) -> Result { + pub(crate) fn start( + config: &Config, + audio_processor: RealtimeAudioProcessor, + ) -> Result { let (device, config) = crate::audio_device::select_configured_output_device_and_config(config)?; let output_sample_rate = config.sample_rate().0; let output_channels = config.channels(); let queue = Arc::new(Mutex::new(VecDeque::new())); - let stream = build_output_stream(&device, &config, Arc::clone(&queue))?; + let stream = build_output_stream( + &device, + &config, + Arc::clone(&queue), + audio_processor.render_stage(output_sample_rate, output_channels), + )?; stream .play() .map_err(|e| format!("failed to start output stream: {e}"))?; Ok(Self { _stream: stream, queue, + audio_processor, output_sample_rate, output_channels, }) @@ -336,6 +381,8 @@ impl RealtimeAudioPlayer { .map_err(|_| "failed to lock output audio queue".to_string())?; // TODO(aibrahim): Cap or trim this queue if we observe producer bursts outrunning playback. guard.extend(converted); + drop(guard); + self.audio_processor.set_output_will_be_muted(false); Ok(()) } @@ -343,6 +390,7 @@ impl RealtimeAudioPlayer { if let Ok(mut guard) = self.queue.lock() { guard.clear(); } + self.audio_processor.set_output_will_be_muted(true); } } @@ -350,140 +398,94 @@ fn build_output_stream( device: &cpal::Device, config: &cpal::SupportedStreamConfig, queue: Arc>>, + render_processor: RealtimeRenderAudioProcessor, ) -> Result { let config_any: cpal::StreamConfig = config.clone().into(); match config.sample_format() { - cpal::SampleFormat::F32 => device - .build_output_stream( - &config_any, - move |output: &mut [f32], _| fill_output_f32(output, &queue), - move |err| error!("audio output error: {err}"), - None, - ) - .map_err(|e| format!("failed to build f32 output stream: {e}")), - cpal::SampleFormat::I16 => device - .build_output_stream( - &config_any, - move |output: &mut [i16], _| fill_output_i16(output, &queue), - move |err| error!("audio output error: {err}"), - None, - ) - .map_err(|e| format!("failed to build i16 output stream: {e}")), - cpal::SampleFormat::U16 => device - .build_output_stream( - &config_any, - move |output: &mut [u16], _| fill_output_u16(output, &queue), - move |err| error!("audio output error: {err}"), - None, - ) - .map_err(|e| format!("failed to build u16 output stream: {e}")), + cpal::SampleFormat::F32 => { + let mut render_processor = render_processor; + device + .build_output_stream( + &config_any, + move |output: &mut [f32], _| { + fill_output_f32(output, &queue, &mut render_processor) + }, + move |err| error!("audio output error: {err}"), + None, + ) + .map_err(|e| format!("failed to build f32 output stream: {e}")) + } + cpal::SampleFormat::I16 => { + let mut render_processor = render_processor; + device + .build_output_stream( + &config_any, + move |output: &mut [i16], _| { + fill_output_i16(output, &queue, &mut render_processor) + }, + move |err| error!("audio output error: {err}"), + None, + ) + .map_err(|e| format!("failed to build i16 output stream: {e}")) + } + cpal::SampleFormat::U16 => { + let mut render_processor = render_processor; + device + .build_output_stream( + &config_any, + move |output: &mut [u16], _| { + fill_output_u16(output, &queue, &mut render_processor) + }, + move |err| error!("audio output error: {err}"), + None, + ) + .map_err(|e| format!("failed to build u16 output stream: {e}")) + } other => Err(format!("unsupported output sample format: {other:?}")), } } -fn fill_output_i16(output: &mut [i16], queue: &Arc>>) { - if let Ok(mut guard) = queue.lock() { - for sample in output { - *sample = guard.pop_front().unwrap_or(0); - } - return; - } - output.fill(0); +fn fill_output_i16( + output: &mut [i16], + queue: &Arc>>, + render_processor: &mut RealtimeRenderAudioProcessor, +) { + let samples = drain_output_samples(output.len(), queue); + output.copy_from_slice(&samples); + render_processor.process_samples(output); } -fn fill_output_f32(output: &mut [f32], queue: &Arc>>) { - if let Ok(mut guard) = queue.lock() { - for sample in output { - let v = guard.pop_front().unwrap_or(0); - *sample = (v as f32) / (i16::MAX as f32); - } - return; +fn fill_output_f32( + output: &mut [f32], + queue: &Arc>>, + render_processor: &mut RealtimeRenderAudioProcessor, +) { + let samples = drain_output_samples(output.len(), queue); + for (output_sample, sample) in output.iter_mut().zip(samples.iter()) { + *output_sample = (*sample as f32) / (i16::MAX as f32); } - output.fill(0.0); + render_processor.process_samples(&samples); } -fn fill_output_u16(output: &mut [u16], queue: &Arc>>) { - if let Ok(mut guard) = queue.lock() { - for sample in output { - let v = guard.pop_front().unwrap_or(0); - *sample = (v as i32 + 32768).clamp(0, u16::MAX as i32) as u16; - } - return; +fn fill_output_u16( + output: &mut [u16], + queue: &Arc>>, + render_processor: &mut RealtimeRenderAudioProcessor, +) { + let samples = drain_output_samples(output.len(), queue); + for (output_sample, sample) in output.iter_mut().zip(samples.iter()) { + *output_sample = (*sample as i32 + 32768).clamp(0, u16::MAX as i32) as u16; } - output.fill(32768); + render_processor.process_samples(&samples); } -fn convert_pcm16( - input: &[i16], - input_sample_rate: u32, - input_channels: u16, - output_sample_rate: u32, - output_channels: u16, -) -> Vec { - if input.is_empty() || input_channels == 0 || output_channels == 0 { - return Vec::new(); - } - - let in_channels = input_channels as usize; - let out_channels = output_channels as usize; - let in_frames = input.len() / in_channels; - if in_frames == 0 { - return Vec::new(); - } - - let out_frames = if input_sample_rate == output_sample_rate { - in_frames - } else { - (((in_frames as u64) * (output_sample_rate as u64)) / (input_sample_rate as u64)).max(1) - as usize +fn drain_output_samples(output_len: usize, queue: &Arc>>) -> Vec { + let mut samples = vec![0; output_len]; + let Ok(mut guard) = queue.lock() else { + return samples; }; - - let mut out = Vec::with_capacity(out_frames.saturating_mul(out_channels)); - for out_frame_idx in 0..out_frames { - let src_frame_idx = if out_frames <= 1 || in_frames <= 1 { - 0 - } else { - ((out_frame_idx as u64) * ((in_frames - 1) as u64) / ((out_frames - 1) as u64)) as usize - }; - let src_start = src_frame_idx.saturating_mul(in_channels); - let src = &input[src_start..src_start + in_channels]; - match (in_channels, out_channels) { - (1, 1) => out.push(src[0]), - (1, n) => { - for _ in 0..n { - out.push(src[0]); - } - } - (n, 1) if n >= 2 => { - let sum: i32 = src.iter().map(|s| *s as i32).sum(); - out.push((sum / (n as i32)) as i16); - } - (n, m) if n == m => out.extend_from_slice(src), - (n, m) if n > m => out.extend_from_slice(&src[..m]), - (n, m) => { - out.extend_from_slice(src); - let last = *src.last().unwrap_or(&0); - for _ in n..m { - out.push(last); - } - } - } - } - out -} - -#[cfg(test)] -mod tests { - use super::convert_pcm16; - use pretty_assertions::assert_eq; - - #[test] - fn convert_pcm16_downmixes_and_resamples_for_model_input() { - let input = vec![100, 300, 200, 400, 500, 700, 600, 800]; - let converted = convert_pcm16( - &input, /*input_sample_rate*/ 48_000, /*input_channels*/ 2, - /*output_sample_rate*/ 24_000, /*output_channels*/ 1, - ); - assert_eq!(converted, vec![200, 700]); + for sample in &mut samples { + *sample = guard.pop_front().unwrap_or(0); } + samples } diff --git a/patches/BUILD.bazel b/patches/BUILD.bazel index 75d6be358e..1af443f7e0 100644 --- a/patches/BUILD.bazel +++ b/patches/BUILD.bazel @@ -19,6 +19,8 @@ exports_files([ "v8_bazel_rules.patch", "v8_module_deps.patch", "v8_source_portability.patch", + "webrtc-audio-processing-sys_logging_macos_sdk.patch", + "webrtc-audio-processing-sys_meson_env.patch", "windows-link.patch", "zstd-sys_windows_msvc_include_dirs.patch", ]) diff --git a/patches/webrtc-audio-processing-sys_logging_macos_sdk.patch b/patches/webrtc-audio-processing-sys_logging_macos_sdk.patch new file mode 100644 index 0000000000..9468eca1cb --- /dev/null +++ b/patches/webrtc-audio-processing-sys_logging_macos_sdk.patch @@ -0,0 +1,12 @@ +diff --git a/webrtc-audio-processing/webrtc/rtc_base/logging.cc b/webrtc-audio-processing/webrtc/rtc_base/logging.cc +--- a/webrtc-audio-processing/webrtc/rtc_base/logging.cc ++++ b/webrtc-audio-processing/webrtc/rtc_base/logging.cc +@@ -24,6 +24,6 @@ + #undef ERROR // wingdi.h + #endif + + #if defined(WEBRTC_MAC) && !defined(WEBRTC_IOS) +-#include ++#include + #elif defined(WEBRTC_ANDROID) + #include diff --git a/patches/webrtc-audio-processing-sys_meson_env.patch b/patches/webrtc-audio-processing-sys_meson_env.patch new file mode 100644 index 0000000000..79f1b81076 --- /dev/null +++ b/patches/webrtc-audio-processing-sys_meson_env.patch @@ -0,0 +1,401 @@ +diff --git a/build.rs b/build.rs +index 8578e9220a..bd5c9ba382 100644 +--- a/build.rs ++++ b/build.rs +@@ -1,10 +1,14 @@ + use anyhow::{bail, Context, Result}; + use bindgen::callbacks::{AttributeInfo, DeriveInfo, ParseCallbacks}; ++#[cfg(not(windows))] ++use std::os::unix::fs::symlink; ++#[cfg(windows)] ++use std::os::windows::fs::symlink_dir as symlink; + use std::{ + env, +- fs::File, ++ fs::{self, File}, + io::{BufWriter, Write}, +- path::PathBuf, ++ path::{Path, PathBuf}, + process::Command, + }; + +@@ -203,8 +207,136 @@ mod webrtc { + let webrtc_build_dir = build_dir.join(BUNDLED_SOURCE_PATH); + eprintln!("Building webrtc-audio-processing in {}", webrtc_build_dir.display()); + +- let mut meson = Command::new("meson"); ++ let meson_launcher = env::var_os("MESON").expect("MESON is set by Bazel"); ++ let cargo_manifest_dir = ++ env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Bazel"); ++ let runfiles_dir = Path::new(&cargo_manifest_dir) ++ .parent() ++ .expect("CARGO_MANIFEST_DIR points inside a Bazel runfiles tree"); ++ let staged_meson = build_dir.join("meson"); ++ fs::copy(&meson_launcher, &staged_meson).context("Failed to stage Meson launcher")?; ++ let mut meson_runfiles = staged_meson.as_os_str().to_owned(); ++ meson_runfiles.push(".runfiles"); ++ let _ = fs::remove_file(&meson_runfiles); ++ symlink(runfiles_dir, &meson_runfiles).context("Failed to link Meson runfiles")?; ++ ++ let mut meson = Command::new(staged_meson); ++ let mut meson_cross_file = None; ++ if let (Ok(host), Ok(target)) = (env::var("HOST"), env::var("TARGET")) { ++ if host != target { ++ let mut target_parts = target.split('-'); ++ let cpu_family = target_parts ++ .next() ++ .expect("TARGET triple has an architecture"); ++ let system = target_parts ++ .next_back() ++ .expect("TARGET triple has an operating system"); ++ let endian = match cpu_family { ++ "aarch64" | "x86_64" => "little", ++ _ => bail!("Unsupported TARGET architecture for Meson cross file: {cpu_family}"), ++ }; ++ let cross_file = build_dir.join("meson-cross-file.ini"); ++ fs::write( ++ &cross_file, ++ format!( ++ concat!( ++ "[host_machine]\n", ++ "system = '{system}'\n", ++ "cpu_family = '{cpu_family}'\n", ++ "cpu = '{cpu_family}'\n", ++ "endian = '{endian}'\n", ++ "\n", ++ "[properties]\n", ++ "needs_exe_wrapper = true\n", ++ ), ++ system = system, ++ cpu_family = cpu_family, ++ endian = endian, ++ ), ++ ) ++ .context("Failed to write Meson cross file")?; ++ meson_cross_file = Some(cross_file); ++ } ++ } ++ ++ let mut ninja_launcher = env::var_os("NINJA"); ++ if ninja_launcher.is_none() { ++ for candidate in ["ninja", "ninja-build"] { ++ let can_execute = Command::new(candidate) ++ .arg("--version") ++ .status() ++ .map(|status| status.success()) ++ .unwrap_or(false); ++ if can_execute { ++ ninja_launcher = Some(candidate.into()); ++ break; ++ } ++ } ++ } ++ if let Some(ninja_real) = env::var_os("NINJA_REAL") { ++ let mut temp_dirs = Vec::new(); ++ if let Some(dir) = env::var_os("CODEX_NINJA_TMPDIR") { ++ temp_dirs.push(PathBuf::from(dir)); ++ } ++ temp_dirs.extend([ ++ PathBuf::from("/private/var/tmp"), ++ PathBuf::from("/var/tmp"), ++ env::temp_dir(), ++ PathBuf::from("/tmp"), ++ ]); ++ if let Some(home) = env::var_os("HOME") { ++ temp_dirs.push(PathBuf::from(home)); ++ } ++ ++ let mut staged_ninja = None; ++ for (attempt, temp_dir) in temp_dirs.into_iter().enumerate() { ++ if !temp_dir.is_dir() { ++ continue; ++ } ++ ++ let stage_dir = ++ temp_dir.join(format!("codex-ninja-{}-{attempt}", std::process::id())); ++ let _ = fs::remove_dir_all(&stage_dir); ++ if fs::create_dir_all(&stage_dir).is_err() { ++ continue; ++ } ++ ++ let candidate_ninja = stage_dir.join("ninja"); ++ let stage_result = ++ fs::copy(&ninja_real, &candidate_ninja).and_then(|_| ensure_executable(&candidate_ninja)); ++ if stage_result.is_ok() { ++ staged_ninja = Some(candidate_ninja.into_os_string()); ++ break; ++ } ++ } ++ ninja_launcher = staged_ninja.or_else(|| Some(ninja_real)); ++ } ++ let ninja_launcher = ++ ninja_launcher.ok_or_else(|| anyhow::anyhow!("Failed to locate ninja launcher"))?; ++ ++ if let Some(ldflags) = env::var("LDFLAGS").ok() { ++ let execroot = determine_execroot(runfiles_dir)?; ++ let ldflags = ldflags ++ .split_whitespace() ++ .map(|token| { ++ if let Some(path) = token.strip_prefix("-Lbazel-out") { ++ format!("-L{}", execroot.join(format!("bazel-out{path}")).display()) ++ } else if token.starts_with("bazel-out") { ++ execroot.join(token).display().to_string() ++ } else { ++ token.to_owned() ++ } ++ }) ++ .collect::>() ++ .join(" "); ++ meson.env("LDFLAGS", ldflags); ++ } + meson.args(["setup", "--prefix", install_dir.to_str().unwrap()]); ++ meson.env("NINJA", &ninja_launcher); ++ if let Some(cross_file) = meson_cross_file { ++ meson.arg("--cross-file"); ++ meson.arg(cross_file); ++ } + meson.arg("--reconfigure"); + + if cfg!(target_os = "macos") { +@@ -221,14 +353,14 @@ mod webrtc { + .context("Failed to execute meson. Do you have it installed?")?; + assert!(status.success(), "Command failed: {:?}", &meson); + +- let mut ninja = Command::new("ninja"); ++ let mut ninja = Command::new(ninja_launcher.clone()); + let status = ninja + .current_dir(&webrtc_build_dir) + .status() + .context("Failed to execute ninja. Do you have it installed?")?; + assert!(status.success(), "Command failed: {:?}", &ninja); + +- let mut install = Command::new("ninja"); ++ let mut install = Command::new(ninja_launcher); + let status = install + .current_dir(&webrtc_build_dir) + .arg("install") +@@ -267,15 +399,32 @@ mod webrtc { + + /// Extract defined (non-external) symbols from a static library using nm. + fn get_defined_symbols(archive_path: &std::path::Path) -> Result> { +- let output = Command::new("nm") ++ let nm = determine_nm_path(); ++ let output = match Command::new(&nm) + .arg("--defined-only") + .arg("--format=posix") + .arg(archive_path) + .output() +- .context("Failed to execute nm")?; ++ { ++ Ok(output) => output, ++ Err(error) ++ if env::var("CARGO_CFG_TARGET_OS") ++ .map(|target_os| target_os == "macos") ++ .or_else(|_| env::var("TARGET").map(|target| target.contains("apple-darwin"))) ++ .unwrap_or(false) ++ && error.kind() == std::io::ErrorKind::NotFound => ++ { ++ println!( ++ "cargo:warning=Skipping symbol prefixing because {:?} is unavailable", ++ nm ++ ); ++ return Ok(Vec::new()); ++ } ++ Err(error) => return Err(error).context(format!("Failed to execute {:?}", nm)), ++ }; + + if !output.status.success() { +- anyhow::bail!("nm failed: {}", String::from_utf8_lossy(&output.stderr)); ++ anyhow::bail!("{:?} failed: {}", nm, String::from_utf8_lossy(&output.stderr)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); +@@ -400,6 +549,7 @@ fn main() -> Result<()> { + } + + let binding_file = out_dir().join("bindings.rs"); ++ maybe_set_libclang_path(); + let mut builder = bindgen::Builder::default() + .header("src/wrapper.hpp") + .clang_args(&["-x", "c++", "-std=c++17", "-fparse-all-comments"]) +@@ -461,3 +611,180 @@ fn determine_objcopy_path() -> Result { + + Ok(objcopy) + } ++ ++fn determine_execroot(path: &Path) -> Result { ++ for ancestor in path.ancestors() { ++ if ancestor.file_name().and_then(|name| name.to_str()) == Some("bazel-out") { ++ return ancestor ++ .parent() ++ .map(Path::to_path_buf) ++ .context("bazel-out directory is missing its execroot parent"); ++ } ++ } ++ ++ env::current_dir().context("Failed to determine execroot") ++} ++ ++#[cfg(not(windows))] ++fn ensure_executable(path: &Path) -> std::io::Result<()> { ++ use std::os::unix::fs::PermissionsExt; ++ ++ let mut permissions = fs::metadata(path)?.permissions(); ++ permissions.set_mode(0o755); ++ fs::set_permissions(path, permissions) ++} ++ ++#[cfg(windows)] ++fn ensure_executable(_path: &Path) -> std::io::Result<()> { ++ Ok(()) ++} ++ ++fn determine_nm_path() -> PathBuf { ++ if let Some(nm) = env::var_os("NM") { ++ return nm.into(); ++ } ++ ++ if env::var("CARGO_CFG_TARGET_OS") ++ .map(|target_os| target_os == "macos") ++ .or_else(|_| env::var("TARGET").map(|target| target.contains("apple-darwin"))) ++ .unwrap_or(false) ++ { ++ let xcrun_output = Command::new("/usr/bin/xcrun") ++ .args(["--find", "llvm-nm"]) ++ .output(); ++ if let Ok(output) = xcrun_output { ++ if output.status.success() { ++ let path = String::from_utf8_lossy(&output.stdout).trim().to_owned(); ++ if !path.is_empty() { ++ return PathBuf::from(path); ++ } ++ } ++ } ++ } ++ ++ for tool_env in ["AR", "CC", "CXX"] { ++ if let Some(tool_path) = env::var_os(tool_env) { ++ let tool_path = PathBuf::from(tool_path); ++ if let Some(parent) = tool_path.parent() { ++ for candidate_name in ["llvm-nm", "nm"] { ++ let candidate = parent.join(candidate_name); ++ if candidate.exists() { ++ return candidate; ++ } ++ } ++ } ++ ++ if let Some(file_name) = tool_path.file_name().and_then(|name| name.to_str()) { ++ let candidate_name = file_name.replace("llvm-ar", "llvm-nm"); ++ if candidate_name != file_name { ++ let candidate = tool_path.with_file_name(candidate_name); ++ if candidate.exists() { ++ return candidate; ++ } ++ } ++ ++ if file_name != "llvm-nm" { ++ let candidate = tool_path.with_file_name("llvm-nm"); ++ if candidate.exists() { ++ return candidate; ++ } ++ } ++ } ++ } ++ } ++ ++ for candidate in ["llvm-nm", "nm"] { ++ if Command::new(candidate) ++ .arg("--version") ++ .status() ++ .map(|status| status.success()) ++ .unwrap_or(false) ++ { ++ return PathBuf::from(candidate); ++ } ++ } ++ ++ PathBuf::from("nm") ++} ++ ++fn maybe_set_libclang_path() { ++ if env::var_os("LIBCLANG_PATH").is_some() { ++ return; ++ } ++ ++ for tool_env in ["CC", "CXX"] { ++ let Some(tool_value) = env::var_os(tool_env) else { ++ continue; ++ }; ++ let tool_path = PathBuf::from(tool_value); ++ let tool_path = if tool_path.is_absolute() { ++ tool_path ++ } else if let Ok(current_dir) = env::current_dir() { ++ let current_dir_candidate = current_dir.join(&tool_path); ++ if current_dir_candidate.exists() { ++ current_dir_candidate ++ } else if let Some(manifest_dir) = env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from) { ++ let mut resolved = tool_path.clone(); ++ for ancestor in manifest_dir.ancestors() { ++ let ancestor_candidate = ancestor.join(&tool_path); ++ if ancestor_candidate.exists() { ++ resolved = ancestor_candidate; ++ break; ++ } ++ } ++ resolved ++ } else { ++ tool_path ++ } ++ } else { ++ tool_path ++ }; ++ ++ let Some(tool_dir) = tool_path.parent() else { ++ continue; ++ }; ++ let Some(tool_root) = tool_dir.parent() else { ++ continue; ++ }; ++ ++ let mut directories = vec![tool_root.join("lib"), tool_root.join("lib64")]; ++ if let Ok(read_dir) = fs::read_dir(tool_root) { ++ directories.extend( ++ read_dir ++ .filter_map(std::result::Result::ok) ++ .map(|entry| entry.path()) ++ .filter(|path| path.is_dir()), ++ ); ++ } ++ ++ while let Some(directory) = directories.pop() { ++ let read_dir = match fs::read_dir(&directory) { ++ Ok(read_dir) => read_dir, ++ Err(_) => continue, ++ }; ++ ++ for entry in read_dir.filter_map(std::result::Result::ok) { ++ let path = entry.path(); ++ if path.is_dir() { ++ directories.push(path); ++ continue; ++ } ++ ++ let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { ++ continue; ++ }; ++ let is_libclang = matches!(file_name, "libclang.so" | "libclang.dll" | "libclang.dylib") ++ || file_name.starts_with("libclang.so.") ++ || file_name.starts_with("libclang-"); ++ if !is_libclang { ++ continue; ++ } ++ ++ if let Some(parent) = path.parent() { ++ env::set_var("LIBCLANG_PATH", parent); ++ return; ++ } ++ } ++ } ++ } ++} diff --git a/tools/ninja_wrapper/BUILD.bazel b/tools/ninja_wrapper/BUILD.bazel new file mode 100644 index 0000000000..c53df6ef70 --- /dev/null +++ b/tools/ninja_wrapper/BUILD.bazel @@ -0,0 +1,4 @@ +exports_files( + ["ninja_wrapper.sh"], + visibility = ["//visibility:public"], +) diff --git a/tools/ninja_wrapper/ninja_wrapper.sh b/tools/ninja_wrapper/ninja_wrapper.sh new file mode 100755 index 0000000000..7ea424d113 --- /dev/null +++ b/tools/ninja_wrapper/ninja_wrapper.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +set -eu + +real_ninja="${NINJA_REAL:?NINJA_REAL must point to the downloaded ninja binary}" + +try_exec() { + candidate_dir="$1" + [ -n "${candidate_dir}" ] || return 1 + [ -d "${candidate_dir}" ] || return 1 + + staged_dir="$(mktemp -d "${candidate_dir%/}/codex-ninja.XXXXXX" 2>/dev/null)" || return 1 + staged_ninja="${staged_dir}/ninja" + + if cp "${real_ninja}" "${staged_ninja}" 2>/dev/null \ + && chmod +x "${staged_ninja}" 2>/dev/null \ + && "${staged_ninja}" --version >/dev/null 2>&1; then + exec "${staged_ninja}" "$@" + fi + + rm -rf "${staged_dir}" + return 1 +} + +for candidate_dir in "${CODEX_NINJA_TMPDIR:-}" /private/var/tmp /var/tmp "${TMPDIR:-}" /tmp "${HOME:-}"; do + try_exec "${candidate_dir}" +done + +echo "codex ninja wrapper: unable to stage an executable Ninja binary" >&2 +exit 1