SaylorTwift HF Staff commited on
Commit
2569bec
·
verified ·
1 Parent(s): 2ee40b0

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/CODEOWNERS +17 -0
  2. .github/blob-size-allowlist.txt +11 -0
  3. .github/codex/home/config.toml +3 -0
  4. .github/codex/labels/codex-attempt.md +9 -0
  5. .github/codex/labels/codex-review.md +7 -0
  6. .github/codex/labels/codex-rust-review.md +139 -0
  7. .github/dependabot.yaml +36 -0
  8. .github/dotslash-argument-comment-lint-config.json +24 -0
  9. .github/dotslash-config.json +136 -0
  10. .github/dotslash-provisioned-config.json +16 -0
  11. .github/dotslash-zsh-config.json +32 -0
  12. .github/workflows/cargo-deny.yml +31 -0
  13. .github/workflows/python-runtime-release.yml +63 -0
  14. .vscode/extensions.json +13 -0
  15. .vscode/launch.json +22 -0
  16. .vscode/settings.json +27 -0
  17. bazel/build-info/BUILD.bazel +12 -0
  18. bazel/extensions/BUILD.bazel +3 -0
  19. bazel/extensions/codex_release_archive.bzl +76 -0
  20. bazel/modules/BUILD.bazel +1 -0
  21. bazel/modules/wine.MODULE.bazel +26 -0
  22. bazel/platforms/BUILD.bazel +1 -0
  23. bazel/platforms/release_binaries.bzl +30 -0
  24. bazel/rules/BUILD.bazel +6 -0
  25. bazel/rules/e2e_benchmark.bzl +56 -0
  26. bazel/rules/schema_bundle.bzl +39 -0
  27. bazel/rules/testing/BUILD.bazel +5 -0
  28. bazel/rules/testing/compat/BUILD.bazel +31 -0
  29. bazel/rules/testing/compat/exec_server_compat_test.bzl +47 -0
  30. bazel/rules/testing/compat/exec_server_compat_test.rs +379 -0
  31. bazel/rules/testing/foreign_platform_binary.bzl +53 -0
  32. bazel/rules/testing/wine/BUILD.bazel +54 -0
  33. bazel/rules/testing/wine/fixtures/windows_smoke.rs +15 -0
  34. bazel/rules/testing/wine/src/lib.rs +402 -0
  35. bazel/rules/testing/wine/src/lib_tests.rs +451 -0
  36. bazel/rules/testing/wine/wine.bzl +69 -0
  37. bazel/rules/testing/wine/wine_runtime.bzl +40 -0
  38. bazel/schema/BUILD.bazel +8 -0
  39. bazel/toolchains/prost/BUILD.bazel +35 -0
  40. scripts/build_codex_package.py +16 -0
  41. scripts/build_winget_package.py +37 -0
  42. scripts/check-module-bazel-lock.sh +9 -0
  43. scripts/codex_package/README.md +92 -0
  44. scripts/codex_package/__init__.py +1 -0
  45. scripts/codex_package/archive.py +118 -0
  46. scripts/codex_package/cargo.py +194 -0
  47. scripts/codex_package/cli.py +249 -0
  48. scripts/codex_package/codex-zsh +55 -0
  49. scripts/codex_package/dotslash.py +222 -0
  50. 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(&current, &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, &current).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(&registry)
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(&registry)
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(&registry).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(&registry)
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, &registry_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)