Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
- name: Setup Zig
uses: Loongphy/setup-zig@node24-lts-runtime
with:
version: 0.16.0
version: 0.17.0

- name: Zig test
if: ${{ matrix.os != 'windows-latest' }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/preview-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ jobs:
- name: Setup Zig
uses: Loongphy/setup-zig@node24-lts-runtime
with:
version: 0.16.0
version: 0.17.0
cache-key: ${{ matrix.zig_target }}

- name: Build binary
run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseSafe
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
- name: Setup Zig
uses: Loongphy/setup-zig@node24-lts-runtime
with:
version: 0.16.0
version: 0.17.0

- name: Zig test
if: ${{ matrix.os != 'windows-latest' }}
Expand Down Expand Up @@ -157,7 +157,8 @@ jobs:
- name: Setup Zig
uses: Loongphy/setup-zig@node24-lts-runtime
with:
version: 0.16.0
version: 0.17.0
cache-key: ${{ matrix.zig_target }}

- name: Setup Node
uses: actions/setup-node@v6
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ After modifying any `.zig` file, always run `zig build run -- list` to verify th
- Use the paths reported by `zig env` as the source of truth, especially `std_dir` for the standard library and `lib_dir` for other bundled Zig libraries.
- Prefer evidence from local sources: symbol definitions, nearby tests, and existing call sites in this repository.
- If the needed behavior is not clear from `std_dir`, inspect other Zig sources and tests under the local `lib_dir` tree as needed.

# Zig 0.17 Runtime and Tests

- CLI startup borrows `std.process.Init` resources and returns an exit code so standard-library cleanup runs. Do not create a second global I/O runtime or read the libc environment directly.
- Clone the startup environment map before changing a child process's environment. Test code falls back to the test runner's per-test I/O and environment.
- Use `std.Io.Dir.path` rather than deprecated `std.fs.path`. C time bindings are generated by the build's `TranslateC` module.
- Use `zig build test -Dtest-filter=<substring>` for focused checks and `zig build test test-helpers` for the full suite. Repeated `-Dtest-filter` flags match multiple substrings.
- CLI-output tests must capture a subprocess rather than write to the unit test runner's stdout, which carries Zig's `--listen=-` protocol.
- Run the required `zig build run -- list` check with isolated temporary `HOME` and `CODEX_HOME` directories to avoid modifying real account data.
45 changes: 32 additions & 13 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,33 @@ const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const package_module = b.createModule(.{
const test_filters = b.option([]const []const u8, "test-filter", "Only run tests matching these substrings") orelse &.{};
const c_time_translate = b.addTranslateC(.{
.root_source_file = b.path("src/c_time.h"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
const c_time_module = c_time_translate.createModule();

const package_module = b.addModule("codex_auth", .{
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "c_time", .module = c_time_module },
},
});

const main_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "codex_auth", .module = package_module },
},
});
const exe = b.addExecutable(.{
.name = "codex-auth",
Expand Down Expand Up @@ -47,14 +62,6 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
.link_libc = true,
.imports = &.{
.{ .name = "app_runtime", .module = b.createModule(.{
.root_source_file = b.path("src/core/runtime.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
}) },
},
});
const fake_codex_exe = b.addExecutable(.{
.name = "fake-codex",
Expand All @@ -68,9 +75,7 @@ pub fn build(b: *std.Build) void {
test_helpers_step.dependOn(&install_fake_codex.step);

const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
run_cmd.addPassthruArgs();
const run_step = b.step("run", "Run codex-auth");
run_step.dependOn(&run_cmd.step);

Expand All @@ -97,6 +102,10 @@ pub fn build(b: *std.Build) void {
"tests/workflows_live_test.zig",
};

const test_options = b.addOptions();
test_options.addOptionPathUntracked("zig_exe", .zig_exe);
test_options.addOptionPathUntracked("project_root", b.path("."));

const test_step = b.step("test", "Run tests");
for (test_files) |test_file| {
const test_module = b.createModule(.{
Expand All @@ -108,11 +117,21 @@ pub fn build(b: *std.Build) void {
.{ .name = "codex_auth", .module = package_module },
},
});
test_module.addOptions("test_options", test_options);
const test_artifact = b.addTest(.{
.root_module = test_module,
.filters = test_filters,
});
const run_test = b.addRunArtifact(test_artifact);
run_test.setEnvironmentVariable("CODEX_AUTH_CLI_INTEGRATION_PROJECT_ROOT", b.pathFromRoot("."));
run_test.setCwd(b.path("."));
if (std.mem.eql(u8, test_file, "tests/cli_integration_test.zig")) {
run_test.addFileInput(exe.getEmittedBin());
run_test.addFileInput(fake_curl_exe.getEmittedBin());
run_test.addFileInput(fake_curl_fail_exe.getEmittedBin());
run_test.addFileInput(fake_codex_exe.getEmittedBin());
run_test.addFileInput(b.path("build.zig"));
run_test.addFileInput(b.path("build.zig.zon"));
}
test_step.dependOn(&run_test.step);
}
}
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .codex_auth,
.version = "0.3.0-alpha.2",
.fingerprint = 0x5307ec8012c039c4, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.minimum_zig_version = "0.17.0",
.dependencies = .{},
.paths = .{
"build.zig",
Expand Down
2 changes: 1 addition & 1 deletion docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ This document describes the repository's CI, preview package publishing, and tag

- Branch and pull request validation runs in `.github/workflows/ci.yml`.
- The `build-test` matrix runs on `ubuntu-latest`, `macos-latest`, and `windows-latest`.
- CI installs Zig [`0.16.0`](https://ziglang.org/download/) and runs `zig build test`; if Zig is missing locally, download the matching `0.16.0` build from that page.
- CI installs Zig [`0.17.0`](https://ziglang.org/download/) and runs `zig build test`; if Zig is missing locally, download the matching `0.17.0` build from that page.

## Preview Packages for Pull Requests

Expand Down
17 changes: 13 additions & 4 deletions src/api/http_child.zig
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,14 @@ pub fn runChildCaptureWithInputAndOutputLimit(
defer std.heap.page_allocator.free(stdout);
const stderr = try multi_reader.toOwnedSlice(1);
defer std.heap.page_allocator.free(stderr);
const owned_stdout = try allocator.dupe(u8, stdout);
errdefer allocator.free(owned_stdout);
const owned_stderr = try allocator.dupe(u8, stderr);
errdefer allocator.free(owned_stderr);
return .{
.term = .{ .unknown = 0 },
.stdout = try allocator.dupe(u8, stdout),
.stderr = try allocator.dupe(u8, stderr),
.stdout = owned_stdout,
.stderr = owned_stderr,
.timed_out = true,
};
},
Expand All @@ -99,10 +103,15 @@ pub fn runChildCaptureWithInputAndOutputLimit(
const stderr = try multi_reader.toOwnedSlice(1);
defer std.heap.page_allocator.free(stderr);

const owned_stdout = try allocator.dupe(u8, stdout);
errdefer allocator.free(owned_stdout);
const owned_stderr = try allocator.dupe(u8, stderr);
errdefer allocator.free(owned_stderr);

return .{
.term = term,
.stdout = try allocator.dupe(u8, stdout),
.stderr = try allocator.dupe(u8, stderr),
.stdout = owned_stdout,
.stderr = owned_stderr,
.timed_out = false,
};
}
Expand Down
29 changes: 19 additions & 10 deletions src/api/http_curl.zig
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fn runCurlBearerGetJsonCommand(
endpoint: []const u8,
access_token: []const u8,
) !HttpResult {
const authorization = try std.fmt.allocPrint(allocator, "Authorization: Bearer {s}", .{access_token});
const authorization = try allocator.print("Authorization: Bearer {s}", .{access_token});
defer allocator.free(authorization);

return try runCurlJsonCommand(allocator, endpoint, &[_][]const u8{authorization});
Expand All @@ -141,9 +141,9 @@ fn runCurlGetJsonCommandWithExecutable(
access_token: []const u8,
account_id: []const u8,
) !HttpResult {
const authorization = try std.fmt.allocPrint(allocator, "Authorization: Bearer {s}", .{access_token});
const authorization = try allocator.print("Authorization: Bearer {s}", .{access_token});
defer allocator.free(authorization);
const account_header = try std.fmt.allocPrint(allocator, "ChatGPT-Account-Id: {s}", .{account_id});
const account_header = try allocator.print("ChatGPT-Account-Id: {s}", .{account_id});
defer allocator.free(account_header);

return try runCurlJsonCommandWithExecutable(allocator, curl_executable, endpoint, &[_][]const u8{ authorization, account_header });
Expand Down Expand Up @@ -233,6 +233,7 @@ fn runCurlGetJsonBatchCommand(
if (requests.len == 0) return .{ .items = items };

const curl_executable = resolveCurlExecutableForLaunchAlloc(allocator) catch |err| {
if (err == error.Canceled) return error.Canceled;
switch (err) {
error.OutOfMemory => return err,
else => {
Expand All @@ -245,7 +246,7 @@ fn runCurlGetJsonBatchCommand(

const worker_count = @min(requests.len, @max(@as(usize, 1), max_concurrency));
if (worker_count <= 1) {
runCurlGetJsonBatchSerially(allocator, curl_executable, endpoint, requests, items);
try runCurlGetJsonBatchSerially(allocator, curl_executable, endpoint, requests, items);
return .{ .items = items };
}

Expand Down Expand Up @@ -276,6 +277,7 @@ fn runCurlGetJsonBatchCommand(
}

queue.run();
if (queue.canceled.load(.monotonic)) return error.Canceled;
return .{ .items = items };
}

Expand All @@ -286,12 +288,18 @@ const CurlBatchWorkerQueue = struct {
requests: []const BatchRequest,
items: []BatchItemResult,
next_index: std.atomic.Value(usize) = .init(0),
canceled: std.atomic.Value(bool) = .init(false),

fn run(self: *CurlBatchWorkerQueue) void {
while (true) {
while (!self.canceled.load(.monotonic)) {
const idx = self.next_index.fetchAdd(1, .monotonic);
if (idx >= self.requests.len) return;
runCurlBatchItem(self.allocator, self.curl_executable, self.endpoint, self.requests[idx], &self.items[idx]);
runCurlBatchItem(self.allocator, self.curl_executable, self.endpoint, self.requests[idx], &self.items[idx]) catch |err| switch (err) {
error.Canceled => {
self.canceled.store(true, .monotonic);
return;
},
};
}
}
};
Expand All @@ -302,9 +310,9 @@ fn runCurlGetJsonBatchSerially(
endpoint: []const u8,
requests: []const BatchRequest,
items: []BatchItemResult,
) void {
) error{Canceled}!void {
for (requests, 0..) |request, idx| {
runCurlBatchItem(allocator, curl_executable, endpoint, request, &items[idx]);
try runCurlBatchItem(allocator, curl_executable, endpoint, request, &items[idx]);
}
}

Expand All @@ -314,14 +322,15 @@ fn runCurlBatchItem(
endpoint: []const u8,
request: BatchRequest,
item: *BatchItemResult,
) void {
) error{Canceled}!void {
const result = runCurlGetJsonCommandWithExecutable(
allocator,
curl_executable,
endpoint,
request.access_token,
request.account_id,
) catch |err| {
if (err == error.Canceled) return error.Canceled;
item.* = curlBatchErrorItem(allocator, err);
return;
};
Expand Down Expand Up @@ -406,7 +415,7 @@ fn appendCurlConfigQuotedValue(

fn parseCurlHttpOutput(allocator: std.mem.Allocator, output: []const u8) !CurlHttpOutput {
const trimmed = std.mem.trimEnd(u8, output, "\r\n");
const status_idx = std.mem.lastIndexOfScalar(u8, trimmed, '\n') orelse return error.CommandFailed;
const status_idx = std.mem.findScalarLast(u8, trimmed, '\n') orelse return error.CommandFailed;
const status_slice = std.mem.trim(u8, trimmed[status_idx + 1 ..], " \r\t");
const status = std.fmt.parseInt(u16, status_slice, 10) catch return error.CommandFailed;
const body = try allocator.dupe(u8, trimmed[0..status_idx]);
Expand Down
8 changes: 2 additions & 6 deletions src/api/http_env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@ const std = @import("std");
const app_runtime = @import("../core/runtime.zig");

pub fn getEnvMap(allocator: std.mem.Allocator) !std.process.Environ.Map {
return try app_runtime.currentEnviron().createMap(allocator);
return app_runtime.getEnvMap(allocator);
}

pub fn getEnvVarOwned(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
var env_map = try getEnvMap(allocator);
defer env_map.deinit();

const value = env_map.get(name) orelse return error.EnvironmentVariableNotFound;
return try allocator.dupe(u8, value);
return app_runtime.getEnvVarOwned(allocator, name);
}
12 changes: 6 additions & 6 deletions src/api/http_executable.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn ensureExecutableAvailableAlloc(allocator: std.mem.Allocator, executable:
}

fn resolveExecutableForLaunchAlloc(allocator: std.mem.Allocator, executable: []const u8) !?[]u8 {
if (std.fs.path.isAbsolute(executable) or std.mem.indexOfAny(u8, executable, "/\\") != null) {
if (std.Io.Dir.path.isAbsolute(executable) or std.mem.findAny(u8, executable, "/\\") != null) {
if (!accessPath(executable)) return null;
return try allocator.dupe(u8, executable);
}
Expand All @@ -36,7 +36,7 @@ fn resolveExecutableForLaunchAlloc(allocator: std.mem.Allocator, executable: []c
};
defer allocator.free(path_value);

var path_it = std.mem.splitScalar(u8, path_value, std.fs.path.delimiter);
var path_it = std.mem.splitScalar(u8, path_value, std.Io.Dir.path.delimiter);
while (path_it.next()) |entry| {
if (entry.len == 0) continue;
if (try resolveExecutablePathEntryForLaunchAlloc(allocator, entry, executable)) |resolved| return resolved;
Expand All @@ -50,14 +50,14 @@ pub fn resolveExecutablePathEntryForLaunchAlloc(
entry: []const u8,
executable: []const u8,
) !?[]u8 {
const candidate = try std.fs.path.join(allocator, &[_][]const u8{ entry, executable });
const candidate = try std.Io.Dir.path.join(allocator, &[_][]const u8{ entry, executable });
defer allocator.free(candidate);

if (accessPath(candidate)) {
return try allocator.dupe(u8, candidate);
}

if (builtin.os.tag == .windows and std.fs.path.extension(executable).len == 0) {
if (builtin.os.tag == .windows and std.Io.Dir.path.extension(executable).len == 0) {
const path_ext = getEnvVarOwned(allocator, "PATHEXT") catch |err| switch (err) {
error.EnvironmentVariableNotFound => try allocator.dupe(u8, ".COM;.EXE;.BAT;.CMD"),
else => return err,
Expand All @@ -70,7 +70,7 @@ pub fn resolveExecutablePathEntryForLaunchAlloc(
const ext = std.mem.trim(u8, raw_ext, " \t");
if (ext.len == 0) continue;

const ext_candidate = try std.fmt.allocPrint(allocator, "{s}{s}", .{ candidate, ext });
const ext_candidate = try allocator.print("{s}{s}", .{ candidate, ext });
defer allocator.free(ext_candidate);

if (accessPath(ext_candidate)) {
Expand All @@ -82,7 +82,7 @@ pub fn resolveExecutablePathEntryForLaunchAlloc(
return null;
}
fn accessPath(path: []const u8) bool {
if (std.fs.path.isAbsolute(path)) {
if (std.Io.Dir.path.isAbsolute(path)) {
std.Io.Dir.accessAbsolute(app_runtime.io(), path, .{}) catch return false;
return true;
}
Expand Down
5 changes: 3 additions & 2 deletions src/api/usage.zig
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ pub fn fetchUsageForAuthPathsDetailedBatch(

for (auth_paths, 0..) |auth_path, idx| {
var info = auth.parseAuthInfo(arena, auth_path) catch |err| {
if (err == error.Canceled) return error.Canceled;
results[idx].error_name = @errorName(err);
continue;
};
Expand Down Expand Up @@ -355,8 +356,8 @@ fn parseCredits(allocator: std.mem.Allocator, v: std.json.Value) !?registry.Cred
fn parseBalance(allocator: std.mem.Allocator, value: std.json.Value) !?[]u8 {
return switch (value) {
.string => |s| if (s.len == 0) null else try allocator.dupe(u8, s),
.integer => |i| try std.fmt.allocPrint(allocator, "{d}", .{i}),
.float => |f| if (std.math.isFinite(f)) try std.fmt.allocPrint(allocator, "{d}", .{f}) else null,
.integer => |i| try allocator.print("{d}", .{i}),
.float => |f| if (std.math.isFinite(f)) try allocator.print("{d}", .{f}) else null,
.number_string => |s| if (s.len == 0) null else try allocator.dupe(u8, s),
else => null,
};
Expand Down
Loading
Loading