Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .github/CODEOWNERS +17 -0
- .github/blob-size-allowlist.txt +11 -0
- .github/codex/home/config.toml +3 -0
- .github/codex/labels/codex-attempt.md +9 -0
- .github/codex/labels/codex-review.md +7 -0
- .github/codex/labels/codex-rust-review.md +139 -0
- .github/dependabot.yaml +36 -0
- .github/dotslash-argument-comment-lint-config.json +24 -0
- .github/dotslash-config.json +136 -0
- .github/dotslash-provisioned-config.json +16 -0
- .github/dotslash-zsh-config.json +32 -0
- .github/workflows/cargo-deny.yml +31 -0
- .github/workflows/python-runtime-release.yml +63 -0
- .vscode/extensions.json +13 -0
- .vscode/launch.json +22 -0
- .vscode/settings.json +27 -0
- bazel/build-info/BUILD.bazel +12 -0
- bazel/extensions/BUILD.bazel +3 -0
- bazel/extensions/codex_release_archive.bzl +76 -0
- bazel/modules/BUILD.bazel +1 -0
- bazel/modules/wine.MODULE.bazel +26 -0
- bazel/platforms/BUILD.bazel +1 -0
- bazel/platforms/release_binaries.bzl +30 -0
- bazel/rules/BUILD.bazel +6 -0
- bazel/rules/e2e_benchmark.bzl +56 -0
- bazel/rules/schema_bundle.bzl +39 -0
- bazel/rules/testing/BUILD.bazel +5 -0
- bazel/rules/testing/compat/BUILD.bazel +31 -0
- bazel/rules/testing/compat/exec_server_compat_test.bzl +47 -0
- bazel/rules/testing/compat/exec_server_compat_test.rs +379 -0
- bazel/rules/testing/foreign_platform_binary.bzl +53 -0
- bazel/rules/testing/wine/BUILD.bazel +54 -0
- bazel/rules/testing/wine/fixtures/windows_smoke.rs +15 -0
- bazel/rules/testing/wine/src/lib.rs +402 -0
- bazel/rules/testing/wine/src/lib_tests.rs +451 -0
- bazel/rules/testing/wine/wine.bzl +69 -0
- bazel/rules/testing/wine/wine_runtime.bzl +40 -0
- bazel/schema/BUILD.bazel +8 -0
- bazel/toolchains/prost/BUILD.bazel +35 -0
- scripts/build_codex_package.py +16 -0
- scripts/build_winget_package.py +37 -0
- scripts/check-module-bazel-lock.sh +9 -0
- scripts/codex_package/README.md +92 -0
- scripts/codex_package/__init__.py +1 -0
- scripts/codex_package/archive.py +118 -0
- scripts/codex_package/cargo.py +194 -0
- scripts/codex_package/cli.py +249 -0
- scripts/codex_package/codex-zsh +55 -0
- scripts/codex_package/dotslash.py +222 -0
- scripts/codex_package/layout.py +187 -0
.github/CODEOWNERS
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core crate ownership.
|
| 2 |
+
/codex-rs/arg0/ @openai/codex-core-agent-team
|
| 3 |
+
/codex-rs/codex-mcp/ @openai/codex-core-agent-team
|
| 4 |
+
/codex-rs/core/ @openai/codex-core-agent-team
|
| 5 |
+
/codex-rs/exec-server/ @openai/codex-core-agent-team
|
| 6 |
+
/codex-rs/exec-server-protocol/ @openai/codex-core-agent-team
|
| 7 |
+
/codex-rs/ext/extension-api/ @openai/codex-core-agent-team
|
| 8 |
+
/codex-rs/prompts/ @openai/codex-core-agent-team
|
| 9 |
+
/codex-rs/utils/path-uri/ @openai/codex-core-agent-team
|
| 10 |
+
|
| 11 |
+
# Keep macOS AKV signing changes reviewed by Codex maintainers.
|
| 12 |
+
/.github/actions/setup-akv-pkcs11-codesigning/ @openai/codex-core-agent-team
|
| 13 |
+
/.github/scripts/macos-signing/ @openai/codex-core-agent-team
|
| 14 |
+
/.github/workflows/rust-release.yml @openai/codex-core-agent-team
|
| 15 |
+
|
| 16 |
+
# Keep ownership changes reviewed by the same team.
|
| 17 |
+
/.github/CODEOWNERS @openai/codex-core-agent-team
|
.github/blob-size-allowlist.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Paths are matched exactly, relative to the repository root.
|
| 2 |
+
# Keep this list short and limited to intentional large checked-in assets.
|
| 3 |
+
|
| 4 |
+
.github/codex-cli-splash.png
|
| 5 |
+
MODULE.bazel.lock
|
| 6 |
+
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
|
| 7 |
+
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
|
| 8 |
+
codex-rs/models-manager/models.json
|
| 9 |
+
codex-rs/tui/tests/fixtures/oss-story.jsonl
|
| 10 |
+
codex-rs/tui_app_server/tests/fixtures/oss-story.jsonl
|
| 11 |
+
codex-rs/tui/src/app.rs
|
.github/codex/home/config.toml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model = "gpt-5.1"
|
| 2 |
+
|
| 3 |
+
# Consider setting [mcp_servers] here!
|
.github/codex/labels/codex-attempt.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Attempt to solve the reported issue.
|
| 2 |
+
|
| 3 |
+
If a code change is required, create a new branch, commit the fix, and open a pull request that resolves the problem.
|
| 4 |
+
|
| 5 |
+
Here is the original GitHub issue that triggered this run:
|
| 6 |
+
|
| 7 |
+
### {CODEX_ACTION_ISSUE_TITLE}
|
| 8 |
+
|
| 9 |
+
{CODEX_ACTION_ISSUE_BODY}
|
.github/codex/labels/codex-review.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Review this PR and respond with a very concise final message, formatted in Markdown.
|
| 2 |
+
|
| 3 |
+
There should be a summary of the changes (1-2 sentences) and a few bullet points if necessary.
|
| 4 |
+
|
| 5 |
+
Then provide the **review** (1-2 sentences plus bullet points, friendly tone).
|
| 6 |
+
|
| 7 |
+
{CODEX_ACTION_GITHUB_EVENT_PATH} contains the JSON that triggered this GitHub workflow. It contains the `base` and `head` refs that define this PR. Both refs are available locally.
|
.github/codex/labels/codex-rust-review.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Review this PR and respond with a very concise final message, formatted in Markdown.
|
| 2 |
+
|
| 3 |
+
There should be a summary of the changes (1-2 sentences) and a few bullet points if necessary.
|
| 4 |
+
|
| 5 |
+
Then provide the **review** (1-2 sentences plus bullet points, friendly tone).
|
| 6 |
+
|
| 7 |
+
Things to look out for when doing the review:
|
| 8 |
+
|
| 9 |
+
## General Principles
|
| 10 |
+
|
| 11 |
+
- **Make sure the pull request body explains the motivation behind the change.** If the author has failed to do this, call it out, and if you think you can deduce the motivation behind the change, propose copy.
|
| 12 |
+
- Ideally, the PR body also contains a small summary of the change. For small changes, the PR title may be sufficient.
|
| 13 |
+
- Each PR should ideally do one conceptual thing. For example, if a PR does a refactoring as well as introducing a new feature, push back and suggest the refactoring be done in a separate PR. This makes things easier for the reviewer, as refactoring changes can often be far-reaching, yet quick to review.
|
| 14 |
+
- When introducing new code, be on the lookout for code that duplicates existing code. When found, propose a way to refactor the existing code such that it should be reused.
|
| 15 |
+
|
| 16 |
+
## Code Organization
|
| 17 |
+
|
| 18 |
+
- Each crate in the Cargo workspace in `codex-rs` has a specific purpose: make a note if you believe new code is not introduced in the correct crate.
|
| 19 |
+
- When possible, try to keep the `core` crate as small as possible. Non-core but shared logic is often a good candidate for `codex-rs/common`.
|
| 20 |
+
- Be wary of large files and offer suggestions for how to break things into more reasonably-sized files.
|
| 21 |
+
- Rust files should generally be organized such that the public parts of the API appear near the top of the file and helper functions go below. This is analogous to the "inverted pyramid" structure that is favored in journalism.
|
| 22 |
+
|
| 23 |
+
## Assertions in Tests
|
| 24 |
+
|
| 25 |
+
Assert the equality of the entire objects instead of doing "piecemeal comparisons," performing `assert_eq!()` on individual fields.
|
| 26 |
+
|
| 27 |
+
Note that unit tests also function as "executable documentation." As shown in the following example, "piecemeal comparisons" are often more verbose, provide less coverage, and are not as useful as executable documentation.
|
| 28 |
+
|
| 29 |
+
For example, suppose you have the following enum:
|
| 30 |
+
|
| 31 |
+
```rust
|
| 32 |
+
#[derive(Debug, PartialEq)]
|
| 33 |
+
enum Message {
|
| 34 |
+
Request {
|
| 35 |
+
id: String,
|
| 36 |
+
method: String,
|
| 37 |
+
params: Option<serde_json::Value>,
|
| 38 |
+
},
|
| 39 |
+
Notification {
|
| 40 |
+
method: String,
|
| 41 |
+
params: Option<serde_json::Value>,
|
| 42 |
+
},
|
| 43 |
+
}
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
This is an example of a _piecemeal_ comparison:
|
| 47 |
+
|
| 48 |
+
```rust
|
| 49 |
+
// BAD: Piecemeal Comparison
|
| 50 |
+
|
| 51 |
+
#[test]
|
| 52 |
+
fn test_get_latest_messages() {
|
| 53 |
+
let messages = get_latest_messages();
|
| 54 |
+
assert_eq!(messages.len(), 2);
|
| 55 |
+
|
| 56 |
+
let m0 = &messages[0];
|
| 57 |
+
match m0 {
|
| 58 |
+
Message::Request { id, method, params } => {
|
| 59 |
+
assert_eq!(id, "123");
|
| 60 |
+
assert_eq!(method, "subscribe");
|
| 61 |
+
assert_eq!(
|
| 62 |
+
*params,
|
| 63 |
+
Some(json!({
|
| 64 |
+
"conversation_id": "x42z86"
|
| 65 |
+
}))
|
| 66 |
+
)
|
| 67 |
+
}
|
| 68 |
+
Message::Notification { .. } => {
|
| 69 |
+
panic!("expected Request");
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
let m1 = &messages[1];
|
| 74 |
+
match m1 {
|
| 75 |
+
Message::Request { .. } => {
|
| 76 |
+
panic!("expected Notification");
|
| 77 |
+
}
|
| 78 |
+
Message::Notification { method, params } => {
|
| 79 |
+
assert_eq!(method, "log");
|
| 80 |
+
assert_eq!(
|
| 81 |
+
*params,
|
| 82 |
+
Some(json!({
|
| 83 |
+
"level": "info",
|
| 84 |
+
"message": "subscribed"
|
| 85 |
+
}))
|
| 86 |
+
)
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
This is a _deep_ comparison:
|
| 93 |
+
|
| 94 |
+
```rust
|
| 95 |
+
// GOOD: Verify the entire structure with a single assert_eq!().
|
| 96 |
+
|
| 97 |
+
use pretty_assertions::assert_eq;
|
| 98 |
+
|
| 99 |
+
#[test]
|
| 100 |
+
fn test_get_latest_messages() {
|
| 101 |
+
let messages = get_latest_messages();
|
| 102 |
+
|
| 103 |
+
assert_eq!(
|
| 104 |
+
vec![
|
| 105 |
+
Message::Request {
|
| 106 |
+
id: "123".to_string(),
|
| 107 |
+
method: "subscribe".to_string(),
|
| 108 |
+
params: Some(json!({
|
| 109 |
+
"conversation_id": "x42z86"
|
| 110 |
+
})),
|
| 111 |
+
},
|
| 112 |
+
Message::Notification {
|
| 113 |
+
method: "log".to_string(),
|
| 114 |
+
params: Some(json!({
|
| 115 |
+
"level": "info",
|
| 116 |
+
"message": "subscribed"
|
| 117 |
+
})),
|
| 118 |
+
},
|
| 119 |
+
],
|
| 120 |
+
messages,
|
| 121 |
+
);
|
| 122 |
+
}
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## More Tactical Rust Things To Look Out For
|
| 126 |
+
|
| 127 |
+
- Do not use `unsafe` (unless you have a really, really good reason like using an operating system API directly and no safe wrapper exists). For example, there are cases where it is tempting to use `unsafe` in order to use `std::env::set_var()`, but this indeed `unsafe` and has led to race conditions on multiple occasions. (When this happens, find a mechanism other than environment variables to use for configuration.)
|
| 128 |
+
- Encourage the use of small enums or the newtype pattern in Rust if it helps readability without adding significant cognitive load or lines of code.
|
| 129 |
+
- If you see opportunities for the changes in a diff to use more idiomatic Rust, please make specific recommendations. For example, favor the use of expressions over `return`.
|
| 130 |
+
- When modifying a `Cargo.toml` file, make sure that dependency lists stay alphabetically sorted. Also consider whether a new dependency is added to the appropriate place (e.g., `[dependencies]` versus `[dev-dependencies]`)
|
| 131 |
+
|
| 132 |
+
## Pull Request Body
|
| 133 |
+
|
| 134 |
+
- If the nature of the change seems to have a visual component (which is often the case for changes to `codex-rs/tui`), recommend including a screenshot or video to demonstrate the change, if appropriate.
|
| 135 |
+
- References to existing GitHub issues and PRs are encouraged, where appropriate, though you likely do not have network access, so may not be able to help here.
|
| 136 |
+
|
| 137 |
+
# PR Information
|
| 138 |
+
|
| 139 |
+
{CODEX_ACTION_GITHUB_EVENT_PATH} contains the JSON that triggered this GitHub workflow. It contains the `base` and `head` refs that define this PR. Both refs are available locally.
|
.github/dependabot.yaml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
|
| 2 |
+
|
| 3 |
+
version: 2
|
| 4 |
+
updates:
|
| 5 |
+
- package-ecosystem: bun
|
| 6 |
+
directory: .github/actions/codex
|
| 7 |
+
schedule:
|
| 8 |
+
interval: weekly
|
| 9 |
+
cooldown:
|
| 10 |
+
default-days: 7
|
| 11 |
+
- package-ecosystem: cargo
|
| 12 |
+
directories:
|
| 13 |
+
- codex-rs
|
| 14 |
+
- codex-rs/*
|
| 15 |
+
schedule:
|
| 16 |
+
interval: weekly
|
| 17 |
+
cooldown:
|
| 18 |
+
default-days: 7
|
| 19 |
+
- package-ecosystem: docker
|
| 20 |
+
directory: codex-cli
|
| 21 |
+
schedule:
|
| 22 |
+
interval: weekly
|
| 23 |
+
cooldown:
|
| 24 |
+
default-days: 7
|
| 25 |
+
- package-ecosystem: github-actions
|
| 26 |
+
directory: /
|
| 27 |
+
schedule:
|
| 28 |
+
interval: weekly
|
| 29 |
+
cooldown:
|
| 30 |
+
default-days: 7
|
| 31 |
+
- package-ecosystem: rust-toolchain
|
| 32 |
+
directory: codex-rs
|
| 33 |
+
schedule:
|
| 34 |
+
interval: weekly
|
| 35 |
+
cooldown:
|
| 36 |
+
default-days: 7
|
.github/dotslash-argument-comment-lint-config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"outputs": {
|
| 3 |
+
"argument-comment-lint": {
|
| 4 |
+
"platforms": {
|
| 5 |
+
"macos-aarch64": {
|
| 6 |
+
"regex": "^argument-comment-lint-aarch64-apple-darwin\\.tar\\.gz$",
|
| 7 |
+
"path": "argument-comment-lint/bin/argument-comment-lint"
|
| 8 |
+
},
|
| 9 |
+
"linux-x86_64": {
|
| 10 |
+
"regex": "^argument-comment-lint-x86_64-unknown-linux-gnu\\.tar\\.gz$",
|
| 11 |
+
"path": "argument-comment-lint/bin/argument-comment-lint"
|
| 12 |
+
},
|
| 13 |
+
"linux-aarch64": {
|
| 14 |
+
"regex": "^argument-comment-lint-aarch64-unknown-linux-gnu\\.tar\\.gz$",
|
| 15 |
+
"path": "argument-comment-lint/bin/argument-comment-lint"
|
| 16 |
+
},
|
| 17 |
+
"windows-x86_64": {
|
| 18 |
+
"regex": "^argument-comment-lint-x86_64-pc-windows-msvc\\.zip$",
|
| 19 |
+
"path": "argument-comment-lint/bin/argument-comment-lint.exe"
|
| 20 |
+
}
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
}
|
.github/dotslash-config.json
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"outputs": {
|
| 3 |
+
"codex": {
|
| 4 |
+
"platforms": {
|
| 5 |
+
"macos-aarch64": {
|
| 6 |
+
"regex": "^codex-package-aarch64-apple-darwin\\.tar\\.zst$",
|
| 7 |
+
"path": "bin/codex"
|
| 8 |
+
},
|
| 9 |
+
"macos-x86_64": {
|
| 10 |
+
"regex": "^codex-package-x86_64-apple-darwin\\.tar\\.zst$",
|
| 11 |
+
"path": "bin/codex"
|
| 12 |
+
},
|
| 13 |
+
"linux-x86_64": {
|
| 14 |
+
"regex": "^codex-package-x86_64-unknown-linux-musl\\.tar\\.zst$",
|
| 15 |
+
"path": "bin/codex"
|
| 16 |
+
},
|
| 17 |
+
"linux-aarch64": {
|
| 18 |
+
"regex": "^codex-package-aarch64-unknown-linux-musl\\.tar\\.zst$",
|
| 19 |
+
"path": "bin/codex"
|
| 20 |
+
},
|
| 21 |
+
"windows-x86_64": {
|
| 22 |
+
"regex": "^codex-package-x86_64-pc-windows-msvc\\.tar\\.zst$",
|
| 23 |
+
"path": "bin/codex.exe"
|
| 24 |
+
},
|
| 25 |
+
"windows-aarch64": {
|
| 26 |
+
"regex": "^codex-package-aarch64-pc-windows-msvc\\.tar\\.zst$",
|
| 27 |
+
"path": "bin/codex.exe"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"codex-app-server": {
|
| 32 |
+
"platforms": {
|
| 33 |
+
"macos-aarch64": {
|
| 34 |
+
"regex": "^codex-app-server-package-aarch64-apple-darwin\\.tar\\.zst$",
|
| 35 |
+
"path": "bin/codex-app-server"
|
| 36 |
+
},
|
| 37 |
+
"macos-x86_64": {
|
| 38 |
+
"regex": "^codex-app-server-package-x86_64-apple-darwin\\.tar\\.zst$",
|
| 39 |
+
"path": "bin/codex-app-server"
|
| 40 |
+
},
|
| 41 |
+
"linux-x86_64": {
|
| 42 |
+
"regex": "^codex-app-server-package-x86_64-unknown-linux-musl\\.tar\\.zst$",
|
| 43 |
+
"path": "bin/codex-app-server"
|
| 44 |
+
},
|
| 45 |
+
"linux-aarch64": {
|
| 46 |
+
"regex": "^codex-app-server-package-aarch64-unknown-linux-musl\\.tar\\.zst$",
|
| 47 |
+
"path": "bin/codex-app-server"
|
| 48 |
+
},
|
| 49 |
+
"windows-x86_64": {
|
| 50 |
+
"regex": "^codex-app-server-package-x86_64-pc-windows-msvc\\.tar\\.zst$",
|
| 51 |
+
"path": "bin/codex-app-server.exe"
|
| 52 |
+
},
|
| 53 |
+
"windows-aarch64": {
|
| 54 |
+
"regex": "^codex-app-server-package-aarch64-pc-windows-msvc\\.tar\\.zst$",
|
| 55 |
+
"path": "bin/codex-app-server.exe"
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"codex-responses-api-proxy": {
|
| 60 |
+
"platforms": {
|
| 61 |
+
"macos-aarch64": {
|
| 62 |
+
"regex": "^codex-responses-api-proxy-aarch64-apple-darwin\\.zst$",
|
| 63 |
+
"path": "codex-responses-api-proxy"
|
| 64 |
+
},
|
| 65 |
+
"macos-x86_64": {
|
| 66 |
+
"regex": "^codex-responses-api-proxy-x86_64-apple-darwin\\.zst$",
|
| 67 |
+
"path": "codex-responses-api-proxy"
|
| 68 |
+
},
|
| 69 |
+
"linux-x86_64": {
|
| 70 |
+
"regex": "^codex-responses-api-proxy-x86_64-unknown-linux-musl\\.zst$",
|
| 71 |
+
"path": "codex-responses-api-proxy"
|
| 72 |
+
},
|
| 73 |
+
"linux-aarch64": {
|
| 74 |
+
"regex": "^codex-responses-api-proxy-aarch64-unknown-linux-musl\\.zst$",
|
| 75 |
+
"path": "codex-responses-api-proxy"
|
| 76 |
+
},
|
| 77 |
+
"windows-x86_64": {
|
| 78 |
+
"regex": "^codex-responses-api-proxy-x86_64-pc-windows-msvc\\.exe\\.zst$",
|
| 79 |
+
"path": "codex-responses-api-proxy.exe"
|
| 80 |
+
},
|
| 81 |
+
"windows-aarch64": {
|
| 82 |
+
"regex": "^codex-responses-api-proxy-aarch64-pc-windows-msvc\\.exe\\.zst$",
|
| 83 |
+
"path": "codex-responses-api-proxy.exe"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
},
|
| 87 |
+
"bwrap": {
|
| 88 |
+
"platforms": {
|
| 89 |
+
"linux-x86_64": {
|
| 90 |
+
"regex": "^bwrap-x86_64-unknown-linux-musl\\.zst$",
|
| 91 |
+
"path": "bwrap"
|
| 92 |
+
},
|
| 93 |
+
"linux-aarch64": {
|
| 94 |
+
"regex": "^bwrap-aarch64-unknown-linux-musl\\.zst$",
|
| 95 |
+
"path": "bwrap"
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
},
|
| 99 |
+
"codex-command-runner": {
|
| 100 |
+
"platforms": {
|
| 101 |
+
"windows-x86_64": {
|
| 102 |
+
"regex": "^codex-command-runner-x86_64-pc-windows-msvc\\.exe\\.zst$",
|
| 103 |
+
"path": "codex-command-runner.exe"
|
| 104 |
+
},
|
| 105 |
+
"windows-aarch64": {
|
| 106 |
+
"regex": "^codex-command-runner-aarch64-pc-windows-msvc\\.exe\\.zst$",
|
| 107 |
+
"path": "codex-command-runner.exe"
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"codex-windows-sandbox-setup": {
|
| 112 |
+
"platforms": {
|
| 113 |
+
"windows-x86_64": {
|
| 114 |
+
"regex": "^codex-windows-sandbox-setup-x86_64-pc-windows-msvc\\.exe\\.zst$",
|
| 115 |
+
"path": "codex-windows-sandbox-setup.exe"
|
| 116 |
+
},
|
| 117 |
+
"windows-aarch64": {
|
| 118 |
+
"regex": "^codex-windows-sandbox-setup-aarch64-pc-windows-msvc\\.exe\\.zst$",
|
| 119 |
+
"path": "codex-windows-sandbox-setup.exe"
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"codex-windows-sandbox-service": {
|
| 124 |
+
"platforms": {
|
| 125 |
+
"windows-x86_64": {
|
| 126 |
+
"regex": "^codex-windows-sandbox-service-x86_64-pc-windows-msvc\\.exe\\.zst$",
|
| 127 |
+
"path": "codex-windows-sandbox-service.exe"
|
| 128 |
+
},
|
| 129 |
+
"windows-aarch64": {
|
| 130 |
+
"regex": "^codex-windows-sandbox-service-aarch64-pc-windows-msvc\\.exe\\.zst$",
|
| 131 |
+
"path": "codex-windows-sandbox-service.exe"
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
.github/dotslash-provisioned-config.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"outputs": {
|
| 3 |
+
"codex-provisioned": {
|
| 4 |
+
"platforms": {
|
| 5 |
+
"macos-aarch64": {
|
| 6 |
+
"regex": "^codex-provisioned-package-aarch64-apple-darwin\\.tar\\.gz$",
|
| 7 |
+
"path": "bin/codex"
|
| 8 |
+
},
|
| 9 |
+
"macos-x86_64": {
|
| 10 |
+
"regex": "^codex-provisioned-package-x86_64-apple-darwin\\.tar\\.gz$",
|
| 11 |
+
"path": "bin/codex"
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
}
|
| 16 |
+
}
|
.github/dotslash-zsh-config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"outputs": {
|
| 3 |
+
"codex-zsh": {
|
| 4 |
+
"platforms": {
|
| 5 |
+
"macos-aarch64": {
|
| 6 |
+
"name": "codex-zsh-aarch64-apple-darwin.tar.gz",
|
| 7 |
+
"format": "tar.gz",
|
| 8 |
+
"hash": "sha256",
|
| 9 |
+
"path": "codex-zsh/bin/zsh"
|
| 10 |
+
},
|
| 11 |
+
"macos-x86_64": {
|
| 12 |
+
"name": "codex-zsh-x86_64-apple-darwin.tar.gz",
|
| 13 |
+
"format": "tar.gz",
|
| 14 |
+
"hash": "sha256",
|
| 15 |
+
"path": "codex-zsh/bin/zsh"
|
| 16 |
+
},
|
| 17 |
+
"linux-x86_64": {
|
| 18 |
+
"name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz",
|
| 19 |
+
"format": "tar.gz",
|
| 20 |
+
"hash": "sha256",
|
| 21 |
+
"path": "codex-zsh/bin/zsh"
|
| 22 |
+
},
|
| 23 |
+
"linux-aarch64": {
|
| 24 |
+
"name": "codex-zsh-aarch64-unknown-linux-musl.tar.gz",
|
| 25 |
+
"format": "tar.gz",
|
| 26 |
+
"hash": "sha256",
|
| 27 |
+
"path": "codex-zsh/bin/zsh"
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
.github/workflows/cargo-deny.yml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: cargo-deny
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_call:
|
| 5 |
+
|
| 6 |
+
jobs:
|
| 7 |
+
cargo-deny:
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
defaults:
|
| 10 |
+
run:
|
| 11 |
+
working-directory: ./codex-rs
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout
|
| 14 |
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
| 15 |
+
with:
|
| 16 |
+
persist-credentials: false
|
| 17 |
+
|
| 18 |
+
- uses: ./.github/actions/setup-ci
|
| 19 |
+
|
| 20 |
+
- name: Install Rust toolchain
|
| 21 |
+
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
| 22 |
+
|
| 23 |
+
- name: Run cargo-deny
|
| 24 |
+
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
|
| 25 |
+
with:
|
| 26 |
+
rust-version: 1.95.0
|
| 27 |
+
manifest-path: ./codex-rs/Cargo.toml
|
| 28 |
+
|
| 29 |
+
- name: Check for a clean worktree
|
| 30 |
+
if: always() && !cancelled()
|
| 31 |
+
uses: ./.github/actions/check-clean-worktree
|
.github/workflows/python-runtime-release.yml
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: python-runtime-release
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
inputs:
|
| 6 |
+
runtime_version:
|
| 7 |
+
description: "Runtime version to publish before updating the SDK pin, for example 0.136.0, 0.136.0a2, or 0.136.0a2.post1."
|
| 8 |
+
required: true
|
| 9 |
+
type: string
|
| 10 |
+
|
| 11 |
+
concurrency:
|
| 12 |
+
group: python-runtime-release-${{ inputs.runtime_version }}
|
| 13 |
+
cancel-in-progress: false
|
| 14 |
+
|
| 15 |
+
jobs:
|
| 16 |
+
prepare-python-runtime:
|
| 17 |
+
name: prepare-python-runtime
|
| 18 |
+
permissions:
|
| 19 |
+
contents: read
|
| 20 |
+
uses: ./.github/workflows/python-runtime-build.yml
|
| 21 |
+
with:
|
| 22 |
+
runtime_version: ${{ inputs.runtime_version }}
|
| 23 |
+
|
| 24 |
+
# PyPI must trust this top-level workflow for manual runtime publication.
|
| 25 |
+
publish-python-runtime:
|
| 26 |
+
if: github.repository == 'openai/codex'
|
| 27 |
+
name: publish-python-runtime
|
| 28 |
+
needs: prepare-python-runtime
|
| 29 |
+
runs-on: ubuntu-latest
|
| 30 |
+
environment: pypi
|
| 31 |
+
permissions:
|
| 32 |
+
contents: read
|
| 33 |
+
id-token: write # Required for PyPI trusted publishing.
|
| 34 |
+
|
| 35 |
+
steps:
|
| 36 |
+
- name: Checkout repository
|
| 37 |
+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
| 38 |
+
with:
|
| 39 |
+
persist-credentials: false
|
| 40 |
+
|
| 41 |
+
- name: Download Python runtime wheels
|
| 42 |
+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
| 43 |
+
with:
|
| 44 |
+
name: python-runtime-wheels
|
| 45 |
+
path: dist/python-runtime
|
| 46 |
+
|
| 47 |
+
- name: Publish Python runtime wheels to PyPI
|
| 48 |
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
| 49 |
+
with:
|
| 50 |
+
packages-dir: dist/python-runtime
|
| 51 |
+
skip-existing: true
|
| 52 |
+
|
| 53 |
+
- name: Install uv
|
| 54 |
+
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
| 55 |
+
with:
|
| 56 |
+
version: "0.11.3"
|
| 57 |
+
|
| 58 |
+
- name: Verify Python runtime wheels are available on PyPI
|
| 59 |
+
env:
|
| 60 |
+
PYTHON_RUNTIME_VERSION: ${{ needs.prepare-python-runtime.outputs.python_version }}
|
| 61 |
+
run: |
|
| 62 |
+
uv run --no-project --with packaging==26.2 python .github/scripts/verify_pypi_release.py \
|
| 63 |
+
openai-codex-cli-bin "$PYTHON_RUNTIME_VERSION"
|
.vscode/extensions.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"recommendations": [
|
| 3 |
+
"BazelBuild.vscode-bazel",
|
| 4 |
+
"rust-lang.rust-analyzer",
|
| 5 |
+
"charliermarsh.ruff",
|
| 6 |
+
"tamasfe.even-better-toml",
|
| 7 |
+
"vadimcn.vscode-lldb",
|
| 8 |
+
|
| 9 |
+
// Useful if touching files in .github/workflows, though most
|
| 10 |
+
// contributors will not be doing that?
|
| 11 |
+
// "github.vscode-github-actions",
|
| 12 |
+
]
|
| 13 |
+
}
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "0.2.0",
|
| 3 |
+
"configurations": [
|
| 4 |
+
{
|
| 5 |
+
"type": "lldb",
|
| 6 |
+
"request": "launch",
|
| 7 |
+
"name": "Cargo launch",
|
| 8 |
+
"cargo": {
|
| 9 |
+
"cwd": "${workspaceFolder}/codex-rs",
|
| 10 |
+
"args": ["build", "--bin=codex-tui"]
|
| 11 |
+
},
|
| 12 |
+
"args": []
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"type": "lldb",
|
| 16 |
+
"request": "attach",
|
| 17 |
+
"name": "Attach to running codex CLI",
|
| 18 |
+
"pid": "${command:pickProcess}",
|
| 19 |
+
"sourceLanguages": ["rust"]
|
| 20 |
+
}
|
| 21 |
+
]
|
| 22 |
+
}
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"rust-analyzer.checkOnSave": true,
|
| 3 |
+
"rust-analyzer.check.command": "clippy",
|
| 4 |
+
"rust-analyzer.check.extraArgs": ["--tests"],
|
| 5 |
+
"rust-analyzer.rustfmt.extraArgs": ["--config", "imports_granularity=Item"],
|
| 6 |
+
"rust-analyzer.cargo.targetDir": "${workspaceFolder}/codex-rs/target/rust-analyzer",
|
| 7 |
+
"[rust]": {
|
| 8 |
+
"editor.defaultFormatter": "rust-lang.rust-analyzer",
|
| 9 |
+
"editor.formatOnSave": true,
|
| 10 |
+
},
|
| 11 |
+
"[toml]": {
|
| 12 |
+
"editor.defaultFormatter": "tamasfe.even-better-toml",
|
| 13 |
+
"editor.formatOnSave": true,
|
| 14 |
+
},
|
| 15 |
+
"[python]": {
|
| 16 |
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
| 17 |
+
"editor.formatOnSave": true,
|
| 18 |
+
"editor.codeActionsOnSave": {
|
| 19 |
+
"source.fixAll.ruff": "explicit",
|
| 20 |
+
"source.organizeImports.ruff": "explicit",
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
// Array order for options in ~/.codex/config.toml such as `notify` and the
|
| 24 |
+
// `args` for an MCP server is significant, so we disable reordering.
|
| 25 |
+
"evenBetterToml.formatter.reorderArrays": false,
|
| 26 |
+
"evenBetterToml.formatter.reorderKeys": true,
|
| 27 |
+
}
|
bazel/build-info/BUILD.bazel
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
load("@bazel_lib//lib:expand_template.bzl", "expand_template")
|
| 2 |
+
|
| 3 |
+
# Isolate workspace status in a cheap action whose output changes only when
|
| 4 |
+
# the embedded commit changes. Rust compiler actions consume only this file.
|
| 5 |
+
expand_template(
|
| 6 |
+
name = "build-commit-env",
|
| 7 |
+
out = "build-commit.env",
|
| 8 |
+
stamp = 1,
|
| 9 |
+
stamp_substitutions = {"{BUILD_COMMIT}": "{{STABLE_GIT_COMMIT}}"},
|
| 10 |
+
template = ["STABLE_GIT_COMMIT={BUILD_COMMIT}"],
|
| 11 |
+
visibility = ["//codex-rs:__subpackages__"],
|
| 12 |
+
)
|
bazel/extensions/BUILD.bazel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package(default_visibility = ["//visibility:public"])
|
| 2 |
+
|
| 3 |
+
exports_files(["codex_release_archive.bzl"])
|
bazel/extensions/codex_release_archive.bzl
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bazel module extension for pinned Codex release archives."""
|
| 2 |
+
|
| 3 |
+
_CODEX_RELEASE_BUILD_FILE = """\
|
| 4 |
+
package(default_visibility = ["//visibility:public"])
|
| 5 |
+
|
| 6 |
+
filegroup(
|
| 7 |
+
name = "codex",
|
| 8 |
+
srcs = [{entrypoint}],
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
filegroup(
|
| 12 |
+
name = "package",
|
| 13 |
+
srcs = glob([
|
| 14 |
+
"codex-package.json",
|
| 15 |
+
{binaries},
|
| 16 |
+
{resources},
|
| 17 |
+
{path},
|
| 18 |
+
]),
|
| 19 |
+
)
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def _codex_release_repository_impl(repository_ctx):
|
| 23 |
+
asset = "codex-package-x86_64-unknown-linux-musl.tar.gz"
|
| 24 |
+
version = repository_ctx.attr.version
|
| 25 |
+
repository_ctx.download_and_extract(
|
| 26 |
+
url = [
|
| 27 |
+
"https://releases.openai.com/codex/releases/{}/{}".format(version, asset),
|
| 28 |
+
"https://github.com/openai/codex/releases/download/rust-v{}/{}".format(version, asset),
|
| 29 |
+
],
|
| 30 |
+
sha256 = repository_ctx.attr.sha256,
|
| 31 |
+
)
|
| 32 |
+
manifest = json.decode(repository_ctx.read("codex-package.json"))
|
| 33 |
+
entrypoint = manifest["entrypoint"]
|
| 34 |
+
binaries = entrypoint.rpartition("/")[0] + "/**"
|
| 35 |
+
repository_ctx.file(
|
| 36 |
+
"BUILD.bazel",
|
| 37 |
+
_CODEX_RELEASE_BUILD_FILE.format(
|
| 38 |
+
binaries = json.encode(binaries),
|
| 39 |
+
entrypoint = json.encode(entrypoint),
|
| 40 |
+
path = json.encode(manifest["pathDir"] + "/**"),
|
| 41 |
+
resources = json.encode(manifest["resourcesDir"] + "/**"),
|
| 42 |
+
),
|
| 43 |
+
executable = False,
|
| 44 |
+
)
|
| 45 |
+
return repository_ctx.repo_metadata(reproducible = True)
|
| 46 |
+
|
| 47 |
+
_codex_release_repository = repository_rule(
|
| 48 |
+
implementation = _codex_release_repository_impl,
|
| 49 |
+
attrs = {
|
| 50 |
+
"sha256": attr.string(mandatory = True),
|
| 51 |
+
"version": attr.string(mandatory = True),
|
| 52 |
+
},
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
_RELEASE = tag_class(
|
| 56 |
+
attrs = {
|
| 57 |
+
"sha256": attr.string(mandatory = True),
|
| 58 |
+
"version": attr.string(mandatory = True),
|
| 59 |
+
},
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
def _codex_release_archive_impl(module_ctx):
|
| 63 |
+
for module in module_ctx.modules:
|
| 64 |
+
for release in module.tags.release:
|
| 65 |
+
_codex_release_repository(
|
| 66 |
+
name = "codex_release_{}_linux_x86_64".format(release.version),
|
| 67 |
+
sha256 = release.sha256,
|
| 68 |
+
version = release.version,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return module_ctx.extension_metadata(reproducible = True)
|
| 72 |
+
|
| 73 |
+
codex_release_archive = module_extension(
|
| 74 |
+
implementation = _codex_release_archive_impl,
|
| 75 |
+
tag_classes = {"release": _RELEASE},
|
| 76 |
+
)
|
bazel/modules/BUILD.bazel
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
exports_files(["wine.MODULE.bazel"])
|
bazel/modules/wine.MODULE.bazel
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
| 2 |
+
|
| 3 |
+
# Pin a new-WoW64 build so tests need neither system Wine nor 32-bit host
|
| 4 |
+
# libraries.
|
| 5 |
+
http_archive(
|
| 6 |
+
name = "wine_linux_x86_64",
|
| 7 |
+
build_file = "//third_party/wine:BUILD.bazel",
|
| 8 |
+
sha256 = "39574efa1132c3ca0d5c77dd2eddbe4a49cca0d6cc2c290ff4924493a1c40314",
|
| 9 |
+
strip_prefix = "wine-11.0-amd64-wow64",
|
| 10 |
+
urls = [
|
| 11 |
+
"https://github.com/Kron4ek/Wine-Builds/releases/download/11.0/wine-11.0-amd64-wow64.tar.xz",
|
| 12 |
+
],
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Pin the self-contained Windows distribution so Wine tests need neither a
|
| 16 |
+
# system PowerShell installation nor a separate .NET runtime. This intentionally
|
| 17 |
+
# stays on 7.2.24 for the test fixture: 7.4.16 and 7.6.2 currently fail during
|
| 18 |
+
# CLR startup under the pinned Wine 11 runtime, while 7.2.24 runs successfully.
|
| 19 |
+
http_archive(
|
| 20 |
+
name = "powershell_windows_x86_64",
|
| 21 |
+
build_file = "//third_party/powershell:BUILD.bazel",
|
| 22 |
+
sha256 = "a1ccb6d8ad52f917470a136c3752af4465f261bcbe570cf44f52aa69ae6e867e",
|
| 23 |
+
urls = [
|
| 24 |
+
"https://github.com/PowerShell/PowerShell/releases/download/v7.2.24/PowerShell-7.2.24-win-x64.zip",
|
| 25 |
+
],
|
| 26 |
+
)
|
bazel/platforms/BUILD.bazel
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Release platform rules live in this package.
|
bazel/platforms/release_binaries.bzl
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rules for building release binaries across supported target platforms."""
|
| 2 |
+
|
| 3 |
+
load("@rules_platform//platform_data:defs.bzl", "platform_data")
|
| 4 |
+
|
| 5 |
+
_PLATFORM_TRIPLES = {
|
| 6 |
+
"linux_arm64_musl": "aarch64-unknown-linux-musl",
|
| 7 |
+
"linux_amd64_musl": "x86_64-unknown-linux-musl",
|
| 8 |
+
"macos_amd64": "x86_64-apple-darwin",
|
| 9 |
+
"macos_arm64": "aarch64-apple-darwin",
|
| 10 |
+
"windows_amd64": "x86_64-pc-windows-msvc",
|
| 11 |
+
"windows_arm64": "aarch64-pc-windows-msvc",
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
PLATFORMS = _PLATFORM_TRIPLES.keys()
|
| 15 |
+
|
| 16 |
+
def multiplatform_binaries(name, platforms = PLATFORMS):
|
| 17 |
+
"""Build a binary for a subset of the declared release platforms."""
|
| 18 |
+
for platform in platforms:
|
| 19 |
+
platform_data(
|
| 20 |
+
name = name + "_" + platform,
|
| 21 |
+
platform = "@rules_rs//rs/platforms:" + _PLATFORM_TRIPLES[platform],
|
| 22 |
+
target = name,
|
| 23 |
+
tags = ["manual"],
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
native.filegroup(
|
| 27 |
+
name = "release_binaries",
|
| 28 |
+
srcs = [name + "_" + platform for platform in platforms],
|
| 29 |
+
tags = ["manual"],
|
| 30 |
+
)
|
bazel/rules/BUILD.bazel
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package(default_visibility = ["//visibility:public"])
|
| 2 |
+
|
| 3 |
+
exports_files([
|
| 4 |
+
"e2e_benchmark.bzl",
|
| 5 |
+
"schema_bundle.bzl",
|
| 6 |
+
])
|
bazel/rules/e2e_benchmark.bzl
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
load("@crates//:defs.bzl", "all_crate_deps")
|
| 2 |
+
load("@rules_rust//rust:defs.bzl", "rust_binary")
|
| 3 |
+
load("//:defs.bzl", "workspace_root_test")
|
| 4 |
+
|
| 5 |
+
_WORKSPACE_ROOT_MARKER = "//codex-rs/utils/cargo-bin:repo_root.marker"
|
| 6 |
+
|
| 7 |
+
def codex_e2e_benchmark(name, binaries = [], data = [], deps = []):
|
| 8 |
+
"""Defines a Bazel-only Divan end-to-end benchmark.
|
| 9 |
+
|
| 10 |
+
The benchmark source lives at `e2e_benches/<name>.rs`, with hyphens in
|
| 11 |
+
`name` replaced by underscores. `binaries` are runtime executables made
|
| 12 |
+
available through the same `CARGO_BIN_EXE_*` bridge used by Rust tests.
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
name: Stem for the generated `<name>-bench` target and benchmark.
|
| 16 |
+
binaries: Runtime executable labels that the benchmark spawns.
|
| 17 |
+
data: Additional runtime files needed by the benchmark.
|
| 18 |
+
deps: Additional Rust dependencies beyond the crate's Cargo deps.
|
| 19 |
+
"""
|
| 20 |
+
benchmark_name = name.replace("-", "_")
|
| 21 |
+
source = "e2e_benches/{}.rs".format(benchmark_name)
|
| 22 |
+
binary_name = name + "-bench-bin"
|
| 23 |
+
runfile_env = {
|
| 24 |
+
binary: "CARGO_BIN_EXE_" + native.package_relative_label(binary).name
|
| 25 |
+
for binary in binaries
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
rust_binary(
|
| 29 |
+
name = binary_name,
|
| 30 |
+
testonly = True,
|
| 31 |
+
srcs = [source],
|
| 32 |
+
crate_name = benchmark_name + "_bench",
|
| 33 |
+
crate_root = source,
|
| 34 |
+
deps = all_crate_deps(
|
| 35 |
+
normal = True,
|
| 36 |
+
normal_dev = True,
|
| 37 |
+
) + [
|
| 38 |
+
"@crates//:divan",
|
| 39 |
+
] + deps,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
workspace_root_test(
|
| 43 |
+
name = name + "-bench",
|
| 44 |
+
args = [
|
| 45 |
+
"--bench",
|
| 46 |
+
benchmark_name,
|
| 47 |
+
],
|
| 48 |
+
data = data,
|
| 49 |
+
# Keep path resolution inside the wrapper so manifest-only runfiles
|
| 50 |
+
# work on every supported host platform.
|
| 51 |
+
runfile_env = runfile_env,
|
| 52 |
+
tags = ["manual"],
|
| 53 |
+
test_bin = ":" + binary_name,
|
| 54 |
+
visibility = ["//codex-rs:__pkg__"],
|
| 55 |
+
workspace_root_marker = _WORKSPACE_ROOT_MARKER,
|
| 56 |
+
)
|
bazel/rules/schema_bundle.bzl
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cache schema generation from the filtered public Rust test executable."""
|
| 2 |
+
|
| 3 |
+
def _schema_bundle_impl(ctx):
|
| 4 |
+
outputs = []
|
| 5 |
+
for mode in ["stable", "experimental"]:
|
| 6 |
+
schema = ctx.actions.declare_directory(ctx.label.name + "." + mode)
|
| 7 |
+
ctx.actions.run(
|
| 8 |
+
executable = ctx.executable.generator,
|
| 9 |
+
tools = [ctx.attr.generator[DefaultInfo].files_to_run],
|
| 10 |
+
arguments = [
|
| 11 |
+
"--exact",
|
| 12 |
+
"--ignored",
|
| 13 |
+
"schema_fixtures_tests::write_schema_fixtures_from_env",
|
| 14 |
+
],
|
| 15 |
+
env = {
|
| 16 |
+
"CODEX_APP_SERVER_SCHEMA_ROOT": schema.path,
|
| 17 |
+
"CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL": "1" if mode == "experimental" else "0",
|
| 18 |
+
"RUST_MIN_STACK": "8388608",
|
| 19 |
+
},
|
| 20 |
+
outputs = [schema],
|
| 21 |
+
mnemonic = "PublicSchema",
|
| 22 |
+
progress_message = "Generating %s public app-server schemas" % mode,
|
| 23 |
+
)
|
| 24 |
+
outputs.append(schema)
|
| 25 |
+
|
| 26 |
+
# Expose the pinned compressor alongside the cached directories so callers
|
| 27 |
+
# can normalize the JSON without a separate `bazel run` invocation.
|
| 28 |
+
zstd = ctx.actions.declare_file(ctx.label.name + ".zstd")
|
| 29 |
+
ctx.actions.symlink(output = zstd, target_file = ctx.executable.zstd, is_executable = True)
|
| 30 |
+
return [DefaultInfo(files = depset(outputs + [zstd]))]
|
| 31 |
+
|
| 32 |
+
schema_bundle = rule(
|
| 33 |
+
implementation = _schema_bundle_impl,
|
| 34 |
+
attrs = {
|
| 35 |
+
"generator": attr.label(mandatory = True, executable = True, cfg = "exec"),
|
| 36 |
+
"zstd": attr.label(default = "@zstd//:zstd_cli", executable = True, cfg = "exec"),
|
| 37 |
+
},
|
| 38 |
+
doc = "Generates both schema modes as cacheable actions with explicit tool dependencies.",
|
| 39 |
+
)
|
bazel/rules/testing/BUILD.bazel
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package(default_visibility = ["//visibility:public"])
|
| 2 |
+
|
| 3 |
+
exports_files([
|
| 4 |
+
"foreign_platform_binary.bzl",
|
| 5 |
+
])
|
bazel/rules/testing/compat/BUILD.bazel
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
load("@rules_rust//rust:defs.bzl", "rust_test")
|
| 2 |
+
|
| 3 |
+
package(default_visibility = ["//visibility:public"])
|
| 4 |
+
|
| 5 |
+
exports_files(["exec_server_compat_test.bzl"])
|
| 6 |
+
|
| 7 |
+
# exec_server_compat_test wraps this shared binary with current or released builds.
|
| 8 |
+
# Keep it manual so only wrapped compatibility tests enter the normal test graph.
|
| 9 |
+
rust_test(
|
| 10 |
+
name = "exec-server-compat-test-bin",
|
| 11 |
+
srcs = ["exec_server_compat_test.rs"],
|
| 12 |
+
crate_name = "exec_server_compat_test",
|
| 13 |
+
crate_root = "exec_server_compat_test.rs",
|
| 14 |
+
edition = "2024",
|
| 15 |
+
tags = ["manual"],
|
| 16 |
+
target_compatible_with = [
|
| 17 |
+
"@platforms//cpu:x86_64",
|
| 18 |
+
"@platforms//os:linux",
|
| 19 |
+
],
|
| 20 |
+
visibility = ["//codex-rs/exec-server:__pkg__"],
|
| 21 |
+
deps = [
|
| 22 |
+
"//codex-rs/exec-server",
|
| 23 |
+
"//codex-rs/exec-server/tests/support",
|
| 24 |
+
"@crates//:anyhow",
|
| 25 |
+
"@crates//:pretty_assertions",
|
| 26 |
+
"@crates//:serde_json",
|
| 27 |
+
"@crates//:tempfile",
|
| 28 |
+
"@crates//:tokio",
|
| 29 |
+
"@crates//:wiremock",
|
| 30 |
+
],
|
| 31 |
+
)
|
bazel/rules/testing/compat/exec_server_compat_test.bzl
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bazel coverage for app-server and exec-server release compatibility."""
|
| 2 |
+
|
| 3 |
+
load("//:defs.bzl", "workspace_root_test")
|
| 4 |
+
|
| 5 |
+
def exec_server_compat_test(
|
| 6 |
+
name,
|
| 7 |
+
comparison_binary = None,
|
| 8 |
+
current_binary = "//codex-rs/cli:codex",
|
| 9 |
+
release = None):
|
| 10 |
+
"""Tests both executor directions against another Codex build or release.
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
name: Name of the generated compatibility test target.
|
| 14 |
+
comparison_binary: Built Codex executable to compare with the current build.
|
| 15 |
+
current_binary: Built Codex executable representing the current version.
|
| 16 |
+
release: External release repository exposing `:codex` and `:package`.
|
| 17 |
+
"""
|
| 18 |
+
if (comparison_binary == None) == (release == None):
|
| 19 |
+
fail("exactly one of comparison_binary and release must be set")
|
| 20 |
+
|
| 21 |
+
comparison = comparison_binary if release == None else release + "//:codex"
|
| 22 |
+
data = [] if release == None else [release + "//:package"]
|
| 23 |
+
comparison_alias = name + "-comparison-binary"
|
| 24 |
+
native.alias(
|
| 25 |
+
name = comparison_alias,
|
| 26 |
+
actual = comparison,
|
| 27 |
+
tags = ["manual"],
|
| 28 |
+
visibility = ["//visibility:private"],
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
workspace_root_test(
|
| 32 |
+
name = name,
|
| 33 |
+
args = ["--test-threads=1"],
|
| 34 |
+
data = data,
|
| 35 |
+
runfile_env = {
|
| 36 |
+
"//codex-rs/bwrap:bwrap": "CARGO_BIN_EXE_bwrap",
|
| 37 |
+
current_binary: "CODEX_TEST_CURRENT_CODEX",
|
| 38 |
+
":" + comparison_alias: "CODEX_TEST_RELEASED_CODEX",
|
| 39 |
+
},
|
| 40 |
+
tags = ["no-sandbox"],
|
| 41 |
+
target_compatible_with = [
|
| 42 |
+
"@platforms//cpu:x86_64",
|
| 43 |
+
"@platforms//os:linux",
|
| 44 |
+
],
|
| 45 |
+
test_bin = "//bazel/rules/testing/compat:exec-server-compat-test-bin",
|
| 46 |
+
workspace_root_marker = "//codex-rs/utils/cargo-bin:repo_root.marker",
|
| 47 |
+
)
|
bazel/rules/testing/compat/exec_server_compat_test.rs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::path::Path;
|
| 2 |
+
use std::path::PathBuf;
|
| 3 |
+
use std::process::Stdio;
|
| 4 |
+
use std::sync::Arc;
|
| 5 |
+
use std::sync::Mutex;
|
| 6 |
+
|
| 7 |
+
use anyhow::Context;
|
| 8 |
+
use anyhow::Result;
|
| 9 |
+
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
| 10 |
+
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR;
|
| 11 |
+
use codex_exec_server::CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR;
|
| 12 |
+
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
|
| 13 |
+
use codex_exec_server_test_support::relay::TEST_TIMEOUT as VERSION_SKEW_TIMEOUT;
|
| 14 |
+
use codex_exec_server_test_support::relay::accept_websocket;
|
| 15 |
+
use codex_exec_server_test_support::relay::assert_relay_data_is_encrypted;
|
| 16 |
+
use codex_exec_server_test_support::relay::proxy_relay_frames;
|
| 17 |
+
use codex_exec_server_test_support::relay::registered_executor_public_key;
|
| 18 |
+
use pretty_assertions::assert_eq;
|
| 19 |
+
use serde_json::Value;
|
| 20 |
+
use serde_json::json;
|
| 21 |
+
use tempfile::TempDir;
|
| 22 |
+
use tokio::io::AsyncBufReadExt;
|
| 23 |
+
use tokio::io::AsyncWriteExt;
|
| 24 |
+
use tokio::io::BufReader;
|
| 25 |
+
use tokio::io::Lines;
|
| 26 |
+
use tokio::net::TcpListener;
|
| 27 |
+
use tokio::process::ChildStdin;
|
| 28 |
+
use tokio::process::ChildStdout;
|
| 29 |
+
use tokio::process::Command;
|
| 30 |
+
use tokio::time::timeout;
|
| 31 |
+
use wiremock::Mock;
|
| 32 |
+
use wiremock::MockServer;
|
| 33 |
+
use wiremock::ResponseTemplate;
|
| 34 |
+
use wiremock::matchers::header;
|
| 35 |
+
use wiremock::matchers::method;
|
| 36 |
+
use wiremock::matchers::path;
|
| 37 |
+
|
| 38 |
+
const ENVIRONMENT_ID: &str = "env-noise-relay-test";
|
| 39 |
+
const EXECUTOR_REGISTRATION_ID: &str = "registration-1";
|
| 40 |
+
const HARNESS_KEY_AUTHORIZATION: &str = "harness-key-authorization";
|
| 41 |
+
const REGISTRY_TOKEN: &str = "registry-token";
|
| 42 |
+
const RELEASED_CODEX_ENV_VAR: &str = "CODEX_TEST_RELEASED_CODEX";
|
| 43 |
+
const CURRENT_CODEX_ENV_VAR: &str = "CODEX_TEST_CURRENT_CODEX";
|
| 44 |
+
const EXECUTOR_MARKER_ENV_VAR: &str = "CODEX_EXECUTOR_VERSION_SKEW_MARKER";
|
| 45 |
+
const EXPECTED_OUTPUT: &str = "executor-version-skew-ok";
|
| 46 |
+
|
| 47 |
+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
| 48 |
+
async fn current_app_server_runs_commands_on_released_exec_server_over_noise() -> Result<()> {
|
| 49 |
+
let Some((current, released)) = version_skew_binaries()? else {
|
| 50 |
+
return Ok(());
|
| 51 |
+
};
|
| 52 |
+
assert_noise_version_skew(¤t, &released).await
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
| 56 |
+
async fn released_app_server_runs_commands_on_current_exec_server_over_noise() -> Result<()> {
|
| 57 |
+
let Some((current, released)) = version_skew_binaries()? else {
|
| 58 |
+
return Ok(());
|
| 59 |
+
};
|
| 60 |
+
assert_noise_version_skew(&released, ¤t).await
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
fn version_skew_binaries() -> Result<Option<(PathBuf, PathBuf)>> {
|
| 64 |
+
let Some(released) = std::env::var_os(RELEASED_CODEX_ENV_VAR) else {
|
| 65 |
+
return Ok(None);
|
| 66 |
+
};
|
| 67 |
+
let current = std::env::var_os(CURRENT_CODEX_ENV_VAR)
|
| 68 |
+
.with_context(|| format!("{CURRENT_CODEX_ENV_VAR} must name the current Codex binary"))?;
|
| 69 |
+
let current = PathBuf::from(current);
|
| 70 |
+
let released = PathBuf::from(released);
|
| 71 |
+
anyhow::ensure!(
|
| 72 |
+
current.is_file(),
|
| 73 |
+
"current Codex does not exist: {}",
|
| 74 |
+
current.display()
|
| 75 |
+
);
|
| 76 |
+
anyhow::ensure!(
|
| 77 |
+
released.is_file(),
|
| 78 |
+
"released Codex does not exist: {}",
|
| 79 |
+
released.display()
|
| 80 |
+
);
|
| 81 |
+
Ok(Some((current, released)))
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
async fn assert_noise_version_skew(app_binary: &Path, executor_binary: &Path) -> Result<()> {
|
| 85 |
+
let codex_home = TempDir::new()?;
|
| 86 |
+
let model = mock_model(codex_home.path()).await?;
|
| 87 |
+
let model_url = model.uri();
|
| 88 |
+
std::fs::write(
|
| 89 |
+
codex_home.path().join("config.toml"),
|
| 90 |
+
format!(
|
| 91 |
+
r#"
|
| 92 |
+
model = "mock-model"
|
| 93 |
+
approval_policy = "never"
|
| 94 |
+
sandbox_mode = "danger-full-access"
|
| 95 |
+
model_provider = "mock_provider"
|
| 96 |
+
|
| 97 |
+
[model_providers.mock_provider]
|
| 98 |
+
name = "Mock provider"
|
| 99 |
+
base_url = "{model_url}/v1"
|
| 100 |
+
wire_api = "responses"
|
| 101 |
+
request_max_retries = 0
|
| 102 |
+
stream_max_retries = 0
|
| 103 |
+
"#
|
| 104 |
+
),
|
| 105 |
+
)?;
|
| 106 |
+
|
| 107 |
+
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
| 108 |
+
let rendezvous_url = format!("ws://{}", listener.local_addr()?);
|
| 109 |
+
let registry = MockServer::start().await;
|
| 110 |
+
let registry_url = registry.uri();
|
| 111 |
+
Mock::given(method("POST"))
|
| 112 |
+
.and(path(format!(
|
| 113 |
+
"/cloud/environment/{ENVIRONMENT_ID}/register"
|
| 114 |
+
)))
|
| 115 |
+
.and(header("authorization", format!("Bearer {REGISTRY_TOKEN}")))
|
| 116 |
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
| 117 |
+
"environment_id": ENVIRONMENT_ID,
|
| 118 |
+
"url": format!("{rendezvous_url}/relay?role=environment"),
|
| 119 |
+
"security_profile": "noise_hybrid_ik_v1",
|
| 120 |
+
"executor_registration_id": EXECUTOR_REGISTRATION_ID,
|
| 121 |
+
})))
|
| 122 |
+
.expect(1)
|
| 123 |
+
.mount(®istry)
|
| 124 |
+
.await;
|
| 125 |
+
Mock::given(method("POST"))
|
| 126 |
+
.and(path(format!(
|
| 127 |
+
"/cloud/environment/{ENVIRONMENT_ID}/validate"
|
| 128 |
+
)))
|
| 129 |
+
.and(header("authorization", format!("Bearer {REGISTRY_TOKEN}")))
|
| 130 |
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"valid": true})))
|
| 131 |
+
.expect(1)
|
| 132 |
+
.mount(®istry)
|
| 133 |
+
.await;
|
| 134 |
+
|
| 135 |
+
let mut executor = Command::new(executor_binary)
|
| 136 |
+
.args([
|
| 137 |
+
"exec-server",
|
| 138 |
+
"--remote",
|
| 139 |
+
registry_url.as_str(),
|
| 140 |
+
"--environment-id",
|
| 141 |
+
ENVIRONMENT_ID,
|
| 142 |
+
])
|
| 143 |
+
.current_dir(codex_home.path())
|
| 144 |
+
.env("CODEX_HOME", codex_home.path())
|
| 145 |
+
.env("CODEX_API_KEY", REGISTRY_TOKEN)
|
| 146 |
+
.env(EXECUTOR_MARKER_ENV_VAR, EXPECTED_OUTPUT)
|
| 147 |
+
.stdin(Stdio::null())
|
| 148 |
+
.stdout(Stdio::null())
|
| 149 |
+
.stderr(Stdio::inherit())
|
| 150 |
+
.kill_on_drop(true)
|
| 151 |
+
.spawn()
|
| 152 |
+
.with_context(|| format!("start remote executor from {}", executor_binary.display()))?;
|
| 153 |
+
|
| 154 |
+
let environment_websocket = accept_websocket(&listener, "environment").await?;
|
| 155 |
+
let executor_public_key = registered_executor_public_key(®istry).await?;
|
| 156 |
+
Mock::given(method("POST"))
|
| 157 |
+
.and(path(format!("/cloud/environment/{ENVIRONMENT_ID}/connect")))
|
| 158 |
+
.and(header("authorization", format!("Bearer {REGISTRY_TOKEN}")))
|
| 159 |
+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
| 160 |
+
"environment_id": ENVIRONMENT_ID,
|
| 161 |
+
"url": format!("{rendezvous_url}/relay?role=harness"),
|
| 162 |
+
"security_profile": "noise_hybrid_ik_v1",
|
| 163 |
+
"executor_registration_id": EXECUTOR_REGISTRATION_ID,
|
| 164 |
+
"executor_public_key": executor_public_key,
|
| 165 |
+
"harness_key_authorization": HARNESS_KEY_AUTHORIZATION,
|
| 166 |
+
})))
|
| 167 |
+
.expect(1)
|
| 168 |
+
.mount(®istry)
|
| 169 |
+
.await;
|
| 170 |
+
|
| 171 |
+
let captured_frames = Arc::new(Mutex::new(Vec::new()));
|
| 172 |
+
let captured_relay_frames = Arc::clone(&captured_frames);
|
| 173 |
+
let relay = tokio::spawn(async move {
|
| 174 |
+
let harness_websocket = accept_websocket(&listener, "harness").await?;
|
| 175 |
+
proxy_relay_frames(
|
| 176 |
+
environment_websocket,
|
| 177 |
+
harness_websocket,
|
| 178 |
+
captured_relay_frames,
|
| 179 |
+
)
|
| 180 |
+
.await
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
let mut app_server = Command::new(app_binary)
|
| 184 |
+
.arg("app-server")
|
| 185 |
+
.current_dir(codex_home.path())
|
| 186 |
+
.env("CODEX_HOME", codex_home.path())
|
| 187 |
+
.env("CODEX_API_KEY", REGISTRY_TOKEN)
|
| 188 |
+
.env(
|
| 189 |
+
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
|
| 190 |
+
codex_home.path().join("managed_config.toml"),
|
| 191 |
+
)
|
| 192 |
+
.env(CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR, ®istry_url)
|
| 193 |
+
.env(
|
| 194 |
+
CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR,
|
| 195 |
+
ENVIRONMENT_ID,
|
| 196 |
+
)
|
| 197 |
+
.env(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR, REGISTRY_TOKEN)
|
| 198 |
+
.env_remove(CODEX_EXEC_SERVER_URL_ENV_VAR)
|
| 199 |
+
.env_remove(EXECUTOR_MARKER_ENV_VAR)
|
| 200 |
+
.stdin(Stdio::piped())
|
| 201 |
+
.stdout(Stdio::piped())
|
| 202 |
+
.stderr(Stdio::inherit())
|
| 203 |
+
.kill_on_drop(true)
|
| 204 |
+
.spawn()
|
| 205 |
+
.with_context(|| format!("start app-server from {}", app_binary.display()))?;
|
| 206 |
+
let mut stdin = app_server.stdin.take().context("app-server stdin")?;
|
| 207 |
+
let stdout = app_server.stdout.take().context("app-server stdout")?;
|
| 208 |
+
let mut stdout = BufReader::new(stdout).lines();
|
| 209 |
+
let mut notifications = Vec::new();
|
| 210 |
+
|
| 211 |
+
app_server_request(
|
| 212 |
+
&mut stdin,
|
| 213 |
+
&mut stdout,
|
| 214 |
+
&mut notifications,
|
| 215 |
+
/*id*/ 1,
|
| 216 |
+
"initialize",
|
| 217 |
+
json!({
|
| 218 |
+
"clientInfo": {"name": "noise-version-skew", "version": "0.1.0"},
|
| 219 |
+
"capabilities": {"experimentalApi": true},
|
| 220 |
+
}),
|
| 221 |
+
)
|
| 222 |
+
.await?;
|
| 223 |
+
stdin.write_all(b"{\"method\":\"initialized\"}\n").await?;
|
| 224 |
+
|
| 225 |
+
let thread = app_server_request(
|
| 226 |
+
&mut stdin,
|
| 227 |
+
&mut stdout,
|
| 228 |
+
&mut notifications,
|
| 229 |
+
/*id*/ 2,
|
| 230 |
+
"thread/start",
|
| 231 |
+
json!({"cwd": codex_home.path()}),
|
| 232 |
+
)
|
| 233 |
+
.await?;
|
| 234 |
+
let thread_id = thread["thread"]["id"]
|
| 235 |
+
.as_str()
|
| 236 |
+
.context("thread/start should return a thread id")?;
|
| 237 |
+
app_server_request(
|
| 238 |
+
&mut stdin,
|
| 239 |
+
&mut stdout,
|
| 240 |
+
&mut notifications,
|
| 241 |
+
/*id*/ 3,
|
| 242 |
+
"turn/start",
|
| 243 |
+
json!({
|
| 244 |
+
"threadId": thread_id,
|
| 245 |
+
"input": [{
|
| 246 |
+
"type": "text",
|
| 247 |
+
"text": "run the Noise compatibility command",
|
| 248 |
+
"textElements": [],
|
| 249 |
+
}],
|
| 250 |
+
}),
|
| 251 |
+
)
|
| 252 |
+
.await?;
|
| 253 |
+
|
| 254 |
+
while !notifications
|
| 255 |
+
.iter()
|
| 256 |
+
.any(|notification: &Value| notification["method"] == "turn/completed")
|
| 257 |
+
{
|
| 258 |
+
let line = timeout(VERSION_SKEW_TIMEOUT, stdout.next_line())
|
| 259 |
+
.await
|
| 260 |
+
.context("waiting for turn/completed")??
|
| 261 |
+
.context("app-server exited before turn/completed")?;
|
| 262 |
+
notifications.push(serde_json::from_str(&line)?);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
assert_eq!(
|
| 266 |
+
std::fs::read_to_string(codex_home.path().join("version-skew-output.txt"))?,
|
| 267 |
+
EXPECTED_OUTPUT
|
| 268 |
+
);
|
| 269 |
+
assert_relay_data_is_encrypted(&captured_frames)?;
|
| 270 |
+
registry.verify().await;
|
| 271 |
+
model.verify().await;
|
| 272 |
+
|
| 273 |
+
let _ = app_server.start_kill();
|
| 274 |
+
let _ = executor.start_kill();
|
| 275 |
+
relay.abort();
|
| 276 |
+
Ok(())
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
async fn app_server_request(
|
| 280 |
+
stdin: &mut ChildStdin,
|
| 281 |
+
stdout: &mut Lines<BufReader<ChildStdout>>,
|
| 282 |
+
notifications: &mut Vec<Value>,
|
| 283 |
+
id: u64,
|
| 284 |
+
method: &str,
|
| 285 |
+
params: Value,
|
| 286 |
+
) -> Result<Value> {
|
| 287 |
+
let request = json!({"id": id, "method": method, "params": params});
|
| 288 |
+
stdin.write_all(request.to_string().as_bytes()).await?;
|
| 289 |
+
stdin.write_all(b"\n").await?;
|
| 290 |
+
|
| 291 |
+
loop {
|
| 292 |
+
let line = timeout(VERSION_SKEW_TIMEOUT, stdout.next_line())
|
| 293 |
+
.await
|
| 294 |
+
.with_context(|| format!("waiting for {method} response"))??
|
| 295 |
+
.with_context(|| format!("app-server exited before {method} response"))?;
|
| 296 |
+
let response: Value = serde_json::from_str(&line)?;
|
| 297 |
+
if response["id"] == id {
|
| 298 |
+
anyhow::ensure!(
|
| 299 |
+
response.get("error").is_none(),
|
| 300 |
+
"{method} failed: {}",
|
| 301 |
+
response["error"]
|
| 302 |
+
);
|
| 303 |
+
return response.get("result").cloned().context("missing result");
|
| 304 |
+
}
|
| 305 |
+
notifications.push(response);
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
async fn mock_model(codex_home: &Path) -> Result<MockServer> {
|
| 310 |
+
let server = MockServer::start().await;
|
| 311 |
+
let arguments = serde_json::to_string(&json!({
|
| 312 |
+
"cmd": format!("printf '%s' \"${EXECUTOR_MARKER_ENV_VAR}\" > version-skew-output.txt"),
|
| 313 |
+
"workdir": codex_home,
|
| 314 |
+
"yield_time_ms": 5_000,
|
| 315 |
+
}))?;
|
| 316 |
+
let completed = |id| {
|
| 317 |
+
json!({
|
| 318 |
+
"type": "response.completed",
|
| 319 |
+
"response": {
|
| 320 |
+
"id": id,
|
| 321 |
+
"usage": {
|
| 322 |
+
"input_tokens": 0,
|
| 323 |
+
"input_tokens_details": null,
|
| 324 |
+
"output_tokens": 0,
|
| 325 |
+
"output_tokens_details": null,
|
| 326 |
+
"total_tokens": 0,
|
| 327 |
+
},
|
| 328 |
+
},
|
| 329 |
+
})
|
| 330 |
+
};
|
| 331 |
+
let responses = vec![
|
| 332 |
+
event_stream(vec![
|
| 333 |
+
json!({"type": "response.created", "response": {"id": "response-1"}}),
|
| 334 |
+
json!({
|
| 335 |
+
"type": "response.output_item.done",
|
| 336 |
+
"item": {
|
| 337 |
+
"type": "function_call",
|
| 338 |
+
"call_id": "noise-version-skew-command",
|
| 339 |
+
"name": "exec_command",
|
| 340 |
+
"arguments": arguments,
|
| 341 |
+
},
|
| 342 |
+
}),
|
| 343 |
+
completed("response-1"),
|
| 344 |
+
])?,
|
| 345 |
+
event_stream(vec![
|
| 346 |
+
json!({"type": "response.created", "response": {"id": "response-2"}}),
|
| 347 |
+
json!({
|
| 348 |
+
"type": "response.output_item.done",
|
| 349 |
+
"item": {
|
| 350 |
+
"type": "message",
|
| 351 |
+
"role": "assistant",
|
| 352 |
+
"id": "message-1",
|
| 353 |
+
"content": [{"type": "output_text", "text": "done"}],
|
| 354 |
+
},
|
| 355 |
+
}),
|
| 356 |
+
completed("response-2"),
|
| 357 |
+
])?,
|
| 358 |
+
];
|
| 359 |
+
for response in responses {
|
| 360 |
+
Mock::given(method("POST"))
|
| 361 |
+
.and(path("/v1/responses"))
|
| 362 |
+
.respond_with(ResponseTemplate::new(200).set_body_raw(response, "text/event-stream"))
|
| 363 |
+
.up_to_n_times(1)
|
| 364 |
+
.expect(1)
|
| 365 |
+
.mount(&server)
|
| 366 |
+
.await;
|
| 367 |
+
}
|
| 368 |
+
Ok(server)
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
fn event_stream(events: Vec<Value>) -> Result<String> {
|
| 372 |
+
events
|
| 373 |
+
.into_iter()
|
| 374 |
+
.map(|event| {
|
| 375 |
+
let event_type = event["type"].as_str().context("SSE event type")?;
|
| 376 |
+
Ok(format!("event: {event_type}\ndata: {event}\n\n"))
|
| 377 |
+
})
|
| 378 |
+
.collect()
|
| 379 |
+
}
|
bazel/rules/testing/foreign_platform_binary.bzl
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Makes a binary built for a foreign platform available as test data."""
|
| 2 |
+
|
| 3 |
+
_EXTRA_RUSTC_FLAGS = "@rules_rust//rust/settings:extra_rustc_flags"
|
| 4 |
+
|
| 5 |
+
def _foreign_platform_transition_impl(settings, attr):
|
| 6 |
+
# A transition cannot rewrite a dependency's rule attributes. Use the
|
| 7 |
+
# rules_rust build setting when every Rust target in the foreign
|
| 8 |
+
# configuration needs additional compiler or linker flags.
|
| 9 |
+
return {
|
| 10 |
+
"//command_line_option:platforms": [attr.platform],
|
| 11 |
+
_EXTRA_RUSTC_FLAGS: settings[_EXTRA_RUSTC_FLAGS] + attr.extra_rustc_flags,
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
_foreign_platform_transition = transition(
|
| 15 |
+
implementation = _foreign_platform_transition_impl,
|
| 16 |
+
inputs = [_EXTRA_RUSTC_FLAGS],
|
| 17 |
+
outputs = [
|
| 18 |
+
"//command_line_option:platforms",
|
| 19 |
+
_EXTRA_RUSTC_FLAGS,
|
| 20 |
+
],
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
def _foreign_platform_binary_impl(ctx):
|
| 24 |
+
if len(ctx.attr.binary) != 1:
|
| 25 |
+
fail("expected exactly one transitioned binary")
|
| 26 |
+
binary = ctx.attr.binary[0][DefaultInfo]
|
| 27 |
+
runfiles = ctx.runfiles(transitive_files = binary.files)
|
| 28 |
+
runfiles = runfiles.merge(binary.default_runfiles)
|
| 29 |
+
return [
|
| 30 |
+
DefaultInfo(
|
| 31 |
+
files = binary.files,
|
| 32 |
+
runfiles = runfiles,
|
| 33 |
+
),
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
foreign_platform_binary = rule(
|
| 37 |
+
implementation = _foreign_platform_binary_impl,
|
| 38 |
+
attrs = {
|
| 39 |
+
"binary": attr.label(
|
| 40 |
+
cfg = _foreign_platform_transition,
|
| 41 |
+
executable = True,
|
| 42 |
+
mandatory = True,
|
| 43 |
+
),
|
| 44 |
+
"extra_rustc_flags": attr.string_list(
|
| 45 |
+
doc = "Additional flags applied to every Rust target in the foreign configuration.",
|
| 46 |
+
),
|
| 47 |
+
"platform": attr.string(mandatory = True),
|
| 48 |
+
"_allowlist_function_transition": attr.label(
|
| 49 |
+
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
|
| 50 |
+
),
|
| 51 |
+
},
|
| 52 |
+
doc = "Builds `binary` for `platform` and exposes its files and runfiles.",
|
| 53 |
+
)
|
bazel/rules/testing/wine/BUILD.bazel
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library")
|
| 2 |
+
load(":wine.bzl", "wine_rust_test")
|
| 3 |
+
|
| 4 |
+
package(default_visibility = ["//visibility:public"])
|
| 5 |
+
|
| 6 |
+
exports_files([
|
| 7 |
+
"wine.bzl",
|
| 8 |
+
"wine_runtime.bzl",
|
| 9 |
+
])
|
| 10 |
+
|
| 11 |
+
rust_library(
|
| 12 |
+
name = "wine_test_support",
|
| 13 |
+
testonly = True,
|
| 14 |
+
srcs = [
|
| 15 |
+
"src/lib.rs",
|
| 16 |
+
"src/lib_tests.rs",
|
| 17 |
+
],
|
| 18 |
+
crate_name = "wine_test_support",
|
| 19 |
+
crate_root = "src/lib.rs",
|
| 20 |
+
edition = "2024",
|
| 21 |
+
target_compatible_with = ["@platforms//os:linux"],
|
| 22 |
+
deps = [
|
| 23 |
+
"//codex-rs/utils/cargo-bin",
|
| 24 |
+
"//codex-rs/utils/pty",
|
| 25 |
+
"@crates//:anyhow",
|
| 26 |
+
"@crates//:tempfile",
|
| 27 |
+
"@crates//:tokio",
|
| 28 |
+
],
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
rust_binary(
|
| 32 |
+
name = "windows-smoke",
|
| 33 |
+
testonly = True,
|
| 34 |
+
srcs = ["fixtures/windows_smoke.rs"],
|
| 35 |
+
crate_name = "wine_smoke",
|
| 36 |
+
crate_root = "fixtures/windows_smoke.rs",
|
| 37 |
+
edition = "2024",
|
| 38 |
+
tags = ["manual"],
|
| 39 |
+
target_compatible_with = ["@platforms//os:windows"],
|
| 40 |
+
visibility = ["//visibility:private"],
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
wine_rust_test(
|
| 44 |
+
name = "wine-test-support-unit-tests",
|
| 45 |
+
timeout = "short",
|
| 46 |
+
crate = ":wine_test_support",
|
| 47 |
+
windows_binaries = {
|
| 48 |
+
"wine-smoke": ":windows-smoke",
|
| 49 |
+
},
|
| 50 |
+
deps = [
|
| 51 |
+
"@crates//:futures",
|
| 52 |
+
"@crates//:pretty_assertions",
|
| 53 |
+
],
|
| 54 |
+
)
|
bazel/rules/testing/wine/fixtures/windows_smoke.rs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::io::Write;
|
| 2 |
+
|
| 3 |
+
fn main() {
|
| 4 |
+
println!("WINE_TEST_READY");
|
| 5 |
+
std::io::stdout().flush().expect("flush readiness marker");
|
| 6 |
+
|
| 7 |
+
if std::env::args().any(|arg| arg == "--fail") {
|
| 8 |
+
std::process::exit(9);
|
| 9 |
+
}
|
| 10 |
+
if std::env::args().any(|arg| arg == "--wait") {
|
| 11 |
+
loop {
|
| 12 |
+
std::thread::park();
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
}
|
bazel/rules/testing/wine/src/lib.rs
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#[cfg(not(target_os = "linux"))]
|
| 2 |
+
compile_error!("wine_test_support can only run on Linux");
|
| 3 |
+
|
| 4 |
+
use std::ffi::OsString;
|
| 5 |
+
use std::fs;
|
| 6 |
+
use std::future::Future;
|
| 7 |
+
use std::io::Write;
|
| 8 |
+
use std::path::Path;
|
| 9 |
+
use std::path::PathBuf;
|
| 10 |
+
use std::process::Command as StdCommand;
|
| 11 |
+
use std::process::Stdio;
|
| 12 |
+
use std::time::Duration;
|
| 13 |
+
|
| 14 |
+
use anyhow::Context;
|
| 15 |
+
use anyhow::Result;
|
| 16 |
+
use tempfile::TempDir;
|
| 17 |
+
use tokio::process::Child;
|
| 18 |
+
use tokio::process::ChildStdout;
|
| 19 |
+
use tokio::process::Command as TokioCommand;
|
| 20 |
+
|
| 21 |
+
/// Builds a command that runs a Windows executable in an isolated Wine prefix.
|
| 22 |
+
pub struct WineTestCommand {
|
| 23 |
+
executable: PathBuf,
|
| 24 |
+
args: Vec<OsString>,
|
| 25 |
+
env: Vec<(OsString, OsString)>,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/// Owns a Wine process and its isolated wineserver.
|
| 29 |
+
///
|
| 30 |
+
/// Call [`Self::scope`] or [`Self::shutdown`] on every successful path. A
|
| 31 |
+
/// normal unguarded drop panics, while a drop during unwinding performs
|
| 32 |
+
/// blocking cleanup without introducing a second panic.
|
| 33 |
+
pub struct WineTestProcess {
|
| 34 |
+
processes: Option<WineProcesses>,
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
struct WineProcesses {
|
| 38 |
+
child: Child,
|
| 39 |
+
cleanup_complete: bool,
|
| 40 |
+
prefix: TempDir,
|
| 41 |
+
runtime: WineRuntimePaths,
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
struct WineRuntimePaths {
|
| 45 |
+
dll_path: PathBuf,
|
| 46 |
+
powershell_runtime: PathBuf,
|
| 47 |
+
wine: PathBuf,
|
| 48 |
+
wineserver: PathBuf,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
impl WineTestCommand {
|
| 52 |
+
/// Creates a Wine command for `executable`.
|
| 53 |
+
pub fn new(executable: impl Into<PathBuf>) -> Self {
|
| 54 |
+
Self {
|
| 55 |
+
executable: executable.into(),
|
| 56 |
+
args: Vec::new(),
|
| 57 |
+
env: Vec::new(),
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/// Adds an argument passed to the Windows executable.
|
| 62 |
+
#[must_use]
|
| 63 |
+
pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
|
| 64 |
+
self.args.push(arg.into());
|
| 65 |
+
self
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/// Adds or overrides an environment variable for the Wine process.
|
| 69 |
+
#[must_use]
|
| 70 |
+
pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
|
| 71 |
+
self.env.push((key.into(), value.into()));
|
| 72 |
+
self
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/// Starts the Windows executable with a fresh `WINEPREFIX`.
|
| 76 |
+
pub fn spawn(self) -> Result<WineTestProcess> {
|
| 77 |
+
let runtime = WineRuntimePaths::from_runfiles()?;
|
| 78 |
+
let prefix = TempDir::new().context("create isolated Wine prefix")?;
|
| 79 |
+
install_powershell_runtime(prefix.path(), &runtime.powershell_runtime)?;
|
| 80 |
+
let mut command = StdCommand::new(&runtime.wine);
|
| 81 |
+
configure_wine_environment(&mut command, &runtime, prefix.path());
|
| 82 |
+
command
|
| 83 |
+
.arg(self.executable)
|
| 84 |
+
.args(self.args)
|
| 85 |
+
.envs(self.env)
|
| 86 |
+
.stdin(Stdio::null())
|
| 87 |
+
.stdout(Stdio::piped())
|
| 88 |
+
.stderr(Stdio::inherit());
|
| 89 |
+
|
| 90 |
+
let mut command = TokioCommand::from(command);
|
| 91 |
+
command.kill_on_drop(true);
|
| 92 |
+
let child = command
|
| 93 |
+
.spawn()
|
| 94 |
+
.context("start Windows process under Wine")?;
|
| 95 |
+
|
| 96 |
+
Ok(WineTestProcess {
|
| 97 |
+
processes: Some(WineProcesses {
|
| 98 |
+
child,
|
| 99 |
+
cleanup_complete: false,
|
| 100 |
+
prefix,
|
| 101 |
+
runtime,
|
| 102 |
+
}),
|
| 103 |
+
})
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
impl WineTestProcess {
|
| 108 |
+
/// Returns the host path to this process's isolated Wine prefix.
|
| 109 |
+
pub fn prefix_path(&self) -> &Path {
|
| 110 |
+
let Some(processes) = self.processes.as_ref() else {
|
| 111 |
+
panic!("Wine process guard is missing");
|
| 112 |
+
};
|
| 113 |
+
processes.prefix.path()
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Takes the piped standard output of the Wine process.
|
| 117 |
+
///
|
| 118 |
+
/// This may only be called once for a process created by
|
| 119 |
+
/// [`WineTestCommand::spawn`].
|
| 120 |
+
pub fn take_stdout(&mut self) -> ChildStdout {
|
| 121 |
+
let Some(processes) = self.processes.as_mut() else {
|
| 122 |
+
panic!("Wine process guard is missing");
|
| 123 |
+
};
|
| 124 |
+
let Some(stdout) = processes.child.stdout.take() else {
|
| 125 |
+
panic!("Wine process stdout has already been taken");
|
| 126 |
+
};
|
| 127 |
+
stdout
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/// Runs `future`, then asynchronously tears down Wine before returning.
|
| 131 |
+
///
|
| 132 |
+
/// If both the scoped operation and teardown fail, the operation error is
|
| 133 |
+
/// returned with the teardown error attached as context. A panic in the
|
| 134 |
+
/// scoped operation triggers the blocking unwind-time fallback instead.
|
| 135 |
+
pub async fn scope<T>(self, future: impl Future<Output = Result<T>>) -> Result<T> {
|
| 136 |
+
let scope_result = future.await;
|
| 137 |
+
let shutdown_result = self.shutdown().await;
|
| 138 |
+
match (scope_result, shutdown_result) {
|
| 139 |
+
(Ok(value), Ok(())) => Ok(value),
|
| 140 |
+
(Err(error), Ok(())) => Err(error),
|
| 141 |
+
(Ok(_), Err(error)) => Err(error),
|
| 142 |
+
(Err(error), Err(shutdown_error)) => {
|
| 143 |
+
Err(error.context(format!("Wine teardown also failed: {shutdown_error:#}")))
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/// Kills the Windows process, waits for it, and stops its wineserver.
|
| 149 |
+
pub async fn shutdown(mut self) -> Result<()> {
|
| 150 |
+
let Some(processes) = self.processes.as_mut() else {
|
| 151 |
+
anyhow::bail!("Wine process guard is missing");
|
| 152 |
+
};
|
| 153 |
+
let result = processes.shutdown().await;
|
| 154 |
+
self.processes.take();
|
| 155 |
+
result
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
impl Drop for WineTestProcess {
|
| 160 |
+
fn drop(&mut self) {
|
| 161 |
+
// Panicking here starts unwinding, after which WineProcesses performs
|
| 162 |
+
// the blocking fallback while its field is dropped.
|
| 163 |
+
if self.processes.is_some() && !std::thread::panicking() {
|
| 164 |
+
panic!("WineTestProcess dropped without async teardown");
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
impl WineRuntimePaths {
|
| 170 |
+
fn from_runfiles() -> Result<Self> {
|
| 171 |
+
let wine = codex_utils_cargo_bin::cargo_bin("wine")?;
|
| 172 |
+
let runtime_marker = codex_utils_cargo_bin::cargo_bin("wine-runtime-marker")?;
|
| 173 |
+
let dll_path = runtime_marker
|
| 174 |
+
.parent()
|
| 175 |
+
.context("locate Wine runtime directory")?
|
| 176 |
+
.to_path_buf();
|
| 177 |
+
let wineserver = codex_utils_cargo_bin::cargo_bin("wineserver")?;
|
| 178 |
+
let powershell_runtime = codex_utils_cargo_bin::cargo_bin("pwsh-runtime-marker")?
|
| 179 |
+
.parent()
|
| 180 |
+
.context("locate PowerShell runtime directory")?
|
| 181 |
+
.to_path_buf();
|
| 182 |
+
Ok(Self {
|
| 183 |
+
dll_path,
|
| 184 |
+
powershell_runtime,
|
| 185 |
+
wine,
|
| 186 |
+
wineserver,
|
| 187 |
+
})
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
impl WineProcesses {
|
| 192 |
+
async fn shutdown(&mut self) -> Result<()> {
|
| 193 |
+
let (kill_result, check_exit_status) = match self.child.try_wait() {
|
| 194 |
+
Ok(Some(_)) => (Ok(()), true),
|
| 195 |
+
Ok(None) => (
|
| 196 |
+
self.child
|
| 197 |
+
.start_kill()
|
| 198 |
+
.context("kill Windows process running under Wine"),
|
| 199 |
+
false,
|
| 200 |
+
),
|
| 201 |
+
Err(error) => (Err(error).context("check Windows process status"), false),
|
| 202 |
+
};
|
| 203 |
+
let wait_result = self
|
| 204 |
+
.child
|
| 205 |
+
.wait()
|
| 206 |
+
.await
|
| 207 |
+
.context("wait for Windows process running under Wine")
|
| 208 |
+
.and_then(|status| {
|
| 209 |
+
anyhow::ensure!(
|
| 210 |
+
!check_exit_status || status.success(),
|
| 211 |
+
"Windows process exited with {status}"
|
| 212 |
+
);
|
| 213 |
+
Ok(())
|
| 214 |
+
});
|
| 215 |
+
let wineserver_result = async {
|
| 216 |
+
let mut command = TokioCommand::from(self.stop_wineserver_command());
|
| 217 |
+
let status = command.status().await.context("stop isolated wineserver")?;
|
| 218 |
+
anyhow::ensure!(status.success(), "wineserver exited with {status}");
|
| 219 |
+
Ok(())
|
| 220 |
+
}
|
| 221 |
+
.await;
|
| 222 |
+
|
| 223 |
+
// Every cleanup action has been attempted, so an individual error
|
| 224 |
+
// should not cause the blocking fallback to repeat them.
|
| 225 |
+
self.cleanup_complete = true;
|
| 226 |
+
kill_result?;
|
| 227 |
+
wait_result?;
|
| 228 |
+
wineserver_result
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
fn stop_wineserver_command(&self) -> StdCommand {
|
| 232 |
+
let mut command = StdCommand::new(&self.runtime.wineserver);
|
| 233 |
+
configure_wine_environment(&mut command, &self.runtime, self.prefix.path());
|
| 234 |
+
command
|
| 235 |
+
.args(["-k", "-w"])
|
| 236 |
+
.stdout(Stdio::null())
|
| 237 |
+
.stderr(Stdio::null());
|
| 238 |
+
command
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
fn shutdown_blocking(&mut self) {
|
| 242 |
+
log_panic_cleanup(format_args!(
|
| 243 |
+
"Wine panic cleanup starting for prefix {}",
|
| 244 |
+
self.prefix.path().display()
|
| 245 |
+
));
|
| 246 |
+
if let Err(error) = self.child.start_kill() {
|
| 247 |
+
log_panic_cleanup(format_args!(
|
| 248 |
+
"Wine panic cleanup could not kill its child: {error}"
|
| 249 |
+
));
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
log_panic_cleanup(format_args!("Wine panic cleanup waiting for its child"));
|
| 253 |
+
loop {
|
| 254 |
+
match self.child.try_wait() {
|
| 255 |
+
Ok(Some(status)) => {
|
| 256 |
+
log_panic_cleanup(format_args!(
|
| 257 |
+
"Wine panic cleanup child exited with {status}"
|
| 258 |
+
));
|
| 259 |
+
break;
|
| 260 |
+
}
|
| 261 |
+
Ok(None) => std::thread::sleep(Duration::from_millis(10)),
|
| 262 |
+
Err(error) => {
|
| 263 |
+
log_panic_cleanup(format_args!(
|
| 264 |
+
"Wine panic cleanup could not wait for its child: {error}"
|
| 265 |
+
));
|
| 266 |
+
break;
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
log_panic_cleanup(format_args!("Wine panic cleanup stopping its wineserver"));
|
| 272 |
+
match self.stop_wineserver_command().status() {
|
| 273 |
+
Ok(status) => log_panic_cleanup(format_args!(
|
| 274 |
+
"Wine panic cleanup wineserver exited with {status}"
|
| 275 |
+
)),
|
| 276 |
+
Err(error) => log_panic_cleanup(format_args!(
|
| 277 |
+
"Wine panic cleanup could not stop its wineserver: {error}"
|
| 278 |
+
)),
|
| 279 |
+
}
|
| 280 |
+
self.cleanup_complete = true;
|
| 281 |
+
log_panic_cleanup(format_args!("Wine panic cleanup complete"));
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
impl Drop for WineProcesses {
|
| 286 |
+
fn drop(&mut self) {
|
| 287 |
+
// Never introduce a second panic while unwinding. Blocking here is
|
| 288 |
+
// intentional because test failures must not leak Wine children.
|
| 289 |
+
if !self.cleanup_complete && std::thread::panicking() {
|
| 290 |
+
self.shutdown_blocking();
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
fn log_panic_cleanup(args: std::fmt::Arguments<'_>) {
|
| 296 |
+
let _ = writeln!(std::io::stderr().lock(), "{args}");
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
fn configure_wine_environment(command: &mut StdCommand, runtime: &WineRuntimePaths, prefix: &Path) {
|
| 300 |
+
command
|
| 301 |
+
.env_remove("DISPLAY")
|
| 302 |
+
.env("HOME", prefix)
|
| 303 |
+
.env("XDG_RUNTIME_DIR", prefix)
|
| 304 |
+
.env("WINEARCH", "win64")
|
| 305 |
+
.env("WINEPREFIX", prefix)
|
| 306 |
+
.env("WINEDLLPATH", &runtime.dll_path)
|
| 307 |
+
.env("WINESERVER", &runtime.wineserver)
|
| 308 |
+
.env("WINEDEBUG", "-all")
|
| 309 |
+
.env("WINEDLLOVERRIDES", "mscoree,mshtml,winegstreamer=")
|
| 310 |
+
.env("LANG", "C.UTF-8")
|
| 311 |
+
.env("LC_ALL", "C.UTF-8")
|
| 312 |
+
.env("LC_CTYPE", "C.UTF-8")
|
| 313 |
+
.env("TEMP", r"C:\windows\temp")
|
| 314 |
+
.env("TMP", r"C:\windows\temp");
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/// Installs the complete pinned PowerShell distribution where Windows tooling
|
| 318 |
+
/// expects to discover PowerShell 7.
|
| 319 |
+
///
|
| 320 |
+
/// `pwsh.exe` is not a standalone executable: it loads its adjacent .NET host,
|
| 321 |
+
/// managed assemblies, native libraries, modules, and configuration files at
|
| 322 |
+
/// startup. The Bazel archive is exposed through runfiles rather than a normal
|
| 323 |
+
/// Windows installation, while shell detection deliberately probes the
|
| 324 |
+
/// conventional `C:\Program Files\PowerShell\7` fallback. We therefore have to
|
| 325 |
+
/// reproduce the archive's directory tree inside each isolated Wine prefix;
|
| 326 |
+
/// copying only the executable would fail before a command could run.
|
| 327 |
+
fn install_powershell_runtime(prefix: &Path, runtime: &Path) -> Result<()> {
|
| 328 |
+
let powershell_parent = prefix
|
| 329 |
+
.join("drive_c")
|
| 330 |
+
.join("Program Files")
|
| 331 |
+
.join("PowerShell");
|
| 332 |
+
fs::create_dir_all(&powershell_parent).context("create PowerShell installation parent")?;
|
| 333 |
+
let destination = powershell_parent.join("7");
|
| 334 |
+
materialize_runtime_directory(runtime, &destination)
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/// Recursively reproduces a runfiles directory in a writable Wine prefix.
|
| 338 |
+
///
|
| 339 |
+
/// Bazel runfiles may be immutable, represented by a symlink forest, and may
|
| 340 |
+
/// contain the PowerShell distribution on a different filesystem from the
|
| 341 |
+
/// temporary prefix. Hard links avoid repeatedly copying the roughly
|
| 342 |
+
/// hundred-megabyte runtime when both locations share a filesystem; the copy
|
| 343 |
+
/// fallback preserves correctness for sandbox or remote-execution layouts
|
| 344 |
+
/// where cross-device hard links are unavailable.
|
| 345 |
+
fn materialize_runtime_directory(source: &Path, destination: &Path) -> Result<()> {
|
| 346 |
+
fs::create_dir_all(destination).with_context(|| {
|
| 347 |
+
format!(
|
| 348 |
+
"create PowerShell runtime directory {}",
|
| 349 |
+
destination.display()
|
| 350 |
+
)
|
| 351 |
+
})?;
|
| 352 |
+
for entry in fs::read_dir(source)
|
| 353 |
+
.with_context(|| format!("read PowerShell runtime directory {}", source.display()))?
|
| 354 |
+
{
|
| 355 |
+
let entry = entry.context("read PowerShell runtime entry")?;
|
| 356 |
+
let source_path = entry.path();
|
| 357 |
+
let destination_path = destination.join(entry.file_name());
|
| 358 |
+
// Local Bazel runfiles trees expose external-repository files as
|
| 359 |
+
// symlinks. Resolve those trusted runfiles entries before inspecting
|
| 360 |
+
// or linking them so the writable prefix contains ordinary files.
|
| 361 |
+
let resolved_source_path = fs::canonicalize(&source_path).with_context(|| {
|
| 362 |
+
format!("resolve PowerShell runtime entry {}", source_path.display())
|
| 363 |
+
})?;
|
| 364 |
+
let file_type = fs::metadata(&resolved_source_path)
|
| 365 |
+
.with_context(|| {
|
| 366 |
+
format!(
|
| 367 |
+
"inspect PowerShell runtime entry {}",
|
| 368 |
+
resolved_source_path.display()
|
| 369 |
+
)
|
| 370 |
+
})?
|
| 371 |
+
.file_type();
|
| 372 |
+
if file_type.is_dir() {
|
| 373 |
+
// PowerShell resolves assemblies and modules by their relative
|
| 374 |
+
// locations, so flattening the archive is not an option.
|
| 375 |
+
materialize_runtime_directory(&resolved_source_path, &destination_path)?;
|
| 376 |
+
} else if file_type.is_file() {
|
| 377 |
+
// A hard link gives each prefix the expected installation layout
|
| 378 |
+
// without duplicating the large runtime in the common local case.
|
| 379 |
+
if fs::hard_link(&resolved_source_path, &destination_path).is_err() {
|
| 380 |
+
// Cross-device links are common under Bazel sandboxing and
|
| 381 |
+
// remote execution, where an ordinary copy is still valid.
|
| 382 |
+
fs::copy(&resolved_source_path, &destination_path).with_context(|| {
|
| 383 |
+
format!(
|
| 384 |
+
"copy PowerShell runtime file {} to {}",
|
| 385 |
+
resolved_source_path.display(),
|
| 386 |
+
destination_path.display()
|
| 387 |
+
)
|
| 388 |
+
})?;
|
| 389 |
+
}
|
| 390 |
+
} else {
|
| 391 |
+
anyhow::bail!(
|
| 392 |
+
"unsupported PowerShell runtime entry type at {}",
|
| 393 |
+
source_path.display()
|
| 394 |
+
);
|
| 395 |
+
}
|
| 396 |
+
}
|
| 397 |
+
Ok(())
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
#[cfg(test)]
|
| 401 |
+
#[path = "lib_tests.rs"]
|
| 402 |
+
mod tests;
|
bazel/rules/testing/wine/src/lib_tests.rs
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::any::Any;
|
| 2 |
+
use std::collections::HashMap;
|
| 3 |
+
use std::fs;
|
| 4 |
+
use std::future::Future;
|
| 5 |
+
use std::panic::AssertUnwindSafe;
|
| 6 |
+
use std::path::Path;
|
| 7 |
+
use std::path::PathBuf;
|
| 8 |
+
use std::time::Duration;
|
| 9 |
+
|
| 10 |
+
use anyhow::Context;
|
| 11 |
+
use anyhow::Result;
|
| 12 |
+
use anyhow::anyhow;
|
| 13 |
+
use codex_utils_pty::SpawnedProcess;
|
| 14 |
+
use codex_utils_pty::TerminalSize;
|
| 15 |
+
use futures::FutureExt;
|
| 16 |
+
use pretty_assertions::assert_eq;
|
| 17 |
+
use tempfile::TempDir;
|
| 18 |
+
use tokio::io::AsyncBufReadExt;
|
| 19 |
+
use tokio::io::BufReader;
|
| 20 |
+
use tokio::process::Command as TokioCommand;
|
| 21 |
+
use tokio::time::timeout;
|
| 22 |
+
|
| 23 |
+
use super::WineRuntimePaths;
|
| 24 |
+
use super::WineTestCommand;
|
| 25 |
+
use super::WineTestProcess;
|
| 26 |
+
use super::install_powershell_runtime;
|
| 27 |
+
|
| 28 |
+
async fn waiting_smoke_process() -> Result<WineTestProcess> {
|
| 29 |
+
let executable = codex_utils_cargo_bin::cargo_bin("wine-smoke")?;
|
| 30 |
+
let mut process = WineTestCommand::new(executable).arg("--wait").spawn()?;
|
| 31 |
+
let mut lines = BufReader::new(process.take_stdout()).lines();
|
| 32 |
+
let ready_line = lines
|
| 33 |
+
.next_line()
|
| 34 |
+
.await?
|
| 35 |
+
.context("Windows smoke process exited before becoming ready")?;
|
| 36 |
+
assert_eq!(ready_line, "WINE_TEST_READY");
|
| 37 |
+
Ok(process)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
fn prefix_path(process: &WineTestProcess) -> PathBuf {
|
| 41 |
+
process
|
| 42 |
+
.processes
|
| 43 |
+
.as_ref()
|
| 44 |
+
.expect("Wine process guard")
|
| 45 |
+
.prefix
|
| 46 |
+
.path()
|
| 47 |
+
.to_path_buf()
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
fn assert_prefix_removed(prefix: &Path) {
|
| 51 |
+
assert!(
|
| 52 |
+
!prefix.exists(),
|
| 53 |
+
"Wine prefix remains: {}",
|
| 54 |
+
prefix.display()
|
| 55 |
+
);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
fn assert_panic_message(panic: Box<dyn Any + Send>, expected: &str) {
|
| 59 |
+
assert_eq!(panic.downcast_ref::<&str>(), Some(&expected));
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async fn assert_future_panics<T>(future: impl Future<Output = T>, expected: &str) {
|
| 63 |
+
let panic = match AssertUnwindSafe(future).catch_unwind().await {
|
| 64 |
+
Ok(_) => panic!("future should panic"),
|
| 65 |
+
Err(panic) => panic,
|
| 66 |
+
};
|
| 67 |
+
assert_panic_message(panic, expected);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
async fn process_with_failing_wineserver_stop() -> Result<WineTestProcess> {
|
| 71 |
+
let mut process = waiting_smoke_process().await?;
|
| 72 |
+
let processes = process.processes.as_mut().expect("Wine process guard");
|
| 73 |
+
|
| 74 |
+
let mut command = TokioCommand::from(processes.stop_wineserver_command());
|
| 75 |
+
let status = command
|
| 76 |
+
.status()
|
| 77 |
+
.await
|
| 78 |
+
.context("pre-stop isolated wineserver")?;
|
| 79 |
+
assert!(status.success(), "wineserver exited with {status}");
|
| 80 |
+
|
| 81 |
+
processes.runtime.wineserver = processes.prefix.path().join("missing-wineserver");
|
| 82 |
+
Ok(process)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
#[tokio::test]
|
| 86 |
+
async fn dropping_without_teardown_panics() -> Result<()> {
|
| 87 |
+
let process = waiting_smoke_process().await?;
|
| 88 |
+
let prefix = prefix_path(&process);
|
| 89 |
+
assert_future_panics(
|
| 90 |
+
async move { drop(process) },
|
| 91 |
+
"WineTestProcess dropped without async teardown",
|
| 92 |
+
)
|
| 93 |
+
.await;
|
| 94 |
+
assert_prefix_removed(&prefix);
|
| 95 |
+
Ok(())
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
#[tokio::test]
|
| 99 |
+
async fn dropping_while_panicking_does_not_panic_again() -> Result<()> {
|
| 100 |
+
let process = waiting_smoke_process().await?;
|
| 101 |
+
let prefix = prefix_path(&process);
|
| 102 |
+
assert_future_panics(
|
| 103 |
+
async move {
|
| 104 |
+
let _process = process;
|
| 105 |
+
panic!("sentinel panic");
|
| 106 |
+
},
|
| 107 |
+
"sentinel panic",
|
| 108 |
+
)
|
| 109 |
+
.await;
|
| 110 |
+
assert_prefix_removed(&prefix);
|
| 111 |
+
Ok(())
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
#[tokio::test]
|
| 115 |
+
async fn async_teardown_disarms_drop_bomb() -> Result<()> {
|
| 116 |
+
let process = waiting_smoke_process().await?;
|
| 117 |
+
let prefix = prefix_path(&process);
|
| 118 |
+
process.shutdown().await?;
|
| 119 |
+
assert_prefix_removed(&prefix);
|
| 120 |
+
Ok(())
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
#[tokio::test]
|
| 124 |
+
async fn take_stdout_panics_when_called_twice() -> Result<()> {
|
| 125 |
+
let mut process = waiting_smoke_process().await?;
|
| 126 |
+
let prefix = prefix_path(&process);
|
| 127 |
+
assert_future_panics(
|
| 128 |
+
async {
|
| 129 |
+
process.take_stdout();
|
| 130 |
+
},
|
| 131 |
+
"Wine process stdout has already been taken",
|
| 132 |
+
)
|
| 133 |
+
.await;
|
| 134 |
+
process.shutdown().await?;
|
| 135 |
+
assert_prefix_removed(&prefix);
|
| 136 |
+
Ok(())
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
#[tokio::test]
|
| 140 |
+
async fn scope_returns_value_and_tears_down() -> Result<()> {
|
| 141 |
+
let process = waiting_smoke_process().await?;
|
| 142 |
+
let prefix = prefix_path(&process);
|
| 143 |
+
let value = process
|
| 144 |
+
.scope(async { Ok::<_, anyhow::Error>("scope value") })
|
| 145 |
+
.await?;
|
| 146 |
+
assert_eq!(value, "scope value");
|
| 147 |
+
assert_prefix_removed(&prefix);
|
| 148 |
+
Ok(())
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
#[tokio::test]
|
| 152 |
+
async fn scope_returns_body_error_and_tears_down() -> Result<()> {
|
| 153 |
+
let process = waiting_smoke_process().await?;
|
| 154 |
+
let prefix = prefix_path(&process);
|
| 155 |
+
|
| 156 |
+
let error = process
|
| 157 |
+
.scope(async { Err::<(), _>(anyhow!("scope body failed")) })
|
| 158 |
+
.await
|
| 159 |
+
.expect_err("scope body should fail");
|
| 160 |
+
|
| 161 |
+
assert_eq!(error.to_string(), "scope body failed");
|
| 162 |
+
assert_prefix_removed(&prefix);
|
| 163 |
+
Ok(())
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
#[tokio::test]
|
| 167 |
+
async fn scope_panic_preserves_panic_and_tears_down() -> Result<()> {
|
| 168 |
+
let process = waiting_smoke_process().await?;
|
| 169 |
+
let prefix = prefix_path(&process);
|
| 170 |
+
|
| 171 |
+
assert_future_panics(
|
| 172 |
+
process.scope::<()>(async { panic!("scope panic") }),
|
| 173 |
+
"scope panic",
|
| 174 |
+
)
|
| 175 |
+
.await;
|
| 176 |
+
assert_prefix_removed(&prefix);
|
| 177 |
+
Ok(())
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
#[tokio::test]
|
| 181 |
+
async fn shutdown_reports_nonzero_process_exit() -> Result<()> {
|
| 182 |
+
let executable = codex_utils_cargo_bin::cargo_bin("wine-smoke")?;
|
| 183 |
+
let mut process = WineTestCommand::new(executable).arg("--fail").spawn()?;
|
| 184 |
+
let prefix = prefix_path(&process);
|
| 185 |
+
let status = process
|
| 186 |
+
.processes
|
| 187 |
+
.as_mut()
|
| 188 |
+
.expect("Wine process guard")
|
| 189 |
+
.child
|
| 190 |
+
.wait()
|
| 191 |
+
.await?;
|
| 192 |
+
assert!(
|
| 193 |
+
!status.success(),
|
| 194 |
+
"Windows smoke process unexpectedly passed"
|
| 195 |
+
);
|
| 196 |
+
|
| 197 |
+
let error = process.shutdown().await.expect_err("shutdown should fail");
|
| 198 |
+
|
| 199 |
+
assert!(error.to_string().starts_with("Windows process exited with"));
|
| 200 |
+
assert_prefix_removed(&prefix);
|
| 201 |
+
Ok(())
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
#[tokio::test]
|
| 205 |
+
async fn scope_preserves_body_error_when_teardown_also_fails() -> Result<()> {
|
| 206 |
+
let process = process_with_failing_wineserver_stop().await?;
|
| 207 |
+
let prefix = prefix_path(&process);
|
| 208 |
+
|
| 209 |
+
let error = process
|
| 210 |
+
.scope(async { Err::<(), _>(anyhow!("scope body failed")) })
|
| 211 |
+
.await
|
| 212 |
+
.expect_err("scope body and teardown should fail");
|
| 213 |
+
|
| 214 |
+
assert!(
|
| 215 |
+
error
|
| 216 |
+
.to_string()
|
| 217 |
+
.starts_with("Wine teardown also failed: stop isolated wineserver"),
|
| 218 |
+
"unexpected error: {error:#}"
|
| 219 |
+
);
|
| 220 |
+
assert_eq!(
|
| 221 |
+
error.chain().last().map(ToString::to_string),
|
| 222 |
+
Some("scope body failed".to_string())
|
| 223 |
+
);
|
| 224 |
+
assert_prefix_removed(&prefix);
|
| 225 |
+
Ok(())
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
#[tokio::test]
|
| 229 |
+
async fn shutdown_returns_teardown_error() -> Result<()> {
|
| 230 |
+
let process = process_with_failing_wineserver_stop().await?;
|
| 231 |
+
let prefix = prefix_path(&process);
|
| 232 |
+
|
| 233 |
+
let error = process
|
| 234 |
+
.shutdown()
|
| 235 |
+
.await
|
| 236 |
+
.expect_err("shutdown should report a wineserver failure");
|
| 237 |
+
|
| 238 |
+
assert_eq!(error.to_string(), "stop isolated wineserver");
|
| 239 |
+
assert_prefix_removed(&prefix);
|
| 240 |
+
Ok(())
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
#[test]
|
| 244 |
+
fn powershell_runtime_is_materialized_at_the_windows_fallback_path() -> Result<()> {
|
| 245 |
+
let prefix = TempDir::new()?;
|
| 246 |
+
let runtime = TempDir::new()?;
|
| 247 |
+
fs::create_dir(runtime.path().join("Modules"))?;
|
| 248 |
+
fs::write(runtime.path().join("pwsh.exe"), b"pwsh")?;
|
| 249 |
+
fs::write(runtime.path().join("Modules").join("marker.txt"), b"module")?;
|
| 250 |
+
|
| 251 |
+
install_powershell_runtime(prefix.path(), runtime.path())?;
|
| 252 |
+
|
| 253 |
+
let installed = prefix
|
| 254 |
+
.path()
|
| 255 |
+
.join("drive_c")
|
| 256 |
+
.join("Program Files")
|
| 257 |
+
.join("PowerShell")
|
| 258 |
+
.join("7");
|
| 259 |
+
assert_eq!(fs::read(installed.join("pwsh.exe"))?, b"pwsh");
|
| 260 |
+
assert_eq!(
|
| 261 |
+
fs::read(installed.join("Modules").join("marker.txt"))?,
|
| 262 |
+
b"module"
|
| 263 |
+
);
|
| 264 |
+
Ok(())
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
#[test]
|
| 268 |
+
fn powershell_runtime_follows_runfiles_symlinks() -> Result<()> {
|
| 269 |
+
let prefix = TempDir::new()?;
|
| 270 |
+
let runtime = TempDir::new()?;
|
| 271 |
+
let backing = TempDir::new()?;
|
| 272 |
+
let backing_file = backing.path().join("pwsh.exe");
|
| 273 |
+
fs::write(&backing_file, b"pwsh")?;
|
| 274 |
+
std::os::unix::fs::symlink(&backing_file, runtime.path().join("pwsh.exe"))?;
|
| 275 |
+
|
| 276 |
+
install_powershell_runtime(prefix.path(), runtime.path())?;
|
| 277 |
+
|
| 278 |
+
let installed = prefix
|
| 279 |
+
.path()
|
| 280 |
+
.join("drive_c")
|
| 281 |
+
.join("Program Files")
|
| 282 |
+
.join("PowerShell")
|
| 283 |
+
.join("7")
|
| 284 |
+
.join("pwsh.exe");
|
| 285 |
+
assert_eq!(fs::read(installed)?, b"pwsh");
|
| 286 |
+
Ok(())
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
#[tokio::test]
|
| 290 |
+
async fn pinned_powershell_runs_under_wine_with_a_pty() -> Result<()> {
|
| 291 |
+
// Keep this integration smoke test local to the Wine support crate. The
|
| 292 |
+
// production-shaped PowerShell launch path belongs to exec-server tests.
|
| 293 |
+
// The marker makes the assertion resilient to Wine or PTY startup chatter.
|
| 294 |
+
const POWERSHELL_SMOKE_MARKER: &str = "WINE_PWSH_SMOKE";
|
| 295 |
+
// Besides proving that the pinned runtime starts, report the properties
|
| 296 |
+
// that shell detection and command construction rely on: PowerShell 7 Core
|
| 297 |
+
// running with Windows semantics and a backslash path separator.
|
| 298 |
+
const POWERSHELL_SMOKE_SCRIPT: &str = concat!(
|
| 299 |
+
"$ErrorActionPreference = 'Stop'; ",
|
| 300 |
+
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); ",
|
| 301 |
+
"$separatorCode = [int]([System.IO.Path]::DirectorySeparatorChar); ",
|
| 302 |
+
"if ($PSVersionTable.PSVersion.Major -ne 7) { throw 'expected PowerShell 7' }; ",
|
| 303 |
+
"if ($PSVersionTable.PSEdition -ne 'Core') { throw 'expected PowerShell Core' }; ",
|
| 304 |
+
"if (-not $IsWindows) { throw 'expected Windows semantics' }; ",
|
| 305 |
+
"if ($separatorCode -ne 92) { throw 'expected backslash path separator' }; ",
|
| 306 |
+
"Write-Output ('WINE_PWSH_SMOKE|' + ",
|
| 307 |
+
"$PSVersionTable.PSVersion.ToString() + '|' + ",
|
| 308 |
+
"$PSVersionTable.PSEdition + '|' + ",
|
| 309 |
+
"$IsWindows.ToString().ToLowerInvariant() + '|' + $separatorCode)",
|
| 310 |
+
);
|
| 311 |
+
let runtime = WineRuntimePaths::from_runfiles()?;
|
| 312 |
+
let prefix = TempDir::new()?;
|
| 313 |
+
install_powershell_runtime(prefix.path(), &runtime.powershell_runtime)?;
|
| 314 |
+
let mut env = std::env::vars().collect::<HashMap<_, _>>();
|
| 315 |
+
env.remove("DISPLAY");
|
| 316 |
+
env.extend([
|
| 317 |
+
(
|
| 318 |
+
"HOME".to_string(),
|
| 319 |
+
prefix.path().to_string_lossy().into_owned(),
|
| 320 |
+
),
|
| 321 |
+
(
|
| 322 |
+
"XDG_RUNTIME_DIR".to_string(),
|
| 323 |
+
prefix.path().to_string_lossy().into_owned(),
|
| 324 |
+
),
|
| 325 |
+
("WINEARCH".to_string(), "win64".to_string()),
|
| 326 |
+
(
|
| 327 |
+
"WINEPREFIX".to_string(),
|
| 328 |
+
prefix.path().to_string_lossy().into_owned(),
|
| 329 |
+
),
|
| 330 |
+
(
|
| 331 |
+
"WINEDLLPATH".to_string(),
|
| 332 |
+
runtime.dll_path.to_string_lossy().into_owned(),
|
| 333 |
+
),
|
| 334 |
+
(
|
| 335 |
+
"WINESERVER".to_string(),
|
| 336 |
+
runtime.wineserver.to_string_lossy().into_owned(),
|
| 337 |
+
),
|
| 338 |
+
("WINEDEBUG".to_string(), "-all".to_string()),
|
| 339 |
+
(
|
| 340 |
+
"WINEDLLOVERRIDES".to_string(),
|
| 341 |
+
"mscoree,mshtml,winegstreamer=".to_string(),
|
| 342 |
+
),
|
| 343 |
+
("LANG".to_string(), "C.UTF-8".to_string()),
|
| 344 |
+
("LC_ALL".to_string(), "C.UTF-8".to_string()),
|
| 345 |
+
("LC_CTYPE".to_string(), "C.UTF-8".to_string()),
|
| 346 |
+
("TEMP".to_string(), r"C:\windows\temp".to_string()),
|
| 347 |
+
("TMP".to_string(), r"C:\windows\temp".to_string()),
|
| 348 |
+
]);
|
| 349 |
+
let args = [
|
| 350 |
+
r"C:\Program Files\PowerShell\7\pwsh.exe".to_string(),
|
| 351 |
+
"-NoLogo".to_string(),
|
| 352 |
+
"-NoProfile".to_string(),
|
| 353 |
+
"-NonInteractive".to_string(),
|
| 354 |
+
"-Command".to_string(),
|
| 355 |
+
POWERSHELL_SMOKE_SCRIPT.to_string(),
|
| 356 |
+
];
|
| 357 |
+
let wine = runtime.wine.to_string_lossy().into_owned();
|
| 358 |
+
let SpawnedProcess {
|
| 359 |
+
session,
|
| 360 |
+
mut stdout_rx,
|
| 361 |
+
mut stderr_rx,
|
| 362 |
+
exit_rx,
|
| 363 |
+
} = codex_utils_pty::spawn_pty_process(
|
| 364 |
+
&wine,
|
| 365 |
+
&args,
|
| 366 |
+
prefix.path(),
|
| 367 |
+
&env,
|
| 368 |
+
/*arg0*/ &None,
|
| 369 |
+
TerminalSize::default(),
|
| 370 |
+
/*inherited_fds*/ &[],
|
| 371 |
+
)
|
| 372 |
+
.await?;
|
| 373 |
+
let command_result = timeout(Duration::from_secs(30), async {
|
| 374 |
+
let stdout = async {
|
| 375 |
+
let mut output = Vec::new();
|
| 376 |
+
while let Some(chunk) = stdout_rx.recv().await {
|
| 377 |
+
output.extend(chunk);
|
| 378 |
+
}
|
| 379 |
+
output
|
| 380 |
+
};
|
| 381 |
+
let stderr = async {
|
| 382 |
+
let mut output = Vec::new();
|
| 383 |
+
while let Some(chunk) = stderr_rx.recv().await {
|
| 384 |
+
output.extend(chunk);
|
| 385 |
+
}
|
| 386 |
+
output
|
| 387 |
+
};
|
| 388 |
+
let (stdout, stderr, exit_code) = tokio::join!(stdout, stderr, exit_rx);
|
| 389 |
+
Ok::<_, anyhow::Error>((stdout, stderr, exit_code.context("wait for PowerShell")?))
|
| 390 |
+
})
|
| 391 |
+
.await
|
| 392 |
+
.context("PowerShell smoke test timed out")
|
| 393 |
+
.and_then(std::convert::identity);
|
| 394 |
+
drop(session);
|
| 395 |
+
let shutdown_result = timeout(Duration::from_secs(10), async {
|
| 396 |
+
let mut command = TokioCommand::new(&runtime.wineserver);
|
| 397 |
+
command
|
| 398 |
+
.args(["-k", "-w"])
|
| 399 |
+
.env("HOME", prefix.path())
|
| 400 |
+
.env("WINEPREFIX", prefix.path())
|
| 401 |
+
.env("XDG_RUNTIME_DIR", prefix.path())
|
| 402 |
+
.kill_on_drop(true);
|
| 403 |
+
let status = command.status().await.context("stop isolated wineserver")?;
|
| 404 |
+
anyhow::ensure!(
|
| 405 |
+
status.success() || status.code() == Some(1),
|
| 406 |
+
"wineserver exited with {status}"
|
| 407 |
+
);
|
| 408 |
+
Ok::<_, anyhow::Error>(())
|
| 409 |
+
})
|
| 410 |
+
.await
|
| 411 |
+
.context("stop isolated wineserver timed out")
|
| 412 |
+
.and_then(std::convert::identity);
|
| 413 |
+
let (stdout, stderr, exit_code) = match (command_result, shutdown_result) {
|
| 414 |
+
(Ok(output), Ok(())) => output,
|
| 415 |
+
(Err(error), Ok(())) => return Err(error),
|
| 416 |
+
(Ok(_), Err(error)) => return Err(error),
|
| 417 |
+
(Err(error), Err(shutdown_error)) => {
|
| 418 |
+
return Err(error.context(format!("Wine teardown also failed: {shutdown_error:#}")));
|
| 419 |
+
}
|
| 420 |
+
};
|
| 421 |
+
anyhow::ensure!(
|
| 422 |
+
exit_code == 0,
|
| 423 |
+
"PowerShell exited with {}; stderr: {}",
|
| 424 |
+
exit_code,
|
| 425 |
+
String::from_utf8_lossy(&stderr),
|
| 426 |
+
);
|
| 427 |
+
let output = String::from_utf8(stdout)?;
|
| 428 |
+
let marker_start = output
|
| 429 |
+
.find(POWERSHELL_SMOKE_MARKER)
|
| 430 |
+
.with_context(|| format!("PowerShell smoke marker was missing from {output:?}"))?;
|
| 431 |
+
let smoke = output[marker_start..]
|
| 432 |
+
.lines()
|
| 433 |
+
.next()
|
| 434 |
+
.context("PowerShell smoke marker line was incomplete")?
|
| 435 |
+
.trim_end_matches('\r');
|
| 436 |
+
let fields = smoke.split('|').collect::<Vec<_>>();
|
| 437 |
+
assert_eq!(
|
| 438 |
+
fields.len(),
|
| 439 |
+
5,
|
| 440 |
+
"unexpected PowerShell smoke output: {smoke}"
|
| 441 |
+
);
|
| 442 |
+
assert_eq!(fields[0], POWERSHELL_SMOKE_MARKER);
|
| 443 |
+
assert_eq!(
|
| 444 |
+
fields[1].split('.').next(),
|
| 445 |
+
Some("7"),
|
| 446 |
+
"expected PowerShell 7.x, got {}",
|
| 447 |
+
fields[1],
|
| 448 |
+
);
|
| 449 |
+
assert_eq!(&fields[2..], &["Core", "true", "92"]);
|
| 450 |
+
Ok(())
|
| 451 |
+
}
|
bazel/rules/testing/wine/wine.bzl
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Macros for cross-building Windows Rust binaries and testing them with Wine."""
|
| 2 |
+
|
| 3 |
+
load("@rules_rust//rust:defs.bzl", "rust_test")
|
| 4 |
+
load("//:defs.bzl", "WINDOWS_GNULLVM_RUSTC_LINK_FLAGS")
|
| 5 |
+
load("//bazel/rules/testing:foreign_platform_binary.bzl", "foreign_platform_binary")
|
| 6 |
+
load(":wine_runtime.bzl", "WINE_TEST_TARGET_COMPATIBLE_WITH", "wine_test_runtime")
|
| 7 |
+
|
| 8 |
+
def wine_rust_test(
|
| 9 |
+
name,
|
| 10 |
+
windows_binaries,
|
| 11 |
+
host_binaries = {},
|
| 12 |
+
data = [],
|
| 13 |
+
target_compatible_with = [],
|
| 14 |
+
**kwargs):
|
| 15 |
+
"""Defines an x86-64 Linux Rust test with a pinned Wine runtime.
|
| 16 |
+
|
| 17 |
+
Each `windows_binaries` executable is transitioned to GNU/LLVM Windows;
|
| 18 |
+
every Rust dependency receives the repository's Windows linker flags while
|
| 19 |
+
the test stays on x86-64 Linux. Its environment-variable contract is:
|
| 20 |
+
|
| 21 |
+
* Each `host_binaries` and `windows_binaries` entry contributes
|
| 22 |
+
`CARGO_BIN_EXE_<binary_name>` for its executable.
|
| 23 |
+
* `CARGO_BIN_EXE_wine` and `CARGO_BIN_EXE_wineserver` identify Wine tools.
|
| 24 |
+
* `CARGO_BIN_EXE_wine-runtime-marker` identifies a file whose parent is the
|
| 25 |
+
Wine DLL directory to use as `WINEDLLPATH`.
|
| 26 |
+
* `CARGO_BIN_EXE_pwsh` identifies the pinned PowerShell executable and
|
| 27 |
+
`CARGO_BIN_EXE_pwsh-runtime-marker` identifies a file whose parent is the
|
| 28 |
+
complete PowerShell runtime.
|
| 29 |
+
|
| 30 |
+
These are Bazel runfile locations. Resolve binaries with
|
| 31 |
+
`codex_utils_cargo_bin::cargo_bin`; `:wine_test_support` resolves the fixed
|
| 32 |
+
runtime names and starts each process in an isolated prefix.
|
| 33 |
+
|
| 34 |
+
Args:
|
| 35 |
+
name: Name of the generated Linux `rust_test`.
|
| 36 |
+
windows_binaries: Map from `CARGO_BIN_EXE_*` suffixes to Windows targets.
|
| 37 |
+
host_binaries: Map from `CARGO_BIN_EXE_*` suffixes to Linux host targets.
|
| 38 |
+
data: Additional runtime data for the Linux test.
|
| 39 |
+
target_compatible_with: Additional compatibility constraints.
|
| 40 |
+
**kwargs: Remaining attributes forwarded to `rust_test`.
|
| 41 |
+
"""
|
| 42 |
+
binaries = dict(host_binaries)
|
| 43 |
+
for index, binary_name in enumerate(sorted(windows_binaries.keys())):
|
| 44 |
+
if binary_name in binaries:
|
| 45 |
+
fail("Windows test binary name collides with host binary: {}".format(binary_name))
|
| 46 |
+
transitioned_binary = name + "-windows-binary-" + str(index)
|
| 47 |
+
foreign_platform_binary(
|
| 48 |
+
name = transitioned_binary,
|
| 49 |
+
binary = windows_binaries[binary_name],
|
| 50 |
+
extra_rustc_flags = WINDOWS_GNULLVM_RUSTC_LINK_FLAGS,
|
| 51 |
+
platform = "//:windows_x86_64_gnullvm",
|
| 52 |
+
tags = ["manual"],
|
| 53 |
+
target_compatible_with = [
|
| 54 |
+
"@platforms//cpu:x86_64",
|
| 55 |
+
"@platforms//os:linux",
|
| 56 |
+
],
|
| 57 |
+
testonly = True,
|
| 58 |
+
visibility = ["//visibility:private"],
|
| 59 |
+
)
|
| 60 |
+
binaries[binary_name] = ":" + transitioned_binary
|
| 61 |
+
|
| 62 |
+
runtime = wine_test_runtime(binaries)
|
| 63 |
+
rust_test(
|
| 64 |
+
name = name,
|
| 65 |
+
data = data + runtime.data,
|
| 66 |
+
env = runtime.env,
|
| 67 |
+
target_compatible_with = target_compatible_with + WINE_TEST_TARGET_COMPATIBLE_WITH,
|
| 68 |
+
**kwargs
|
| 69 |
+
)
|
bazel/rules/testing/wine/wine_runtime.bzl
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Runfiles shared by tests that execute Windows binaries through Wine."""
|
| 2 |
+
|
| 3 |
+
_WINE_RUNTIME_BINARIES = {
|
| 4 |
+
"pwsh": "@powershell_windows_x86_64//:pwsh",
|
| 5 |
+
"pwsh-runtime-marker": "@powershell_windows_x86_64//:runtime_marker",
|
| 6 |
+
"wine": "@wine_linux_x86_64//:wine",
|
| 7 |
+
"wine-runtime-marker": "@wine_linux_x86_64//:runtime_marker",
|
| 8 |
+
"wineserver": "@wine_linux_x86_64//:wineserver",
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
_WINE_RUNTIME_DATA = [
|
| 12 |
+
"@powershell_windows_x86_64//:runtime",
|
| 13 |
+
"@wine_linux_x86_64//:runtime",
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
WINE_TEST_TARGET_COMPATIBLE_WITH = [
|
| 17 |
+
"@llvm//constraints/libc:gnu.2.28",
|
| 18 |
+
"@platforms//cpu:x86_64",
|
| 19 |
+
"@platforms//os:linux",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
def wine_test_runtime(test_binaries = {}):
|
| 23 |
+
"""Returns data and environment mappings for a Wine-backed test."""
|
| 24 |
+
binaries = dict(_WINE_RUNTIME_BINARIES)
|
| 25 |
+
for binary_name in sorted(test_binaries.keys()):
|
| 26 |
+
if binary_name in binaries:
|
| 27 |
+
fail("test binary name collides with Wine runtime: {}".format(binary_name))
|
| 28 |
+
binaries[binary_name] = test_binaries[binary_name]
|
| 29 |
+
|
| 30 |
+
return struct(
|
| 31 |
+
data = _WINE_RUNTIME_DATA + [binary for binary in binaries.values()],
|
| 32 |
+
env = {
|
| 33 |
+
"CARGO_BIN_EXE_{}".format(binary_name): "$(rlocationpath {})".format(binary)
|
| 34 |
+
for binary_name, binary in binaries.items()
|
| 35 |
+
},
|
| 36 |
+
runfile_env = {
|
| 37 |
+
binary_label: "CARGO_BIN_EXE_" + binary_name
|
| 38 |
+
for binary_name, binary_label in binaries.items()
|
| 39 |
+
},
|
| 40 |
+
)
|
bazel/schema/BUILD.bazel
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
load("//bazel/rules:schema_bundle.bzl", "schema_bundle")
|
| 2 |
+
|
| 3 |
+
schema_bundle(
|
| 4 |
+
name = "public-schema-bundle",
|
| 5 |
+
testonly = True,
|
| 6 |
+
generator = "//codex-rs/app-server-protocol:schema-generator",
|
| 7 |
+
tags = ["manual"],
|
| 8 |
+
)
|
bazel/toolchains/prost/BUILD.bazel
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
load("@rules_rust//extensions/prost:defs.bzl", "rust_prost_toolchain")
|
| 2 |
+
load("@rules_rust//rust:defs.bzl", "rust_library_group")
|
| 3 |
+
|
| 4 |
+
package(default_visibility = ["//visibility:public"])
|
| 5 |
+
|
| 6 |
+
rust_library_group(
|
| 7 |
+
name = "prost_runtime",
|
| 8 |
+
deps = ["@crates//:prost"],
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
rust_library_group(
|
| 12 |
+
name = "tonic_runtime",
|
| 13 |
+
deps = [
|
| 14 |
+
":prost_runtime",
|
| 15 |
+
"@crates//:tonic",
|
| 16 |
+
"@crates//:tonic-prost",
|
| 17 |
+
],
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
rust_prost_toolchain(
|
| 21 |
+
name = "toolchain_impl",
|
| 22 |
+
compile_well_known_types = False,
|
| 23 |
+
prost_plugin = "@rules_rs//rs/private/prost:protoc-gen-prost__protoc-gen-prost",
|
| 24 |
+
prost_runtime = ":prost_runtime",
|
| 25 |
+
prost_types = "@crates//:prost-types-0.14.3",
|
| 26 |
+
tonic_plugin = "@rules_rs//rs/private/prost:protoc-gen-tonic__protoc-gen-tonic",
|
| 27 |
+
tonic_plugin_flag = "--plugin=protoc-gen-tonic=%s",
|
| 28 |
+
tonic_runtime = ":tonic_runtime",
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
toolchain(
|
| 32 |
+
name = "toolchain",
|
| 33 |
+
toolchain = ":toolchain_impl",
|
| 34 |
+
toolchain_type = "@rules_rust//extensions/prost:toolchain_type",
|
| 35 |
+
)
|
scripts/build_codex_package.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build a canonical Codex package directory and optional archive."""
|
| 3 |
+
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# Some developer environments set PYTHONSAFEPATH=1, which prevents Python from
|
| 9 |
+
# adding the script directory to sys.path. Add it explicitly so the local helper
|
| 10 |
+
# package remains importable when this executable is launched from any cwd.
|
| 11 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
| 12 |
+
|
| 13 |
+
from codex_package.cli import main
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
raise SystemExit(main())
|
scripts/build_winget_package.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Adapt an extracted signed Windows package to WinGet's existing root filenames."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def prepare_winget_package(package: Path) -> None:
|
| 10 |
+
metadata_path = package / "codex-package.json"
|
| 11 |
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
| 12 |
+
target = metadata["target"]
|
| 13 |
+
if target not in ("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"):
|
| 14 |
+
raise ValueError("WinGet requires a Windows package")
|
| 15 |
+
if metadata["entrypoint"] != "bin/codex.exe":
|
| 16 |
+
raise ValueError("WinGet requires the canonical Codex entrypoint")
|
| 17 |
+
entrypoint = f"codex-{target}.exe"
|
| 18 |
+
manifest_path = package / "codex-resources/voice/manifest.json"
|
| 19 |
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 20 |
+
manifest["sha256"][entrypoint] = manifest["sha256"].pop("bin/codex.exe")
|
| 21 |
+
(package / "bin/codex.exe").rename(package / entrypoint)
|
| 22 |
+
(package / "bin/codex-code-mode-host.exe").rename(
|
| 23 |
+
package / "codex-code-mode-host.exe"
|
| 24 |
+
)
|
| 25 |
+
# Keep resources in place for package-aware discovery and provide the root
|
| 26 |
+
# filenames declared by the existing WinGet portable installer manifest.
|
| 27 |
+
for helper in ("codex-command-runner.exe", "codex-windows-sandbox-setup.exe"):
|
| 28 |
+
shutil.copy2(package / "codex-resources" / helper, package / helper)
|
| 29 |
+
metadata["entrypoint"] = entrypoint
|
| 30 |
+
for path, value in ((metadata_path, metadata), (manifest_path, manifest)):
|
| 31 |
+
path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 36 |
+
parser.add_argument("package", type=Path)
|
| 37 |
+
prepare_winget_package(parser.parse_args().package)
|
scripts/check-module-bazel-lock.sh
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
| 5 |
+
if ! "${repo_root}/.github/scripts/run_bazel_with_buildbuddy.py" mod deps --lockfile_mode=error; then
|
| 6 |
+
echo "MODULE.bazel.lock is out of date."
|
| 7 |
+
echo "Run 'just bazel-lock-update' and commit the updated lockfile."
|
| 8 |
+
exit 1
|
| 9 |
+
fi
|
scripts/codex_package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Codex package builder
|
| 2 |
+
|
| 3 |
+
This package contains the implementation behind `scripts/build_codex_package.py`.
|
| 4 |
+
The top-level script is the stable executable entry point; these modules keep the
|
| 5 |
+
package-building logic split by responsibility.
|
| 6 |
+
|
| 7 |
+
Run the builder through `just`:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
just assemble-codex-package --help
|
| 11 |
+
just assemble-codex-package --variant codex-app-server
|
| 12 |
+
just assemble-codex-package --target x86_64-unknown-linux-gnu
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
The builder creates a canonical Codex package directory:
|
| 16 |
+
|
| 17 |
+
```text
|
| 18 |
+
.
|
| 19 |
+
├── codex-package.json
|
| 20 |
+
├── bin
|
| 21 |
+
│ ├── <entrypoint>[.exe]
|
| 22 |
+
│ └── codex-code-mode-host[.exe]
|
| 23 |
+
├── codex-resources
|
| 24 |
+
│ ├── bwrap # Linux only
|
| 25 |
+
│ ├── zsh/bin/zsh # supported Unix targets only
|
| 26 |
+
│ ├── codex-command-runner.exe # Windows only
|
| 27 |
+
│ └── codex-windows-sandbox-setup.exe # Windows only
|
| 28 |
+
└── codex-path
|
| 29 |
+
└── rg[.exe]
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
The package directory is the primary artifact. Archive formats such as
|
| 33 |
+
`.tar.gz`, `.tar.zst`, and `.zip` are serializations of that directory.
|
| 34 |
+
|
| 35 |
+
If `--target` is omitted, the builder uses the release target for the current
|
| 36 |
+
host platform. On Linux, that default is a musl target to match Codex release
|
| 37 |
+
artifacts; pass a GNU Linux target explicitly for native glibc local builds. If
|
| 38 |
+
`--package-dir` is omitted, the builder creates a new temporary directory and
|
| 39 |
+
prints its path after the package is built.
|
| 40 |
+
|
| 41 |
+
The `--variant` flag selects the package entrypoint. Supported variants are
|
| 42 |
+
`codex` and `codex-app-server`. The `--package-version` flag sets the version in
|
| 43 |
+
`codex-package.json`; it defaults to `[workspace.package].version` in
|
| 44 |
+
`codex-rs/Cargo.toml`.
|
| 45 |
+
|
| 46 |
+
## Source-built artifacts
|
| 47 |
+
|
| 48 |
+
Artifacts built from this repository are built by the package builder in one
|
| 49 |
+
grouped `cargo build` command per package when they are needed and no prebuilt
|
| 50 |
+
override was provided:
|
| 51 |
+
|
| 52 |
+
- all targets: the selected entrypoint, unless `--entrypoint-bin` is provided
|
| 53 |
+
- all targets: `codex-code-mode-host`, unless `--code-mode-host-bin` is provided
|
| 54 |
+
- Linux targets: `bwrap`, unless `--bwrap-bin` is provided
|
| 55 |
+
- Windows targets: `codex-command-runner` and `codex-windows-sandbox-setup`,
|
| 56 |
+
unless the corresponding prebuilt helper flags are provided
|
| 57 |
+
|
| 58 |
+
The default cargo profile is `dev-small` because local iteration should favor
|
| 59 |
+
fast, small builds. Release jobs should pass `--cargo-profile release` and an
|
| 60 |
+
explicit target. Release jobs that already built and signed/notarized the
|
| 61 |
+
entrypoint should pass `--entrypoint-bin` so the package contains that exact
|
| 62 |
+
binary instead of rebuilding it.
|
| 63 |
+
|
| 64 |
+
Release jobs should likewise pass `--code-mode-host-bin` so the package contains
|
| 65 |
+
the signed host executable beside the signed entrypoint.
|
| 66 |
+
|
| 67 |
+
Release jobs that already built package resource binaries should also pass the
|
| 68 |
+
corresponding resource flags: `--bwrap-bin` for Linux packages, and
|
| 69 |
+
`--codex-command-runner-bin` plus `--codex-windows-sandbox-setup-bin` for
|
| 70 |
+
Windows packages. This keeps package archive creation as a pure staging step
|
| 71 |
+
after signing instead of rebuilding resources.
|
| 72 |
+
|
| 73 |
+
When the builder source-builds an entrypoint for a Darwin or Linux target, it
|
| 74 |
+
downloads and verifies the matching Codex-built V8 release pair before invoking
|
| 75 |
+
Cargo and sets `RUSTY_V8_ARCHIVE` plus `RUSTY_V8_SRC_BINDING_PATH` for that
|
| 76 |
+
build. Windows targets keep Cargo's release-build MSVC artifact path. Explicit
|
| 77 |
+
overrides remain authoritative when both variables are already set. Set
|
| 78 |
+
`V8_FROM_SOURCE=1` to leave the build with the `v8` crate source-build path.
|
| 79 |
+
|
| 80 |
+
`rg` is not built from this repository, so the builder fetches it from the
|
| 81 |
+
DotSlash manifest at `scripts/codex_package/rg`. Downloaded archives are cached
|
| 82 |
+
under `$TMPDIR/codex-package/<target>-rg` and are reused only after the recorded
|
| 83 |
+
size and SHA-256 digest have been verified. Pass `--rg-bin` to use a local
|
| 84 |
+
ripgrep executable instead.
|
| 85 |
+
|
| 86 |
+
The patched zsh fork used by `shell_zsh_fork` is fetched from the DotSlash
|
| 87 |
+
manifest at `scripts/codex_package/codex-zsh` when the selected target has a
|
| 88 |
+
matching prebuilt artifact. Downloaded archives are cached under
|
| 89 |
+
`$TMPDIR/codex-package/<target>-zsh` and installed at
|
| 90 |
+
`codex-resources/zsh/bin/zsh`. Pass `--zsh-bin` to package a prebuilt, signed
|
| 91 |
+
executable, or `--zsh-manifest` to use a different DotSlash manifest, such as
|
| 92 |
+
the manifest published with a standalone zsh artifact release.
|
scripts/codex_package/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Helpers for building canonical Codex package archives."""
|
scripts/codex_package/archive.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Archive writers for canonical Codex package directories."""
|
| 2 |
+
|
| 3 |
+
import shutil
|
| 4 |
+
import subprocess
|
| 5 |
+
import tarfile
|
| 6 |
+
import tempfile
|
| 7 |
+
import zipfile
|
| 8 |
+
from collections.abc import Callable
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from .targets import REPO_ROOT
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
ZSTD_DOTSLASH = REPO_ROOT / ".github" / "workflows" / "zstd"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def write_archive(package_dir: Path, archive_path: Path, *, force: bool) -> None:
|
| 18 |
+
if is_relative_to(archive_path, package_dir):
|
| 19 |
+
raise RuntimeError(
|
| 20 |
+
f"Archive output must be outside the package directory: {archive_path}"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
if archive_path.exists():
|
| 25 |
+
if not force:
|
| 26 |
+
raise RuntimeError(f"Archive output already exists: {archive_path}")
|
| 27 |
+
archive_path.unlink()
|
| 28 |
+
|
| 29 |
+
archive_format = archive_format_for_path(archive_path)
|
| 30 |
+
if archive_format == "tar.gz":
|
| 31 |
+
write_tar_archive(package_dir, archive_path, mode="w:gz")
|
| 32 |
+
elif archive_format == "tar.zst":
|
| 33 |
+
write_tar_zst_archive(package_dir, archive_path)
|
| 34 |
+
elif archive_format == "zip":
|
| 35 |
+
write_zip_archive(package_dir, archive_path)
|
| 36 |
+
else:
|
| 37 |
+
raise AssertionError(f"unexpected archive format: {archive_format}")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def is_relative_to(path: Path, parent: Path) -> bool:
|
| 41 |
+
try:
|
| 42 |
+
path.relative_to(parent)
|
| 43 |
+
return True
|
| 44 |
+
except ValueError:
|
| 45 |
+
return False
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def archive_format_for_path(path: Path) -> str:
|
| 49 |
+
suffixes = path.suffixes
|
| 50 |
+
if suffixes[-2:] == [".tar", ".gz"] or path.suffix == ".tgz":
|
| 51 |
+
return "tar.gz"
|
| 52 |
+
if suffixes[-2:] == [".tar", ".zst"]:
|
| 53 |
+
return "tar.zst"
|
| 54 |
+
if path.suffix == ".zip":
|
| 55 |
+
return "zip"
|
| 56 |
+
raise RuntimeError(
|
| 57 |
+
f"Unsupported archive suffix for {path}. Use .tar.gz, .tgz, .tar.zst, or .zip."
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def write_tar_archive(package_dir: Path, archive_path: Path, *, mode: str) -> None:
|
| 62 |
+
options = {"compresslevel": 6} if mode == "w:gz" else {}
|
| 63 |
+
with tarfile.open(archive_path, mode, **options) as archive:
|
| 64 |
+
for path in package_entries(package_dir):
|
| 65 |
+
archive.add(
|
| 66 |
+
path,
|
| 67 |
+
arcname=path.relative_to(package_dir),
|
| 68 |
+
recursive=False,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def write_tar_zst_archive(package_dir: Path, archive_path: Path) -> None:
|
| 73 |
+
zstd_command = resolve_zstd_command()
|
| 74 |
+
|
| 75 |
+
with tempfile.TemporaryDirectory(prefix="codex-package-archive-") as temp_dir_str:
|
| 76 |
+
tar_path = Path(temp_dir_str) / "package.tar"
|
| 77 |
+
write_tar_archive(package_dir, tar_path, mode="w")
|
| 78 |
+
subprocess.check_call(
|
| 79 |
+
[*zstd_command, "-T0", "-19", "-f", str(tar_path), "-o", str(archive_path)]
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def resolve_zstd_command(
|
| 84 |
+
*,
|
| 85 |
+
dotslash_manifest: Path = ZSTD_DOTSLASH,
|
| 86 |
+
which: Callable[[str], str | None] = shutil.which,
|
| 87 |
+
) -> list[str]:
|
| 88 |
+
zstd = which("zstd")
|
| 89 |
+
if zstd is not None:
|
| 90 |
+
return [zstd]
|
| 91 |
+
|
| 92 |
+
dotslash = which("dotslash")
|
| 93 |
+
if dotslash is not None and dotslash_manifest.is_file():
|
| 94 |
+
return [dotslash, str(dotslash_manifest)]
|
| 95 |
+
|
| 96 |
+
raise RuntimeError(
|
| 97 |
+
"zstd is required to write .tar.zst archives. Install zstd, or install "
|
| 98 |
+
f"DotSlash so the repository wrapper can run: {dotslash_manifest}"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def write_zip_archive(package_dir: Path, archive_path: Path) -> None:
|
| 103 |
+
with zipfile.ZipFile(
|
| 104 |
+
archive_path, "w", compression=zipfile.ZIP_DEFLATED
|
| 105 |
+
) as archive:
|
| 106 |
+
for path in package_entries(package_dir):
|
| 107 |
+
relative_path = path.relative_to(package_dir)
|
| 108 |
+
if path.is_dir():
|
| 109 |
+
archive.write(path, f"{relative_path}/")
|
| 110 |
+
else:
|
| 111 |
+
archive.write(path, relative_path)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def package_entries(package_dir: Path) -> list[Path]:
|
| 115 |
+
return sorted(
|
| 116 |
+
package_dir.rglob("*"),
|
| 117 |
+
key=lambda path: path.relative_to(package_dir).as_posix(),
|
| 118 |
+
)
|
scripts/codex_package/cargo.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Cargo builds for source-built Codex package artifacts."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import subprocess
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from .targets import REPO_ROOT
|
| 9 |
+
from .targets import PackageVariant
|
| 10 |
+
from .targets import TargetSpec
|
| 11 |
+
from .v8 import resolve_codex_v8_cargo_env
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
CODEX_RS_ROOT = REPO_ROOT / "codex-rs"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass(frozen=True)
|
| 18 |
+
class SourceBuildOutputs:
|
| 19 |
+
entrypoint_bin: Path
|
| 20 |
+
code_mode_host_bin: Path
|
| 21 |
+
bwrap_bin: Path | None
|
| 22 |
+
codex_command_runner_bin: Path | None
|
| 23 |
+
codex_windows_sandbox_setup_bin: Path | None
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def build_source_binaries(
|
| 27 |
+
spec: TargetSpec,
|
| 28 |
+
variant: PackageVariant,
|
| 29 |
+
*,
|
| 30 |
+
cargo: str,
|
| 31 |
+
profile: str,
|
| 32 |
+
entrypoint_bin: Path | None,
|
| 33 |
+
code_mode_host_bin: Path | None,
|
| 34 |
+
bwrap_bin: Path | None,
|
| 35 |
+
codex_command_runner_bin: Path | None,
|
| 36 |
+
codex_windows_sandbox_setup_bin: Path | None,
|
| 37 |
+
) -> SourceBuildOutputs:
|
| 38 |
+
validate_prebuilt_resource_inputs(
|
| 39 |
+
spec,
|
| 40 |
+
bwrap_bin=bwrap_bin,
|
| 41 |
+
codex_command_runner_bin=codex_command_runner_bin,
|
| 42 |
+
codex_windows_sandbox_setup_bin=codex_windows_sandbox_setup_bin,
|
| 43 |
+
)
|
| 44 |
+
binaries = source_binaries_for_target(
|
| 45 |
+
spec,
|
| 46 |
+
variant,
|
| 47 |
+
build_entrypoint=entrypoint_bin is None,
|
| 48 |
+
build_code_mode_host=code_mode_host_bin is None,
|
| 49 |
+
build_bwrap=spec.is_linux and bwrap_bin is None,
|
| 50 |
+
build_codex_command_runner=spec.is_windows and codex_command_runner_bin is None,
|
| 51 |
+
build_codex_windows_sandbox_setup=spec.is_windows
|
| 52 |
+
and codex_windows_sandbox_setup_bin is None,
|
| 53 |
+
)
|
| 54 |
+
if binaries:
|
| 55 |
+
cmd = [
|
| 56 |
+
cargo,
|
| 57 |
+
"build",
|
| 58 |
+
"--target",
|
| 59 |
+
spec.target,
|
| 60 |
+
"--profile",
|
| 61 |
+
profile,
|
| 62 |
+
]
|
| 63 |
+
for binary in binaries:
|
| 64 |
+
cmd.extend(["--bin", binary])
|
| 65 |
+
|
| 66 |
+
cargo_env = None
|
| 67 |
+
if entrypoint_bin is None or code_mode_host_bin is None:
|
| 68 |
+
codex_v8_env = resolve_codex_v8_cargo_env(spec)
|
| 69 |
+
if codex_v8_env:
|
| 70 |
+
cargo_env = {**os.environ, **codex_v8_env}
|
| 71 |
+
|
| 72 |
+
print("+", " ".join(cmd))
|
| 73 |
+
subprocess.run(
|
| 74 |
+
cmd,
|
| 75 |
+
cwd=CODEX_RS_ROOT,
|
| 76 |
+
check=True,
|
| 77 |
+
env=cargo_env,
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
output_dir = cargo_profile_output_dir(spec, profile)
|
| 81 |
+
outputs = SourceBuildOutputs(
|
| 82 |
+
entrypoint_bin=resolve_output_path(
|
| 83 |
+
entrypoint_bin,
|
| 84 |
+
output_dir / variant.entrypoint_name(spec),
|
| 85 |
+
),
|
| 86 |
+
code_mode_host_bin=(
|
| 87 |
+
code_mode_host_bin.resolve()
|
| 88 |
+
if code_mode_host_bin is not None
|
| 89 |
+
else output_dir / f"codex-code-mode-host{spec.exe_suffix}"
|
| 90 |
+
),
|
| 91 |
+
bwrap_bin=resolve_output_path(
|
| 92 |
+
bwrap_bin,
|
| 93 |
+
output_dir / "bwrap" if spec.is_linux else None,
|
| 94 |
+
),
|
| 95 |
+
codex_command_runner_bin=resolve_output_path(
|
| 96 |
+
codex_command_runner_bin,
|
| 97 |
+
output_dir / "codex-command-runner.exe" if spec.is_windows else None,
|
| 98 |
+
),
|
| 99 |
+
codex_windows_sandbox_setup_bin=resolve_output_path(
|
| 100 |
+
codex_windows_sandbox_setup_bin,
|
| 101 |
+
output_dir / "codex-windows-sandbox-setup.exe" if spec.is_windows else None,
|
| 102 |
+
),
|
| 103 |
+
)
|
| 104 |
+
validate_source_outputs(outputs)
|
| 105 |
+
return outputs
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def source_binaries_for_target(
|
| 109 |
+
spec: TargetSpec,
|
| 110 |
+
variant: PackageVariant,
|
| 111 |
+
*,
|
| 112 |
+
build_entrypoint: bool,
|
| 113 |
+
build_code_mode_host: bool,
|
| 114 |
+
build_bwrap: bool,
|
| 115 |
+
build_codex_command_runner: bool,
|
| 116 |
+
build_codex_windows_sandbox_setup: bool,
|
| 117 |
+
) -> list[str]:
|
| 118 |
+
binaries = []
|
| 119 |
+
if build_entrypoint:
|
| 120 |
+
binaries.append(variant.cargo_bin)
|
| 121 |
+
if build_code_mode_host:
|
| 122 |
+
binaries.append("codex-code-mode-host")
|
| 123 |
+
if build_bwrap:
|
| 124 |
+
binaries.append("bwrap")
|
| 125 |
+
if build_codex_command_runner:
|
| 126 |
+
binaries.append("codex-command-runner")
|
| 127 |
+
if build_codex_windows_sandbox_setup:
|
| 128 |
+
binaries.append("codex-windows-sandbox-setup")
|
| 129 |
+
return binaries
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def validate_prebuilt_resource_inputs(
|
| 133 |
+
spec: TargetSpec,
|
| 134 |
+
*,
|
| 135 |
+
bwrap_bin: Path | None,
|
| 136 |
+
codex_command_runner_bin: Path | None,
|
| 137 |
+
codex_windows_sandbox_setup_bin: Path | None,
|
| 138 |
+
) -> None:
|
| 139 |
+
if bwrap_bin is not None and not spec.is_linux:
|
| 140 |
+
raise RuntimeError("--bwrap-bin is only supported for Linux targets.")
|
| 141 |
+
if codex_command_runner_bin is not None and not spec.is_windows:
|
| 142 |
+
raise RuntimeError(
|
| 143 |
+
"--codex-command-runner-bin is only supported for Windows targets."
|
| 144 |
+
)
|
| 145 |
+
if codex_windows_sandbox_setup_bin is not None and not spec.is_windows:
|
| 146 |
+
raise RuntimeError(
|
| 147 |
+
"--codex-windows-sandbox-setup-bin is only supported for Windows targets."
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def resolve_output_path(
|
| 152 |
+
explicit_path: Path | None, default_path: Path | None
|
| 153 |
+
) -> Path | None:
|
| 154 |
+
if explicit_path is not None:
|
| 155 |
+
return explicit_path.resolve()
|
| 156 |
+
|
| 157 |
+
return default_path
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def cargo_profile_output_dir(spec: TargetSpec, profile: str) -> Path:
|
| 161 |
+
target_dir = cargo_target_dir()
|
| 162 |
+
return target_dir / spec.target / cargo_profile_dirname(profile)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def cargo_target_dir() -> Path:
|
| 166 |
+
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
| 167 |
+
if target_dir is None:
|
| 168 |
+
return CODEX_RS_ROOT / "target"
|
| 169 |
+
|
| 170 |
+
path = Path(target_dir)
|
| 171 |
+
if path.is_absolute():
|
| 172 |
+
return path
|
| 173 |
+
|
| 174 |
+
return CODEX_RS_ROOT / path
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def cargo_profile_dirname(profile: str) -> str:
|
| 178 |
+
if profile == "dev":
|
| 179 |
+
return "debug"
|
| 180 |
+
if profile == "release":
|
| 181 |
+
return "release"
|
| 182 |
+
return profile
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def validate_source_outputs(outputs: SourceBuildOutputs) -> None:
|
| 186 |
+
for path in [
|
| 187 |
+
outputs.entrypoint_bin,
|
| 188 |
+
outputs.code_mode_host_bin,
|
| 189 |
+
outputs.bwrap_bin,
|
| 190 |
+
outputs.codex_command_runner_bin,
|
| 191 |
+
outputs.codex_windows_sandbox_setup_bin,
|
| 192 |
+
]:
|
| 193 |
+
if path is not None and not path.is_file():
|
| 194 |
+
raise RuntimeError(f"cargo build did not produce expected binary: {path}")
|
scripts/codex_package/cli.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command-line interface for building Codex package directories."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import re
|
| 5 |
+
import tempfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from .archive import write_archive
|
| 9 |
+
from .cargo import build_source_binaries
|
| 10 |
+
from .layout import build_package_dir
|
| 11 |
+
from .layout import prepare_package_dir
|
| 12 |
+
from .layout import validate_package_dir
|
| 13 |
+
from .ripgrep import resolve_rg_bin
|
| 14 |
+
from .targets import PACKAGE_VARIANTS
|
| 15 |
+
from .targets import TARGET_SPECS
|
| 16 |
+
from .targets import PackageInputs
|
| 17 |
+
from .targets import default_target
|
| 18 |
+
from .targets import resolve_input_path
|
| 19 |
+
from .zsh import resolve_zsh_bin
|
| 20 |
+
from .version import read_workspace_version
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Release pipelines run this builder with system Python, so avoid new dependencies.
|
| 24 |
+
SEMVER_PATTERN = re.compile(
|
| 25 |
+
r"(?P<major>0|[1-9][0-9]*)\."
|
| 26 |
+
r"(?P<minor>0|[1-9][0-9]*)\."
|
| 27 |
+
r"(?P<patch>0|[1-9][0-9]*)"
|
| 28 |
+
r"(?:-(?P<prerelease>[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?"
|
| 29 |
+
r"(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def parse_package_version(value: str) -> str:
|
| 34 |
+
match = SEMVER_PATTERN.fullmatch(value)
|
| 35 |
+
if match is not None:
|
| 36 |
+
components = (match.group(name) for name in ("major", "minor", "patch"))
|
| 37 |
+
prerelease = match.group("prerelease") or ""
|
| 38 |
+
if all(int(component) <= 2**64 - 1 for component in components) and all(
|
| 39 |
+
not (part.isdigit() and len(part) > 1 and part.startswith("0"))
|
| 40 |
+
for part in prerelease.split(".")
|
| 41 |
+
):
|
| 42 |
+
return value
|
| 43 |
+
|
| 44 |
+
raise argparse.ArgumentTypeError(f"invalid semantic version: {value!r}")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def parse_args() -> argparse.Namespace:
|
| 48 |
+
parser = argparse.ArgumentParser(
|
| 49 |
+
description="Build a canonical Codex package directory and optional archive.",
|
| 50 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 51 |
+
)
|
| 52 |
+
parser.add_argument(
|
| 53 |
+
"--target",
|
| 54 |
+
default=argparse.SUPPRESS,
|
| 55 |
+
choices=sorted(TARGET_SPECS),
|
| 56 |
+
help=(
|
| 57 |
+
"Rust target triple for the package. Defaults to the release target "
|
| 58 |
+
"for this host platform."
|
| 59 |
+
),
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--variant",
|
| 63 |
+
choices=sorted(PACKAGE_VARIANTS),
|
| 64 |
+
default="codex",
|
| 65 |
+
help="Package variant to build.",
|
| 66 |
+
)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--package-version",
|
| 69 |
+
type=parse_package_version,
|
| 70 |
+
default=read_workspace_version(),
|
| 71 |
+
help="Semantic version to record in codex-package.json.",
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"--package-dir",
|
| 75 |
+
type=Path,
|
| 76 |
+
default=argparse.SUPPRESS,
|
| 77 |
+
help=(
|
| 78 |
+
"Output directory to create as the package root. Defaults to a new temporary directory."
|
| 79 |
+
),
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--archive-output",
|
| 83 |
+
type=Path,
|
| 84 |
+
action="append",
|
| 85 |
+
default=[],
|
| 86 |
+
help=(
|
| 87 |
+
"Optional archive output path. May be repeated. Supported suffixes: "
|
| 88 |
+
".tar.gz, .tgz, .tar.zst, .zip."
|
| 89 |
+
),
|
| 90 |
+
)
|
| 91 |
+
parser.add_argument(
|
| 92 |
+
"--force",
|
| 93 |
+
action="store_true",
|
| 94 |
+
help="Replace an existing package directory or archive output.",
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--cargo",
|
| 98 |
+
default="cargo",
|
| 99 |
+
help="Cargo executable to use for source-built package artifacts.",
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--cargo-profile",
|
| 103 |
+
default="dev-small",
|
| 104 |
+
help=(
|
| 105 |
+
"Cargo profile for source-built package artifacts. Use release for release packages."
|
| 106 |
+
),
|
| 107 |
+
)
|
| 108 |
+
parser.add_argument(
|
| 109 |
+
"--entrypoint-bin",
|
| 110 |
+
type=Path,
|
| 111 |
+
help=(
|
| 112 |
+
"Optional prebuilt entrypoint executable for the selected package "
|
| 113 |
+
"variant. If omitted, the entrypoint is built with Cargo."
|
| 114 |
+
),
|
| 115 |
+
)
|
| 116 |
+
parser.add_argument(
|
| 117 |
+
"--code-mode-host-bin",
|
| 118 |
+
type=Path,
|
| 119 |
+
help=(
|
| 120 |
+
"Optional prebuilt codex-code-mode-host executable. If omitted, "
|
| 121 |
+
"the host is built with Cargo."
|
| 122 |
+
),
|
| 123 |
+
)
|
| 124 |
+
parser.add_argument(
|
| 125 |
+
"--bwrap-bin",
|
| 126 |
+
type=Path,
|
| 127 |
+
help=(
|
| 128 |
+
"Optional prebuilt Linux bwrap executable. If omitted for Linux "
|
| 129 |
+
"targets, bwrap is built with Cargo."
|
| 130 |
+
),
|
| 131 |
+
)
|
| 132 |
+
zsh_source = parser.add_mutually_exclusive_group()
|
| 133 |
+
zsh_source.add_argument(
|
| 134 |
+
"--zsh-manifest",
|
| 135 |
+
type=Path,
|
| 136 |
+
help=(
|
| 137 |
+
"Optional DotSlash manifest for the patched zsh fork instead of "
|
| 138 |
+
"scripts/codex_package/codex-zsh."
|
| 139 |
+
),
|
| 140 |
+
)
|
| 141 |
+
zsh_source.add_argument(
|
| 142 |
+
"--zsh-bin",
|
| 143 |
+
type=Path,
|
| 144 |
+
help="Optional prebuilt zsh executable instead of fetching from a manifest.",
|
| 145 |
+
)
|
| 146 |
+
parser.add_argument(
|
| 147 |
+
"--codex-command-runner-bin",
|
| 148 |
+
type=Path,
|
| 149 |
+
help=(
|
| 150 |
+
"Optional prebuilt Windows codex-command-runner.exe executable. "
|
| 151 |
+
"If omitted for Windows targets, codex-command-runner is built "
|
| 152 |
+
"with Cargo."
|
| 153 |
+
),
|
| 154 |
+
)
|
| 155 |
+
parser.add_argument(
|
| 156 |
+
"--codex-windows-sandbox-setup-bin",
|
| 157 |
+
type=Path,
|
| 158 |
+
help=(
|
| 159 |
+
"Optional prebuilt Windows codex-windows-sandbox-setup.exe "
|
| 160 |
+
"executable. If omitted for Windows targets, "
|
| 161 |
+
"codex-windows-sandbox-setup is built with Cargo."
|
| 162 |
+
),
|
| 163 |
+
)
|
| 164 |
+
parser.add_argument(
|
| 165 |
+
"--rg-bin",
|
| 166 |
+
type=Path,
|
| 167 |
+
help=(
|
| 168 |
+
"Optional local ripgrep executable override instead of fetching from "
|
| 169 |
+
"scripts/codex_package/rg."
|
| 170 |
+
),
|
| 171 |
+
)
|
| 172 |
+
return parser.parse_args()
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def main() -> int:
|
| 176 |
+
args = parse_args()
|
| 177 |
+
spec = TARGET_SPECS[getattr(args, "target", None) or default_target()]
|
| 178 |
+
variant = PACKAGE_VARIANTS[args.variant]
|
| 179 |
+
package_dir_arg = getattr(args, "package_dir", None)
|
| 180 |
+
package_dir = (
|
| 181 |
+
package_dir_arg.resolve()
|
| 182 |
+
if package_dir_arg is not None
|
| 183 |
+
else Path(tempfile.mkdtemp(prefix="codex-package-")).resolve()
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
source_outputs = build_source_binaries(
|
| 187 |
+
spec,
|
| 188 |
+
variant,
|
| 189 |
+
cargo=args.cargo,
|
| 190 |
+
profile=args.cargo_profile,
|
| 191 |
+
entrypoint_bin=resolve_optional_input_path(
|
| 192 |
+
args.entrypoint_bin,
|
| 193 |
+
"prebuilt entrypoint executable",
|
| 194 |
+
"--entrypoint-bin",
|
| 195 |
+
),
|
| 196 |
+
code_mode_host_bin=resolve_optional_input_path(
|
| 197 |
+
args.code_mode_host_bin,
|
| 198 |
+
"prebuilt code-mode host executable",
|
| 199 |
+
"--code-mode-host-bin",
|
| 200 |
+
),
|
| 201 |
+
bwrap_bin=resolve_optional_input_path(
|
| 202 |
+
args.bwrap_bin,
|
| 203 |
+
"prebuilt Linux bwrap executable",
|
| 204 |
+
"--bwrap-bin",
|
| 205 |
+
),
|
| 206 |
+
codex_command_runner_bin=resolve_optional_input_path(
|
| 207 |
+
args.codex_command_runner_bin,
|
| 208 |
+
"prebuilt Windows codex-command-runner.exe executable",
|
| 209 |
+
"--codex-command-runner-bin",
|
| 210 |
+
),
|
| 211 |
+
codex_windows_sandbox_setup_bin=resolve_optional_input_path(
|
| 212 |
+
args.codex_windows_sandbox_setup_bin,
|
| 213 |
+
"prebuilt Windows codex-windows-sandbox-setup.exe executable",
|
| 214 |
+
"--codex-windows-sandbox-setup-bin",
|
| 215 |
+
),
|
| 216 |
+
)
|
| 217 |
+
inputs = PackageInputs(
|
| 218 |
+
entrypoint_bin=source_outputs.entrypoint_bin,
|
| 219 |
+
code_mode_host_bin=source_outputs.code_mode_host_bin,
|
| 220 |
+
rg_bin=resolve_rg_bin(spec, args.rg_bin),
|
| 221 |
+
zsh_bin=resolve_zsh_bin(spec, args.zsh_manifest, zsh_bin=args.zsh_bin),
|
| 222 |
+
bwrap_bin=source_outputs.bwrap_bin,
|
| 223 |
+
codex_command_runner_bin=source_outputs.codex_command_runner_bin,
|
| 224 |
+
codex_windows_sandbox_setup_bin=source_outputs.codex_windows_sandbox_setup_bin,
|
| 225 |
+
)
|
| 226 |
+
prepare_package_dir(package_dir, force=args.force)
|
| 227 |
+
build_package_dir(package_dir, args.package_version, variant, spec, inputs)
|
| 228 |
+
validate_package_dir(
|
| 229 |
+
package_dir, variant, spec, include_zsh=inputs.zsh_bin is not None
|
| 230 |
+
)
|
| 231 |
+
|
| 232 |
+
for archive_output in args.archive_output:
|
| 233 |
+
archive_path = archive_output.resolve()
|
| 234 |
+
write_archive(package_dir, archive_path, force=args.force)
|
| 235 |
+
print(f"Built Codex package archive at {archive_path}")
|
| 236 |
+
|
| 237 |
+
print(f"Built Codex package directory at {package_dir}")
|
| 238 |
+
return 0
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def resolve_optional_input_path(
|
| 242 |
+
explicit_path: Path | None,
|
| 243 |
+
description: str,
|
| 244 |
+
flag_name: str,
|
| 245 |
+
) -> Path | None:
|
| 246 |
+
if explicit_path is None:
|
| 247 |
+
return None
|
| 248 |
+
|
| 249 |
+
return resolve_input_path(explicit_path, description, flag_name)
|
scripts/codex_package/codex-zsh
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env dotslash
|
| 2 |
+
|
| 3 |
+
{
|
| 4 |
+
"name": "codex-zsh",
|
| 5 |
+
"platforms": {
|
| 6 |
+
"macos-aarch64": {
|
| 7 |
+
"size": 358776,
|
| 8 |
+
"hash": "sha256",
|
| 9 |
+
"digest": "49dec9832379688c9090666694a3449502ac5ebd4d76b9ffde1d0999cd088205",
|
| 10 |
+
"format": "tar.gz",
|
| 11 |
+
"path": "codex-zsh/bin/zsh",
|
| 12 |
+
"providers": [
|
| 13 |
+
{
|
| 14 |
+
"url": "https://github.com/openai/codex/releases/download/rust-v0.134.0-alpha.3/codex-zsh-aarch64-apple-darwin.tar.gz"
|
| 15 |
+
}
|
| 16 |
+
]
|
| 17 |
+
},
|
| 18 |
+
"macos-x86_64": {
|
| 19 |
+
"size": 391161,
|
| 20 |
+
"hash": "sha256",
|
| 21 |
+
"digest": "0246fd4703bb540ae74f13ec739ca365ebd607df44a1f21e335eb7a421352923",
|
| 22 |
+
"format": "tar.gz",
|
| 23 |
+
"path": "codex-zsh/bin/zsh",
|
| 24 |
+
"providers": [
|
| 25 |
+
{
|
| 26 |
+
"url": "https://github.com/openai/codex/releases/download/rust-v0.134.0-alpha.3/codex-zsh-x86_64-apple-darwin.tar.gz"
|
| 27 |
+
}
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
"linux-x86_64": {
|
| 31 |
+
"size": 433412,
|
| 32 |
+
"hash": "sha256",
|
| 33 |
+
"digest": "e7f760998c9644448d2cb8a821084a0134d6820c14b284bd7f39d7f32262ad40",
|
| 34 |
+
"format": "tar.gz",
|
| 35 |
+
"path": "codex-zsh/bin/zsh",
|
| 36 |
+
"providers": [
|
| 37 |
+
{
|
| 38 |
+
"url": "https://github.com/openai/codex/releases/download/rust-v0.134.0-alpha.3/codex-zsh-x86_64-unknown-linux-musl.tar.gz"
|
| 39 |
+
}
|
| 40 |
+
]
|
| 41 |
+
},
|
| 42 |
+
"linux-aarch64": {
|
| 43 |
+
"size": 411655,
|
| 44 |
+
"hash": "sha256",
|
| 45 |
+
"digest": "8d50cff5dfabc97e37e1138513da022d9247a802c2ff996ab91654bf1892c7d5",
|
| 46 |
+
"format": "tar.gz",
|
| 47 |
+
"path": "codex-zsh/bin/zsh",
|
| 48 |
+
"providers": [
|
| 49 |
+
{
|
| 50 |
+
"url": "https://github.com/openai/codex/releases/download/rust-v0.134.0-alpha.3/codex-zsh-aarch64-unknown-linux-musl.tar.gz"
|
| 51 |
+
}
|
| 52 |
+
]
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
}
|
scripts/codex_package/dotslash.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fetch executable artifacts from checked-in DotSlash manifests."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
import stat
|
| 7 |
+
import tarfile
|
| 8 |
+
import tempfile
|
| 9 |
+
import zipfile
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from urllib.parse import urlparse
|
| 13 |
+
from urllib.request import urlopen
|
| 14 |
+
|
| 15 |
+
from .targets import TargetSpec
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
DOWNLOAD_TIMEOUT_SECS = 60
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class DotSlashArtifact:
|
| 23 |
+
size: int
|
| 24 |
+
digest: str
|
| 25 |
+
archive_format: str
|
| 26 |
+
archive_member: str
|
| 27 |
+
url: str
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def fetch_dotslash_executable(
|
| 31 |
+
spec: TargetSpec,
|
| 32 |
+
*,
|
| 33 |
+
manifest_path: Path,
|
| 34 |
+
artifact_label: str,
|
| 35 |
+
cache_key: str,
|
| 36 |
+
dest_name: str,
|
| 37 |
+
missing_ok: bool = False,
|
| 38 |
+
) -> Path | None:
|
| 39 |
+
artifact = artifact_for_target(
|
| 40 |
+
spec,
|
| 41 |
+
manifest_path,
|
| 42 |
+
artifact_label=artifact_label,
|
| 43 |
+
missing_ok=missing_ok,
|
| 44 |
+
)
|
| 45 |
+
if artifact is None:
|
| 46 |
+
return None
|
| 47 |
+
|
| 48 |
+
cache_dir = default_cache_root() / cache_key
|
| 49 |
+
archive_path = cache_dir / archive_filename(artifact.url)
|
| 50 |
+
|
| 51 |
+
if not archive_is_valid(archive_path, artifact, artifact_label):
|
| 52 |
+
download_archive(artifact.url, archive_path)
|
| 53 |
+
try:
|
| 54 |
+
verify_archive(archive_path, artifact, artifact_label)
|
| 55 |
+
except RuntimeError:
|
| 56 |
+
archive_path.unlink(missing_ok=True)
|
| 57 |
+
raise
|
| 58 |
+
|
| 59 |
+
dest = cache_dir / dest_name
|
| 60 |
+
extract_archive_member(archive_path, artifact, dest, artifact_label)
|
| 61 |
+
if not spec.is_windows:
|
| 62 |
+
mode = dest.stat().st_mode
|
| 63 |
+
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
| 64 |
+
return dest
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def artifact_for_target(
|
| 68 |
+
spec: TargetSpec,
|
| 69 |
+
manifest_path: Path,
|
| 70 |
+
*,
|
| 71 |
+
artifact_label: str,
|
| 72 |
+
missing_ok: bool = False,
|
| 73 |
+
) -> DotSlashArtifact | None:
|
| 74 |
+
manifest = load_manifest(manifest_path)
|
| 75 |
+
platform_info = manifest.get("platforms", {}).get(spec.dotslash_platform)
|
| 76 |
+
if platform_info is None:
|
| 77 |
+
if missing_ok:
|
| 78 |
+
return None
|
| 79 |
+
raise RuntimeError(
|
| 80 |
+
f"{artifact_label} manifest {manifest_path} is missing platform "
|
| 81 |
+
f"{spec.dotslash_platform!r}"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
providers = platform_info.get("providers")
|
| 85 |
+
if not providers:
|
| 86 |
+
raise RuntimeError(
|
| 87 |
+
f"{artifact_label} manifest {manifest_path} has no providers for "
|
| 88 |
+
f"{spec.dotslash_platform!r}"
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
hash_name = platform_info.get("hash")
|
| 92 |
+
if hash_name != "sha256":
|
| 93 |
+
raise RuntimeError(
|
| 94 |
+
f"Unsupported {artifact_label} hash {hash_name!r} for "
|
| 95 |
+
f"{spec.dotslash_platform!r}; expected sha256"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
return DotSlashArtifact(
|
| 99 |
+
size=int(platform_info["size"]),
|
| 100 |
+
digest=str(platform_info["digest"]),
|
| 101 |
+
archive_format=str(platform_info["format"]),
|
| 102 |
+
archive_member=str(platform_info["path"]),
|
| 103 |
+
url=str(providers[0]["url"]),
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def load_manifest(manifest_path: Path) -> dict:
|
| 108 |
+
text = manifest_path.read_text(encoding="utf-8")
|
| 109 |
+
if text.startswith("#!"):
|
| 110 |
+
text = "\n".join(text.splitlines()[1:])
|
| 111 |
+
return json.loads(text)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def default_cache_root() -> Path:
|
| 115 |
+
return Path(tempfile.gettempdir()) / "codex-package"
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def archive_filename(url: str) -> str:
|
| 119 |
+
filename = Path(urlparse(url).path).name
|
| 120 |
+
if not filename:
|
| 121 |
+
raise RuntimeError(f"Unable to determine archive filename from {url}")
|
| 122 |
+
return filename
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def archive_is_valid(
|
| 126 |
+
archive_path: Path,
|
| 127 |
+
artifact: DotSlashArtifact,
|
| 128 |
+
artifact_label: str,
|
| 129 |
+
) -> bool:
|
| 130 |
+
if not archive_path.is_file():
|
| 131 |
+
return False
|
| 132 |
+
try:
|
| 133 |
+
verify_archive(archive_path, artifact, artifact_label)
|
| 134 |
+
except RuntimeError:
|
| 135 |
+
archive_path.unlink(missing_ok=True)
|
| 136 |
+
return False
|
| 137 |
+
return True
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def verify_archive(
|
| 141 |
+
archive_path: Path,
|
| 142 |
+
artifact: DotSlashArtifact,
|
| 143 |
+
artifact_label: str,
|
| 144 |
+
) -> None:
|
| 145 |
+
actual_size = archive_path.stat().st_size
|
| 146 |
+
if actual_size != artifact.size:
|
| 147 |
+
raise RuntimeError(
|
| 148 |
+
f"{artifact_label} archive {archive_path} has size {actual_size}, "
|
| 149 |
+
f"expected {artifact.size}"
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
digest = hashlib.sha256()
|
| 153 |
+
with open(archive_path, "rb") as fh:
|
| 154 |
+
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
|
| 155 |
+
digest.update(chunk)
|
| 156 |
+
|
| 157 |
+
actual_digest = digest.hexdigest()
|
| 158 |
+
if actual_digest != artifact.digest:
|
| 159 |
+
raise RuntimeError(
|
| 160 |
+
f"{artifact_label} archive {archive_path} has sha256 {actual_digest}, "
|
| 161 |
+
f"expected {artifact.digest}"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def download_archive(url: str, archive_path: Path) -> None:
|
| 166 |
+
archive_path.parent.mkdir(parents=True, exist_ok=True)
|
| 167 |
+
temp_path = archive_path.with_suffix(f"{archive_path.suffix}.tmp")
|
| 168 |
+
temp_path.unlink(missing_ok=True)
|
| 169 |
+
try:
|
| 170 |
+
with urlopen(url, timeout=DOWNLOAD_TIMEOUT_SECS) as response:
|
| 171 |
+
with open(temp_path, "wb") as out:
|
| 172 |
+
shutil.copyfileobj(response, out)
|
| 173 |
+
temp_path.replace(archive_path)
|
| 174 |
+
finally:
|
| 175 |
+
temp_path.unlink(missing_ok=True)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def extract_archive_member(
|
| 179 |
+
archive_path: Path,
|
| 180 |
+
artifact: DotSlashArtifact,
|
| 181 |
+
dest: Path,
|
| 182 |
+
artifact_label: str,
|
| 183 |
+
) -> None:
|
| 184 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 185 |
+
dest.unlink(missing_ok=True)
|
| 186 |
+
|
| 187 |
+
if artifact.archive_format == "tar.gz":
|
| 188 |
+
with tarfile.open(archive_path, "r:gz") as archive:
|
| 189 |
+
try:
|
| 190 |
+
member = archive.getmember(artifact.archive_member)
|
| 191 |
+
except KeyError as exc:
|
| 192 |
+
raise RuntimeError(
|
| 193 |
+
f"{artifact_label} archive {archive_path} is missing "
|
| 194 |
+
f"{artifact.archive_member!r}"
|
| 195 |
+
) from exc
|
| 196 |
+
|
| 197 |
+
extracted = archive.extractfile(member)
|
| 198 |
+
if extracted is None:
|
| 199 |
+
raise RuntimeError(
|
| 200 |
+
f"{artifact_label} archive member {artifact.archive_member!r} is not a file"
|
| 201 |
+
)
|
| 202 |
+
with extracted, open(dest, "wb") as out:
|
| 203 |
+
shutil.copyfileobj(extracted, out)
|
| 204 |
+
return
|
| 205 |
+
|
| 206 |
+
if artifact.archive_format == "zip":
|
| 207 |
+
with zipfile.ZipFile(archive_path) as archive:
|
| 208 |
+
try:
|
| 209 |
+
with archive.open(artifact.archive_member) as extracted:
|
| 210 |
+
with open(dest, "wb") as out:
|
| 211 |
+
shutil.copyfileobj(extracted, out)
|
| 212 |
+
except KeyError as exc:
|
| 213 |
+
raise RuntimeError(
|
| 214 |
+
f"{artifact_label} archive {archive_path} is missing "
|
| 215 |
+
f"{artifact.archive_member!r}"
|
| 216 |
+
) from exc
|
| 217 |
+
return
|
| 218 |
+
|
| 219 |
+
raise RuntimeError(
|
| 220 |
+
f"Unsupported {artifact_label} archive format {artifact.archive_format!r}; "
|
| 221 |
+
"expected tar.gz or zip"
|
| 222 |
+
)
|
scripts/codex_package/layout.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical Codex package directory layout."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import shutil
|
| 5 |
+
import stat
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from .targets import PackageInputs
|
| 9 |
+
from .targets import PackageVariant
|
| 10 |
+
from .targets import TargetSpec
|
| 11 |
+
from .zsh import ZSH_RESOURCE_PATH
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
LAYOUT_VERSION = 1
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def prepare_package_dir(package_dir: Path, *, force: bool) -> None:
|
| 18 |
+
if package_dir.exists():
|
| 19 |
+
if not package_dir.is_dir():
|
| 20 |
+
raise RuntimeError(
|
| 21 |
+
f"Package output exists and is not a directory: {package_dir}"
|
| 22 |
+
)
|
| 23 |
+
if any(package_dir.iterdir()):
|
| 24 |
+
if not force:
|
| 25 |
+
raise RuntimeError(
|
| 26 |
+
f"Package output directory is not empty: {package_dir}. "
|
| 27 |
+
"Pass --force to replace it."
|
| 28 |
+
)
|
| 29 |
+
shutil.rmtree(package_dir)
|
| 30 |
+
|
| 31 |
+
package_dir.mkdir(parents=True, exist_ok=True)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def build_package_dir(
|
| 35 |
+
package_dir: Path,
|
| 36 |
+
version: str,
|
| 37 |
+
variant: PackageVariant,
|
| 38 |
+
spec: TargetSpec,
|
| 39 |
+
inputs: PackageInputs,
|
| 40 |
+
) -> None:
|
| 41 |
+
bin_dir = package_dir / "bin"
|
| 42 |
+
resources_dir = package_dir / "codex-resources"
|
| 43 |
+
path_dir = package_dir / "codex-path"
|
| 44 |
+
bin_dir.mkdir()
|
| 45 |
+
resources_dir.mkdir()
|
| 46 |
+
path_dir.mkdir()
|
| 47 |
+
|
| 48 |
+
entrypoint_name = variant.entrypoint_name(spec)
|
| 49 |
+
copy_executable(
|
| 50 |
+
inputs.entrypoint_bin,
|
| 51 |
+
bin_dir / entrypoint_name,
|
| 52 |
+
is_windows=spec.is_windows,
|
| 53 |
+
)
|
| 54 |
+
copy_executable(
|
| 55 |
+
inputs.code_mode_host_bin,
|
| 56 |
+
bin_dir / f"codex-code-mode-host{spec.exe_suffix}",
|
| 57 |
+
is_windows=spec.is_windows,
|
| 58 |
+
)
|
| 59 |
+
copy_executable(inputs.rg_bin, path_dir / spec.rg_name, is_windows=spec.is_windows)
|
| 60 |
+
|
| 61 |
+
if inputs.zsh_bin is not None:
|
| 62 |
+
copy_executable(
|
| 63 |
+
inputs.zsh_bin,
|
| 64 |
+
resources_dir / ZSH_RESOURCE_PATH,
|
| 65 |
+
is_windows=False,
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
if inputs.bwrap_bin is not None:
|
| 69 |
+
copy_executable(inputs.bwrap_bin, resources_dir / "bwrap", is_windows=False)
|
| 70 |
+
|
| 71 |
+
if inputs.codex_command_runner_bin is not None:
|
| 72 |
+
copy_executable(
|
| 73 |
+
inputs.codex_command_runner_bin,
|
| 74 |
+
resources_dir / "codex-command-runner.exe",
|
| 75 |
+
is_windows=True,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
if inputs.codex_windows_sandbox_setup_bin is not None:
|
| 79 |
+
copy_executable(
|
| 80 |
+
inputs.codex_windows_sandbox_setup_bin,
|
| 81 |
+
resources_dir / "codex-windows-sandbox-setup.exe",
|
| 82 |
+
is_windows=True,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
metadata = {
|
| 86 |
+
"layoutVersion": LAYOUT_VERSION,
|
| 87 |
+
"version": version,
|
| 88 |
+
"target": spec.target,
|
| 89 |
+
"variant": variant.name,
|
| 90 |
+
"entrypoint": f"bin/{entrypoint_name}",
|
| 91 |
+
"resourcesDir": "codex-resources",
|
| 92 |
+
"pathDir": "codex-path",
|
| 93 |
+
}
|
| 94 |
+
write_json(package_dir / "codex-package.json", metadata)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def validate_package_dir(
|
| 98 |
+
package_dir: Path,
|
| 99 |
+
variant: PackageVariant,
|
| 100 |
+
spec: TargetSpec,
|
| 101 |
+
*,
|
| 102 |
+
include_zsh: bool,
|
| 103 |
+
) -> None:
|
| 104 |
+
required_dirs = [
|
| 105 |
+
Path("bin"),
|
| 106 |
+
Path("codex-resources"),
|
| 107 |
+
Path("codex-path"),
|
| 108 |
+
]
|
| 109 |
+
for relative_dir in required_dirs:
|
| 110 |
+
path = package_dir / relative_dir
|
| 111 |
+
if not path.is_dir():
|
| 112 |
+
raise RuntimeError(f"Missing package directory: {relative_dir}")
|
| 113 |
+
|
| 114 |
+
metadata_path = package_dir / "codex-package.json"
|
| 115 |
+
if not metadata_path.is_file():
|
| 116 |
+
raise RuntimeError("Missing package metadata: codex-package.json")
|
| 117 |
+
|
| 118 |
+
with open(metadata_path, encoding="utf-8") as fh:
|
| 119 |
+
metadata = json.load(fh)
|
| 120 |
+
|
| 121 |
+
expected_metadata = {
|
| 122 |
+
"layoutVersion": LAYOUT_VERSION,
|
| 123 |
+
"target": spec.target,
|
| 124 |
+
"variant": variant.name,
|
| 125 |
+
"entrypoint": f"bin/{variant.entrypoint_name(spec)}",
|
| 126 |
+
"resourcesDir": "codex-resources",
|
| 127 |
+
"pathDir": "codex-path",
|
| 128 |
+
}
|
| 129 |
+
for key, expected in expected_metadata.items():
|
| 130 |
+
actual = metadata.get(key)
|
| 131 |
+
if actual != expected:
|
| 132 |
+
raise RuntimeError(
|
| 133 |
+
f"Invalid package metadata field {key!r}: expected {expected!r}, got {actual!r}"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
required_files = [
|
| 137 |
+
Path("bin") / variant.entrypoint_name(spec),
|
| 138 |
+
Path("bin") / f"codex-code-mode-host{spec.exe_suffix}",
|
| 139 |
+
Path("codex-path") / spec.rg_name,
|
| 140 |
+
]
|
| 141 |
+
executable_files = list(required_files)
|
| 142 |
+
|
| 143 |
+
if include_zsh:
|
| 144 |
+
zsh_path = Path("codex-resources") / ZSH_RESOURCE_PATH
|
| 145 |
+
required_files.append(zsh_path)
|
| 146 |
+
executable_files.append(zsh_path)
|
| 147 |
+
|
| 148 |
+
if spec.is_linux:
|
| 149 |
+
required_files.append(Path("codex-resources") / "bwrap")
|
| 150 |
+
executable_files.append(Path("codex-resources") / "bwrap")
|
| 151 |
+
|
| 152 |
+
if spec.is_windows:
|
| 153 |
+
required_files.extend(
|
| 154 |
+
[
|
| 155 |
+
Path("codex-resources") / "codex-command-runner.exe",
|
| 156 |
+
Path("codex-resources") / "codex-windows-sandbox-setup.exe",
|
| 157 |
+
]
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
for relative_file in required_files:
|
| 161 |
+
path = package_dir / relative_file
|
| 162 |
+
if not path.is_file():
|
| 163 |
+
raise RuntimeError(f"Missing package file: {relative_file}")
|
| 164 |
+
|
| 165 |
+
if not spec.is_windows:
|
| 166 |
+
for relative_file in executable_files:
|
| 167 |
+
path = package_dir / relative_file
|
| 168 |
+
if not is_executable(path):
|
| 169 |
+
raise RuntimeError(f"Package file is not executable: {relative_file}")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def copy_executable(src: Path, dest: Path, *, is_windows: bool) -> None:
|
| 173 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 174 |
+
shutil.copyfile(src, dest)
|
| 175 |
+
if not is_windows:
|
| 176 |
+
mode = dest.stat().st_mode
|
| 177 |
+
dest.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def write_json(path: Path, value: object) -> None:
|
| 181 |
+
with open(path, "w", encoding="utf-8") as out:
|
| 182 |
+
json.dump(value, out, indent=2)
|
| 183 |
+
out.write("\n")
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def is_executable(path: Path) -> bool:
|
| 187 |
+
return bool(path.stat().st_mode & stat.S_IXUSR)
|