mirror of
https://github.com/openai/codex.git
synced 2026-06-02 19:31:59 +00:00
[codex] Make justfile recipes Windows-aware (#24983)
## Summary
Make the root `justfile` usable from Windows without maintaining a
separate Windows copy of most recipes.
The repo recipes previously assumed POSIX shell behavior for things like
variadic argument forwarding (`"$@"`) and stderr redirection
(`2>/dev/null`). That made common workflows such as `just fmt`, `just
test`, and `just log` unreliable from Windows. This PR introduces a
small cross-platform shell adapter so recipes can stay mostly unified
while still expanding the few shell-specific constructs correctly on
macOS/Linux and Windows.
## What Changed
- Add `scripts/just-shell.py` as the configured `just` shell adapter.
- On Unix it invokes `sh -cu`.
- On Windows it invokes `pwsh -CommandWithArgs` so arguments containing
spaces are preserved.
- Add portable recipe placeholders:
- `{args}` expands to `"$@"` on Unix and the equivalent PowerShell
forwarded-args expression on Windows.
- `{stderr-null}` expands to the platform-specific stderr suppression
used by `fmt`.
- Convert most variadic one-line recipes to the unified `{args}` form,
including `codex`, `exec`, `file-search`, `app-server-test-client`,
`fix`, `clippy`, `bench`, `mcp-server-run`, `write-app-server-schema`,
and `argument-comment-lint-from-source`.
- Keep genuinely shell-specific recipes split or Unix-only for now,
including recipes backed by `.sh` scripts or recipes whose bodies are
more than simple command forwarding.
- Add a Windows `just install` path that installs PowerShell via
`winget` when `pwsh` is not available, then runs the same basic Rust
setup steps.
- Update the SDK test that validates the root `fmt` recipe so it
recognizes the new portable stderr placeholder.
## Validation
- `just --summary`
- `just --dry-run fmt`
- `just --dry-run bench-smoke`
- `just --dry-run codex foo "bar binky" baz`
- `just --dry-run write-hooks-schema`
- `just --dry-run bazel-lock-update`
- `just --dry-run argument-comment-lint-from-source -- "foo bar"`
- `git diff --check -- justfile scripts/just-shell.py
sdk/python/tests/test_artifact_workflow_and_binaries.py`
- Verified Windows argv preservation through `scripts/just-shell.py`
with arguments containing spaces.
- `uv run --frozen --project sdk/python --extra dev pytest
sdk/python/tests/test_artifact_workflow_and_binaries.py::test_root_fmt_recipe_formats_rust_and_python_sdk`
This commit is contained in:
70
justfile
70
justfile
@@ -1,7 +1,11 @@
|
||||
set working-directory := "codex-rs"
|
||||
set positional-arguments
|
||||
export JUST_SHELL := justfile_directory() / "scripts/just-shell.py"
|
||||
set shell := ["python3", "-c", 'import os, runpy; runpy.run_path(os.environ["JUST_SHELL"], run_name="__main__")']
|
||||
set windows-shell := ["python", "-c", 'import os, runpy; runpy.run_path(os.environ["JUST_SHELL"], run_name="__main__")']
|
||||
|
||||
rust_min_stack := "8388608" # 8 MiB
|
||||
python := if os_family() == "windows" { "python" } else { "python3" }
|
||||
|
||||
# Display help
|
||||
help:
|
||||
@@ -10,81 +14,115 @@ help:
|
||||
# `codex`
|
||||
alias c := codex
|
||||
codex *args:
|
||||
cargo run --bin codex -- "$@"
|
||||
cargo run --bin codex -- {args}
|
||||
|
||||
# `codex exec`
|
||||
exec *args:
|
||||
cargo run --bin codex -- exec "$@"
|
||||
cargo run --bin codex -- exec {args}
|
||||
|
||||
# Start `codex exec-server` and run codex-tui.
|
||||
[unix]
|
||||
[no-cd]
|
||||
[positional-arguments]
|
||||
tui-with-exec-server *args:
|
||||
{{ justfile_directory() }}/scripts/run_tui_with_exec_server.sh "$@"
|
||||
|
||||
# Run the CLI version of the file-search crate.
|
||||
file-search *args:
|
||||
cargo run --bin codex-file-search -- "$@"
|
||||
cargo run --bin codex-file-search -- {args}
|
||||
|
||||
# Build the CLI and run the app-server test client
|
||||
app-server-test-client *args:
|
||||
cargo build -p codex-cli
|
||||
cargo run -p codex-app-server-test-client -- --codex-bin ./target/debug/codex "$@"
|
||||
cargo run -p codex-app-server-test-client -- --codex-bin ./target/debug/codex {args}
|
||||
|
||||
# Format Rust and Python SDK code.
|
||||
fmt:
|
||||
cargo fmt -- --config imports_granularity=Item 2>/dev/null
|
||||
cargo fmt -- --config imports_granularity=Item {stderr-null}
|
||||
uv run --frozen --project ../sdk/python --extra dev ruff check --fix --fix-only ../sdk/python
|
||||
uv run --frozen --project ../sdk/python --extra dev ruff format ../sdk/python
|
||||
|
||||
fix *args:
|
||||
cargo clippy --fix --tests --allow-dirty "$@"
|
||||
cargo clippy --fix --tests --allow-dirty {args}
|
||||
|
||||
clippy *args:
|
||||
cargo clippy --tests "$@"
|
||||
cargo clippy --tests {args}
|
||||
|
||||
[unix]
|
||||
install:
|
||||
rustup show active-toolchain
|
||||
cargo fetch
|
||||
|
||||
[windows]
|
||||
install:
|
||||
#!powershell.exe -File
|
||||
$pwsh = Get-Command pwsh.exe -ErrorAction SilentlyContinue
|
||||
if (-not $pwsh) {
|
||||
winget install --exact --id Microsoft.PowerShell --source winget --accept-package-agreements --accept-source-agreements
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
rustup show active-toolchain
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
cargo fetch
|
||||
exit $LASTEXITCODE
|
||||
|
||||
# Run nextest with --no-fail-fast so all tests are run.
|
||||
#
|
||||
# Run `cargo install --locked cargo-nextest` if you don't have it installed.
|
||||
# Prefer this for routine local runs. Workspace crate features are banned, so
|
||||
# there should be no need to add `--all-features`.
|
||||
[unix]
|
||||
test *args:
|
||||
RUST_MIN_STACK={{ rust_min_stack }} cargo nextest run --no-fail-fast "$@"
|
||||
just bench-smoke
|
||||
|
||||
[windows]
|
||||
test *args:
|
||||
$env:RUST_MIN_STACK = "{{ rust_min_stack }}"; cargo nextest run --no-fail-fast @($args | Select-Object -Skip 1)
|
||||
just bench-smoke
|
||||
|
||||
# Run explicit workspace benchmark targets.
|
||||
bench *args:
|
||||
cargo bench --workspace --bench '*' "$@"
|
||||
cargo bench --workspace --bench '*' {args}
|
||||
|
||||
# Run benchmark targets once to ensure they start successfully.
|
||||
bench-smoke:
|
||||
just bench -- --test
|
||||
|
||||
# Build and run Codex from source using Bazel.
|
||||
# Note we have to use the combination of `[no-cd]` and `--run_under="cd $PWD &&"`
|
||||
# to ensure that Bazel runs the command in the current working directory.
|
||||
# On Unix, use `[no-cd]` and `--run_under="cd $PWD &&"` to ensure Bazel runs
|
||||
# the command in the current working directory.
|
||||
[unix]
|
||||
[no-cd]
|
||||
bazel-codex *args:
|
||||
bazel run //codex-rs/cli:codex --run_under="cd $PWD &&" -- "$@"
|
||||
|
||||
[windows]
|
||||
bazel-codex *args:
|
||||
bazel run //codex-rs/cli:codex --run_under='cd /d "{{invocation_directory_native()}}" &&' -- @($args | Select-Object -Skip 1)
|
||||
|
||||
[no-cd]
|
||||
bazel-lock-update:
|
||||
bazel mod deps --lockfile_mode=update
|
||||
|
||||
[unix]
|
||||
[no-cd]
|
||||
bazel-lock-check:
|
||||
{{ justfile_directory() }}/scripts/check-module-bazel-lock.sh
|
||||
|
||||
[windows]
|
||||
bazel-lock-check:
|
||||
bazel mod deps --lockfile_mode=error; if ($LASTEXITCODE -ne 0) { Write-Error "MODULE.bazel.lock is out of date. Run 'just bazel-lock-update' and commit the updated lockfile."; exit 1 }
|
||||
|
||||
bazel-test:
|
||||
bazel test --test_tag_filters=-argument-comment-lint //... --keep_going
|
||||
|
||||
[unix]
|
||||
[no-cd]
|
||||
bazel-clippy:
|
||||
bazel_targets="$({{ justfile_directory() }}/scripts/list-bazel-clippy-targets.sh)" && bazel build --config=clippy -- ${bazel_targets}
|
||||
|
||||
[unix]
|
||||
[no-cd]
|
||||
bazel-argument-comment-lint:
|
||||
bazel build --config=argument-comment-lint -- $({{ justfile_directory() }}/tools/argument-comment-lint/list-bazel-targets.sh)
|
||||
@@ -97,7 +135,7 @@ build-for-release:
|
||||
|
||||
# Run the MCP server
|
||||
mcp-server-run *args:
|
||||
cargo run -p codex-mcp-server -- "$@"
|
||||
cargo run -p codex-mcp-server -- {args}
|
||||
|
||||
# Regenerate the json schema for config.toml from the current config types.
|
||||
write-config-schema:
|
||||
@@ -105,13 +143,14 @@ write-config-schema:
|
||||
|
||||
# Regenerate vendored app-server protocol schema artifacts.
|
||||
write-app-server-schema *args:
|
||||
cargo run -p codex-app-server-protocol --bin write_schema_fixtures -- "$@"
|
||||
cargo run -p codex-app-server-protocol --bin write_schema_fixtures -- {args}
|
||||
|
||||
[no-cd]
|
||||
write-hooks-schema:
|
||||
cargo run --manifest-path {{ justfile_directory() }}/codex-rs/Cargo.toml -p codex-hooks --bin write_hooks_schema_fixtures
|
||||
|
||||
# Run the argument-comment Dylint checks across codex-rs.
|
||||
[unix]
|
||||
[no-cd]
|
||||
argument-comment-lint *args:
|
||||
if [ "$#" -eq 0 ]; then \
|
||||
@@ -122,8 +161,13 @@ argument-comment-lint *args:
|
||||
|
||||
[no-cd]
|
||||
argument-comment-lint-from-source *args:
|
||||
{{ justfile_directory() }}/tools/argument-comment-lint/run.py "$@"
|
||||
{{ python }} {{ justfile_directory() }}/tools/argument-comment-lint/run.py {args}
|
||||
|
||||
# Tail logs from the state SQLite database
|
||||
[unix]
|
||||
log *args:
|
||||
if [ "${1:-}" = "--" ]; then shift; fi; cargo run -p codex-state --bin logs_client -- "$@"
|
||||
|
||||
[windows]
|
||||
log *args:
|
||||
$forwarded_args = @($args | Select-Object -Skip 1); if ($forwarded_args.Count -gt 0 -and $forwarded_args[0] -eq "--") { $forwarded_args = @($forwarded_args | Select-Object -Skip 1) }; cargo run -p codex-state --bin logs_client -- @forwarded_args
|
||||
|
||||
72
scripts/just-shell.py
Normal file
72
scripts/just-shell.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-platform shell launcher for `just` recipes.
|
||||
|
||||
This keeps recipe bodies as normal shell snippets while giving the justfile one
|
||||
portable placeholder, `{args}`, for forwarding variadic recipe arguments.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
ARGS_TOKEN = "{args}"
|
||||
STDERR_NULL_TOKEN = "{stderr-null}"
|
||||
POWERSHELL_ARGS = "@($args | Select-Object -Skip 1)"
|
||||
POWERSHELL_STDERR_NULL = '2>$null; exit $LASTEXITCODE'
|
||||
SH_ARGS = '"$@"'
|
||||
SH_STDERR_NULL = "2>/dev/null"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("just shell adapter expected a recipe command.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
command = sys.argv[1]
|
||||
recipe_name = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
recipe_args = sys.argv[3:]
|
||||
|
||||
if os.name == "nt":
|
||||
return run_powershell(command, recipe_name, recipe_args)
|
||||
else:
|
||||
return run_sh(command, recipe_name, recipe_args)
|
||||
|
||||
|
||||
def run_sh(command: str, recipe_name: str, recipe_args: list[str]) -> int:
|
||||
command = command.replace(ARGS_TOKEN, SH_ARGS)
|
||||
command = command.replace(STDERR_NULL_TOKEN, SH_STDERR_NULL)
|
||||
os.execvp("sh", ["sh", "-cu", command, recipe_name, *recipe_args])
|
||||
|
||||
|
||||
def run_powershell(command: str, recipe_name: str, recipe_args: list[str]) -> int:
|
||||
pwsh = shutil.which("pwsh.exe") or shutil.which("pwsh")
|
||||
if pwsh is None:
|
||||
print(
|
||||
"PowerShell ('pwsh') is required for Windows just recipes. "
|
||||
"Run 'just install' to install it.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
command = command.replace(ARGS_TOKEN, POWERSHELL_ARGS)
|
||||
command = command.replace(STDERR_NULL_TOKEN, POWERSHELL_STDERR_NULL)
|
||||
return subprocess.run(
|
||||
[
|
||||
pwsh,
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-CommandWithArgs",
|
||||
command,
|
||||
recipe_name,
|
||||
*recipe_args,
|
||||
],
|
||||
check=False,
|
||||
).returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -81,14 +81,16 @@ def test_root_fmt_recipe_formats_rust_and_python_sdk() -> None:
|
||||
fmt_recipe = lines[fmt_index:next_recipe_index]
|
||||
actual = {
|
||||
"working_directory": lines[0],
|
||||
"previous_attribute": lines[fmt_index - 1],
|
||||
"previous_comment": next(
|
||||
line for line in reversed(lines[:fmt_index]) if line.startswith("#")
|
||||
),
|
||||
"commands": [line.strip() for line in fmt_recipe[1:] if line.strip()],
|
||||
}
|
||||
expected = {
|
||||
"working_directory": 'set working-directory := "codex-rs"',
|
||||
"previous_attribute": "# Format Rust and Python SDK code.",
|
||||
"previous_comment": "# Format Rust and Python SDK code.",
|
||||
"commands": [
|
||||
"cargo fmt -- --config imports_granularity=Item 2>/dev/null",
|
||||
"cargo fmt -- --config imports_granularity=Item {stderr-null}",
|
||||
"uv run --frozen --project ../sdk/python --extra dev ruff check --fix --fix-only ../sdk/python",
|
||||
"uv run --frozen --project ../sdk/python --extra dev ruff format ../sdk/python",
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user