diff --git a/.bazelrc b/.bazelrc index ce7c1e1d43..331f8634d2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -75,6 +75,11 @@ common:ci --disk_cache= common:ci-bazel --config=ci common:ci-bazel --build_metadata=TAG_workflow=bazel +# Shared config for Bazel-backed Rust linting. +build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect +build:clippy --output_groups=+clippy_checks +build:clippy --@rules_rust//rust/settings:clippy.toml=//codex-rs:clippy.toml + # Rearrange caches on Windows so they're on the same volume as the checkout. common:ci-windows --config=ci-bazel common:ci-windows --build_metadata=TAG_os=windows diff --git a/.github/actions/setup-bazel-ci/action.yml b/.github/actions/setup-bazel-ci/action.yml new file mode 100644 index 0000000000..a737ec8244 --- /dev/null +++ b/.github/actions/setup-bazel-ci/action.yml @@ -0,0 +1,61 @@ +name: setup-bazel-ci +description: Prepare a Bazel CI runner with shared caches and optional test prerequisites. +inputs: + target: + description: Target triple used for cache namespacing. + required: true + install-test-prereqs: + description: Install Node.js and DotSlash for Bazel-backed test jobs. + required: false + default: "false" +outputs: + cache-hit: + description: Whether the Bazel repository cache key was restored exactly. + value: ${{ steps.cache_bazel_repository_restore.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Set up Node.js for js_repl tests + if: inputs.install-test-prereqs == 'true' + uses: actions/setup-node@v6 + with: + node-version-file: codex-rs/node-version.txt + + # Some integration tests rely on DotSlash being installed. + # See https://github.com/openai/codex/pull/7617. + - name: Install DotSlash + if: inputs.install-test-prereqs == 'true' + uses: facebook/install-dotslash@v2 + + - name: Make DotSlash available in PATH (Unix) + if: inputs.install-test-prereqs == 'true' && runner.os != 'Windows' + shell: bash + run: cp "$(which dotslash)" /usr/local/bin + + - name: Make DotSlash available in PATH (Windows) + if: inputs.install-test-prereqs == 'true' && runner.os == 'Windows' + shell: pwsh + run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe" + + - name: Set up Bazel + uses: bazelbuild/setup-bazelisk@v3 + + # Restore bazel repository cache so we don't have to redownload all the external dependencies + # on every CI run. + - name: Restore bazel repository cache + id: cache_bazel_repository_restore + uses: actions/cache/restore@v5 + with: + path: | + ~/.cache/bazel-repo-cache + key: bazel-cache-${{ inputs.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} + restore-keys: | + bazel-cache-${{ inputs.target }} + + - name: Configure Bazel startup args (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + # Use a very short path to reduce argv/path length issues. + "BAZEL_STARTUP_ARGS=--output_user_root=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/.github/scripts/run-bazel-ci.sh b/.github/scripts/run-bazel-ci.sh new file mode 100755 index 0000000000..06bc97cded --- /dev/null +++ b/.github/scripts/run-bazel-ci.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +set -euo pipefail +set -o pipefail + +print_failed_bazel_test_logs=0 +use_node_test_env=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --print-failed-test-logs) + print_failed_bazel_test_logs=1 + shift + ;; + --use-node-test-env) + use_node_test_env=1 + shift + ;; + --) + shift + break + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [[ $# -eq 0 ]]; then + echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] -- -- " >&2 + exit 1 +fi + +bazel_startup_args=() +if [[ -n "${BAZEL_STARTUP_ARGS:-}" ]]; then + read -r -a bazel_startup_args <<< "${BAZEL_STARTUP_ARGS}" +fi + +ci_config=ci-linux +case "${RUNNER_OS:-}" in + macOS) + ci_config=ci-macos + ;; + Windows) + ci_config=ci-windows + ;; +esac + +print_bazel_test_log_tails() { + local console_log="$1" + local testlogs_dir + local -a bazel_info_cmd=(bazel) + + if (( ${#bazel_startup_args[@]} > 0 )); then + bazel_info_cmd+=("${bazel_startup_args[@]}") + fi + + testlogs_dir="$("${bazel_info_cmd[@]}" info bazel-testlogs 2>/dev/null || echo bazel-testlogs)" + + local failed_targets=() + while IFS= read -r target; do + failed_targets+=("$target") + done < <( + grep -E '^FAIL: //' "$console_log" \ + | sed -E 's#^FAIL: (//[^ ]+).*#\1#' \ + | sort -u + ) + + if [[ ${#failed_targets[@]} -eq 0 ]]; then + echo "No failed Bazel test targets were found in console output." + return + fi + + for target in "${failed_targets[@]}"; do + local rel_path="${target#//}" + rel_path="${rel_path/:/\/}" + local test_log="${testlogs_dir}/${rel_path}/test.log" + + echo "::group::Bazel test log tail for ${target}" + if [[ -f "$test_log" ]]; then + tail -n 200 "$test_log" + else + echo "Missing test log: $test_log" + fi + echo "::endgroup::" + done +} + +bazel_args=() +bazel_targets=() +found_target_separator=0 +for arg in "$@"; do + if [[ "$arg" == "--" && $found_target_separator -eq 0 ]]; then + found_target_separator=1 + continue + fi + + if [[ $found_target_separator -eq 0 ]]; then + bazel_args+=("$arg") + else + bazel_targets+=("$arg") + fi +done + +if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then + echo "Expected Bazel args and targets separated by --" >&2 + exit 1 +fi + +if [[ $use_node_test_env -eq 1 && "${RUNNER_OS:-}" != "Windows" ]]; then + # Bazel test sandboxes on macOS may resolve an older Homebrew `node` + # before the `actions/setup-node` runtime on PATH. + node_bin="$(which node)" + bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}") +fi + +bazel_console_log="$(mktemp)" +trap 'rm -f "$bazel_console_log"' EXIT + +bazel_cmd=(bazel) +if (( ${#bazel_startup_args[@]} > 0 )); then + bazel_cmd+=("${bazel_startup_args[@]}") +fi + +if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then + echo "BuildBuddy API key is available; using remote Bazel configuration." + # Work around Bazel 9 remote repo contents cache / overlay materialization failures + # seen in CI (for example "is not a symlink" or permission errors while + # materializing external repos such as rules_perl). We still use BuildBuddy for + # remote execution/cache; this only disables the startup-level repo contents cache. + set +e + "${bazel_cmd[@]}" \ + --noexperimental_remote_repo_contents_cache \ + "${bazel_args[@]}" \ + "--config=${ci_config}" \ + "--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" \ + -- \ + "${bazel_targets[@]}" \ + 2>&1 | tee "$bazel_console_log" + bazel_status=${PIPESTATUS[0]} + set -e +else + echo "BuildBuddy API key is not available; using local Bazel configuration." + # Keep fork/community PRs on Bazel but disable remote services that are + # configured in .bazelrc and require auth. + # + # Flag docs: + # - Command-line reference: https://bazel.build/reference/command-line-reference + # - Remote caching overview: https://bazel.build/remote/caching + # - Remote execution overview: https://bazel.build/remote/rbe + # - Build Event Protocol overview: https://bazel.build/remote/bep + # + # --noexperimental_remote_repo_contents_cache: + # disable remote repo contents cache enabled in .bazelrc startup options. + # https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache + # --remote_cache= and --remote_executor=: + # clear remote cache/execution endpoints configured in .bazelrc. + # https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache + # https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor + set +e + "${bazel_cmd[@]}" \ + --noexperimental_remote_repo_contents_cache \ + "${bazel_args[@]}" \ + --remote_cache= \ + --remote_executor= \ + -- \ + "${bazel_targets[@]}" \ + 2>&1 | tee "$bazel_console_log" + bazel_status=${PIPESTATUS[0]} + set -e +fi + +if [[ ${bazel_status:-0} -ne 0 ]]; then + if [[ $print_failed_bazel_test_logs -eq 1 ]]; then + print_bazel_test_log_tails "$bazel_console_log" + fi + exit "$bazel_status" +fi diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 79d963a537..7d12511e90 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -1,4 +1,4 @@ -name: Bazel (experimental) +name: Bazel # Note this workflow was originally derived from: # https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml @@ -50,181 +50,91 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Set up Node.js for js_repl tests - uses: actions/setup-node@v6 + - name: Set up Bazel CI + id: setup_bazel + uses: ./.github/actions/setup-bazel-ci with: - node-version-file: codex-rs/node-version.txt - - # Some integration tests rely on DotSlash being installed. - # See https://github.com/openai/codex/pull/7617. - - name: Install DotSlash - uses: facebook/install-dotslash@v2 - - - name: Make DotSlash available in PATH (Unix) - if: runner.os != 'Windows' - run: cp "$(which dotslash)" /usr/local/bin - - - name: Make DotSlash available in PATH (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe" - - # Install Bazel via Bazelisk - - name: Set up Bazel - uses: bazelbuild/setup-bazelisk@v3 + target: ${{ matrix.target }} + install-test-prereqs: "true" - name: Check MODULE.bazel.lock is up to date if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu' shell: bash run: ./scripts/check-module-bazel-lock.sh - # Restore bazel repository cache so we don't have to redownload all the external dependencies - # on every CI run. - - name: Restore bazel repository cache - id: cache_bazel_repository_restore - uses: actions/cache/restore@v5 - with: - path: | - ~/.cache/bazel-repo-cache - key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} - restore-keys: | - bazel-cache-${{ matrix.target }} - - - name: Configure Bazel startup args (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - # Use a very short path to reduce argv/path length issues. - "BAZEL_STARTUP_ARGS=--output_user_root=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: bazel test //... env: BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} shell: bash run: | - set -o pipefail - - bazel_console_log="$(mktemp)" - - print_failed_bazel_test_logs() { - local console_log="$1" - local testlogs_dir - - testlogs_dir="$(bazel $BAZEL_STARTUP_ARGS info bazel-testlogs 2>/dev/null || echo bazel-testlogs)" - - local failed_targets=() - while IFS= read -r target; do - failed_targets+=("$target") - done < <( - grep -E '^FAIL: //' "$console_log" \ - | sed -E 's#^FAIL: (//[^ ]+).*#\1#' \ - | sort -u - ) - - if [[ ${#failed_targets[@]} -eq 0 ]]; then - echo "No failed Bazel test targets were found in console output." - return - fi - - for target in "${failed_targets[@]}"; do - local rel_path="${target#//}" - rel_path="${rel_path/:/\/}" - local test_log="${testlogs_dir}/${rel_path}/test.log" - - echo "::group::Bazel test log tail for ${target}" - if [[ -f "$test_log" ]]; then - tail -n 200 "$test_log" - else - echo "Missing test log: $test_log" - fi - echo "::endgroup::" - done - } - - bazel_args=( - test - --test_verbose_timeout_warnings - --build_metadata=COMMIT_SHA=$(git rev-parse HEAD) - ) - - bazel_targets=( - //... - # Keep V8 out of the ordinary Bazel CI path. Only the dedicated - # canary and release workflows should build `third_party/v8`. + # Keep V8 out of the ordinary Bazel CI path. Only the dedicated + # canary and release workflows should build `third_party/v8`. + ./.github/scripts/run-bazel-ci.sh \ + --print-failed-test-logs \ + --use-node-test-env \ + -- \ + test \ + --test_verbose_timeout_warnings \ + --build_metadata=COMMIT_SHA=${GITHUB_SHA} \ + -- \ + //... \ -//third_party/v8:all - ) - - if [[ "${RUNNER_OS:-}" != "Windows" ]]; then - # Bazel test sandboxes on macOS may resolve an older Homebrew `node` - # before the `actions/setup-node` runtime on PATH. - node_bin="$(which node)" - bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}") - fi - - ci_config=ci-linux - if [[ "${RUNNER_OS:-}" == "macOS" ]]; then - ci_config=ci-macos - elif [[ "${RUNNER_OS:-}" == "Windows" ]]; then - ci_config=ci-windows - fi - - if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then - echo "BuildBuddy API key is available; using remote Bazel configuration." - # Work around Bazel 9 remote repo contents cache / overlay materialization failures - # seen in CI (for example "is not a symlink" or permission errors while - # materializing external repos such as rules_perl). We still use BuildBuddy for - # remote execution/cache; this only disables the startup-level repo contents cache. - set +e - bazel $BAZEL_STARTUP_ARGS \ - --noexperimental_remote_repo_contents_cache \ - "${bazel_args[@]}" \ - "--config=${ci_config}" \ - "--remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" \ - -- \ - "${bazel_targets[@]}" \ - 2>&1 | tee "$bazel_console_log" - bazel_status=${PIPESTATUS[0]} - set -e - else - echo "BuildBuddy API key is not available; using local Bazel configuration." - # Keep fork/community PRs on Bazel but disable remote services that are - # configured in .bazelrc and require auth. - # - # Flag docs: - # - Command-line reference: https://bazel.build/reference/command-line-reference - # - Remote caching overview: https://bazel.build/remote/caching - # - Remote execution overview: https://bazel.build/remote/rbe - # - Build Event Protocol overview: https://bazel.build/remote/bep - # - # --noexperimental_remote_repo_contents_cache: - # disable remote repo contents cache enabled in .bazelrc startup options. - # https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache - # --remote_cache= and --remote_executor=: - # clear remote cache/execution endpoints configured in .bazelrc. - # https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache - # https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor - set +e - bazel $BAZEL_STARTUP_ARGS \ - --noexperimental_remote_repo_contents_cache \ - "${bazel_args[@]}" \ - --remote_cache= \ - --remote_executor= \ - -- \ - "${bazel_targets[@]}" \ - 2>&1 | tee "$bazel_console_log" - bazel_status=${PIPESTATUS[0]} - set -e - fi - - if [[ ${bazel_status:-0} -ne 0 ]]; then - print_failed_bazel_test_logs "$bazel_console_log" - exit "$bazel_status" - fi # Save bazel repository cache explicitly; make non-fatal so cache uploading # never fails the overall job. Only save when key wasn't hit. - name: Save bazel repository cache - if: always() && !cancelled() && steps.cache_bazel_repository_restore.outputs.cache-hit != 'true' + if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true' + continue-on-error: true + uses: actions/cache/save@v5 + with: + path: | + ~/.cache/bazel-repo-cache + key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} + + clippy: + strategy: + fail-fast: false + matrix: + include: + # Keep Linux lint coverage on x64 and add the arm64 macOS path that + # the Bazel test job already exercises. + - os: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + - os: macos-15-xlarge + target: aarch64-apple-darwin + runs-on: ${{ matrix.os }} + name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }} + + steps: + - uses: actions/checkout@v6 + + - name: Set up Bazel CI + id: setup_bazel + uses: ./.github/actions/setup-bazel-ci + with: + target: ${{ matrix.target }} + + - name: bazel build --config=clippy //codex-rs/... + env: + BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} + shell: bash + run: | + # Keep the initial Bazel clippy scope on codex-rs and out of the + # V8 proof-of-concept target for now. + ./.github/scripts/run-bazel-ci.sh \ + -- \ + build \ + --config=clippy \ + --build_metadata=COMMIT_SHA=${GITHUB_SHA} \ + --build_metadata=TAG_job=clippy \ + -- \ + //codex-rs/... \ + -//codex-rs/v8-poc:all + + # Save bazel repository cache explicitly; make non-fatal so cache uploading + # never fails the overall job. Only save when key wasn't hit. + - name: Save bazel repository cache + if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true' continue-on-error: true uses: actions/cache/save@v5 with: diff --git a/codex-rs/BUILD.bazel b/codex-rs/BUILD.bazel index 47324dbdca..b6711d9b81 100644 --- a/codex-rs/BUILD.bazel +++ b/codex-rs/BUILD.bazel @@ -1,3 +1,4 @@ exports_files([ + "clippy.toml", "node-version.txt", ]) diff --git a/codex-rs/shell-escalation/src/unix/socket.rs b/codex-rs/shell-escalation/src/unix/socket.rs index 8325e940ff..aa97b5f379 100644 --- a/codex-rs/shell-escalation/src/unix/socket.rs +++ b/codex-rs/shell-escalation/src/unix/socket.rs @@ -60,8 +60,12 @@ fn extract_fds(control: &[u8]) -> Vec { if level == libc::SOL_SOCKET && ty == libc::SCM_RIGHTS { let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::() }; let fd_count: usize = { - let cmsg_data_len = - unsafe { (*cmsg).cmsg_len as usize } - unsafe { libc::CMSG_LEN(0) as usize }; + // `cmsghdr::cmsg_len` is not typed consistently across targets, so normalize it + // before doing the size arithmetic. + #[allow(clippy::useless_conversion)] + let cmsg_data_len = usize::try_from(unsafe { (*cmsg).cmsg_len }) + .expect("cmsghdr length fits") + - unsafe { libc::CMSG_LEN(0) as usize }; cmsg_data_len / size_of::() }; for i in 0..fd_count { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index b5e81edd9f..1fa27b6753 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -480,6 +480,7 @@ pub(crate) enum AppEvent { /// Voice transcription finished for the given placeholder id. #[cfg(not(target_os = "linux"))] + #[cfg_attr(not(feature = "voice-input"), allow(dead_code))] TranscriptionComplete { id: String, text: String, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index e291480d0b..e8abd9cad1 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -167,6 +167,7 @@ mod voice { pub(crate) enum RealtimeInputBehavior { Ungated, PlaybackAware { + #[allow(dead_code)] playback_queued_samples: Arc, }, } diff --git a/codex-rs/tui_app_server/src/app_event.rs b/codex-rs/tui_app_server/src/app_event.rs index e6d8bdecda..e652cb2351 100644 --- a/codex-rs/tui_app_server/src/app_event.rs +++ b/codex-rs/tui_app_server/src/app_event.rs @@ -491,6 +491,7 @@ pub(crate) enum AppEvent { /// Voice transcription finished for the given placeholder id. #[cfg(not(target_os = "linux"))] + #[cfg_attr(not(feature = "voice-input"), allow(dead_code))] TranscriptionComplete { id: String, text: String, diff --git a/justfile b/justfile index 5c9fa5e6ab..08e77e5b38 100644 --- a/justfile +++ b/justfile @@ -69,6 +69,9 @@ bazel-lock-check: bazel-test: bazel test //... --keep_going +bazel-clippy: + bazel build --config=clippy -- //codex-rs/... -//codex-rs/v8-poc:all + bazel-remote-test: bazel test //... --config=remote --platforms=//:rbe --keep_going