Use released DotSlash package for argument-comment lint (#15199)

## Why
The argument-comment lint now has a packaged DotSlash artifact from
[#15198](https://github.com/openai/codex/pull/15198), so the normal repo
lint path should use that released payload instead of rebuilding the
lint from source every time.

That keeps `just clippy` and CI aligned with the shipped artifact while
preserving a separate source-build path for people actively hacking on
the lint crate.

The current alpha package also exposed two integration wrinkles that the
repo-side prebuilt wrapper needs to smooth over:
- the bundled Dylint library filename includes the host triple, for
example `@nightly-2025-09-18-aarch64-apple-darwin`, and Dylint derives
`RUSTUP_TOOLCHAIN` from that filename
- on Windows, Dylint's driver path also expects `RUSTUP_HOME` to be
present in the environment

Without those adjustments, the prebuilt CI jobs fail during `cargo
metadata` or driver setup. This change makes the checked-in prebuilt
wrapper normalize the packaged library name to the plain
`nightly-2025-09-18` channel before invoking `cargo-dylint`, and it
teaches both the wrapper and the packaged runner source to infer
`RUSTUP_HOME` from `rustup show home` when the environment does not
already provide it.

After the prebuilt Windows lint job started running successfully, it
also surfaced a handful of existing anonymous literal callsites in
`windows-sandbox-rs`. This PR now annotates those callsites so the new
cross-platform lint job is green on the current tree.

## What Changed
- checked in the current
`tools/argument-comment-lint/argument-comment-lint` DotSlash manifest
- kept `tools/argument-comment-lint/run.sh` as the source-build wrapper
for lint development
- added `tools/argument-comment-lint/run-prebuilt-linter.sh` as the
normal enforcement path, using the checked-in DotSlash package and
bundled `cargo-dylint`
- updated `just clippy` and `just argument-comment-lint` to use the
prebuilt wrapper
- split `.github/workflows/rust-ci.yml` so source-package checks live in
a dedicated `argument_comment_lint_package` job, while the released lint
runs in an `argument_comment_lint_prebuilt` matrix on Linux, macOS, and
Windows
- kept the pinned `nightly-2025-09-18` toolchain install in the prebuilt
CI matrix, since the prebuilt package still relies on rustup-provided
toolchain components
- updated `tools/argument-comment-lint/run-prebuilt-linter.sh` to
normalize host-qualified nightly library filenames, keep the `rustup`
shim directory ahead of direct toolchain `cargo` binaries, and export
`RUSTUP_HOME` when needed for Windows Dylint driver setup
- updated `tools/argument-comment-lint/src/bin/argument-comment-lint.rs`
so future published DotSlash artifacts apply the same nightly-filename
normalization and `RUSTUP_HOME` inference internally
- fixed the remaining Windows lint violations in
`codex-rs/windows-sandbox-rs` by adding the required `/*param*/`
comments at the reported callsites
- documented the checked-in DotSlash file, wrapper split, archive
layout, nightly prerequisite, and Windows `RUSTUP_HOME` requirement in
`tools/argument-comment-lint/README.md`
This commit is contained in:
Michael Bolin 2026-03-19 20:19:22 -07:00 committed by GitHub
parent 96a86710c3
commit fa2a2f0be9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 723 additions and 158 deletions

View file

@ -91,17 +91,13 @@ jobs:
- name: cargo shear
run: cargo shear
argument_comment_lint:
name: Argument comment lint
argument_comment_lint_package:
name: Argument comment lint package
runs-on: ubuntu-24.04
needs: changed
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' || github.event_name == 'push' }}
steps:
- uses: actions/checkout@v6
- name: Install Linux sandbox build dependencies
run: |
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
- uses: dtolnay/rust-toolchain@1.93.0
with:
toolchain: nightly-2025-09-18
@ -120,14 +116,46 @@ jobs:
- name: Install cargo-dylint tooling
if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
run: cargo install --locked cargo-dylint dylint-link
- name: Check source wrapper syntax
run: bash -n tools/argument-comment-lint/run.sh
- name: Test argument comment lint package
if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' || github.event_name == 'push' }}
working-directory: tools/argument-comment-lint
run: cargo test
- name: Run argument comment lint on codex-rs
argument_comment_lint_prebuilt:
name: Argument comment lint - ${{ matrix.name }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
needs: changed
if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
strategy:
fail-fast: false
matrix:
include:
- name: Linux
runner: ubuntu-24.04
- name: macOS
runner: macos-15-xlarge
- name: Windows
runner: windows-x64
runs_on:
group: codex-runners
labels: codex-windows-x64
steps:
- uses: actions/checkout@v6
- name: Install Linux sandbox build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
run: |
bash -n tools/argument-comment-lint/run.sh
./tools/argument-comment-lint/run.sh
sudo DEBIAN_FRONTEND=noninteractive apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
- uses: dtolnay/rust-toolchain@1.93.0
with:
toolchain: nightly-2025-09-18
components: llvm-tools-preview, rustc-dev, rust-src
- uses: facebook/install-dotslash@v2
- name: Run argument comment lint on codex-rs
shell: bash
run: ./tools/argument-comment-lint/run-prebuilt-linter.sh
# --- CI to validate on different os/targets --------------------------------
lint_build:
@ -708,14 +736,23 @@ jobs:
results:
name: CI results (required)
needs:
[changed, general, cargo_shear, argument_comment_lint, lint_build, tests]
[
changed,
general,
cargo_shear,
argument_comment_lint_package,
argument_comment_lint_prebuilt,
lint_build,
tests,
]
if: always()
runs-on: ubuntu-24.04
steps:
- name: Summarize
shell: bash
run: |
echo "arglint: ${{ needs.argument_comment_lint.result }}"
echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
echo "general: ${{ needs.general.result }}"
echo "shear : ${{ needs.cargo_shear.result }}"
echo "lint : ${{ needs.lint_build.result }}"
@ -728,8 +765,12 @@ jobs:
exit 0
fi
if [[ '${{ needs.changed.outputs.argument_comment_lint_package }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
[[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
fi
if [[ '${{ needs.changed.outputs.argument_comment_lint }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then
[[ '${{ needs.argument_comment_lint.result }}' == 'success' ]] || { echo 'argument_comment_lint failed'; exit 1; }
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
fi
if [[ '${{ needs.changed.outputs.codex }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' || '${{ github.event_name }}' == 'push' ]]; then

View file

@ -48,6 +48,8 @@ Run `just fmt` (in `codex-rs` directory) automatically after you have finished m
Before finalizing a large change to `codex-rs`, run `just fix -p <project>` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspacewide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Do not re-run tests after running `fix` or `fmt`.
Also run `just argument-comment-lint` to ensure the codebase is clean of comment lint errors.
## TUI style conventions
See `codex-rs/tui/styles.md`.

View file

@ -170,7 +170,7 @@ async fn run_command_under_sandbox(
command_vec,
&cwd_clone,
env_map,
None,
/*timeout_ms*/ None,
config.permissions.windows_sandbox_private_desktop,
)
} else {
@ -181,7 +181,7 @@ async fn run_command_under_sandbox(
command_vec,
&cwd_clone,
env_map,
None,
/*timeout_ms*/ None,
config.permissions.windows_sandbox_private_desktop,
)
}
@ -251,15 +251,15 @@ async fn run_command_under_sandbox(
&config.permissions.file_system_sandbox_policy,
config.permissions.network_sandbox_policy,
sandbox_policy_cwd.as_path(),
false,
/*enforce_managed_network*/ false,
network.as_ref(),
None,
/*extensions*/ None,
);
let network_policy = config.permissions.network_sandbox_policy;
spawn_debug_sandbox_child(
PathBuf::from("/usr/bin/sandbox-exec"),
args,
None,
/*arg0*/ None,
cwd,
network_policy,
env,

View file

@ -422,7 +422,7 @@ fn record_windows_sandbox_spawn_failure(
if let Some(metrics) = codex_otel::metrics::global() {
let _ = metrics.counter(
"codex.windows_sandbox.createprocessasuserw_failed",
1,
/*inc*/ 1,
&[
("error_code", error_code.as_str()),
("path_kind", path_kind),

View file

@ -185,8 +185,8 @@ pub fn run_elevated_setup(
command_cwd,
env_map,
codex_home,
None,
None,
/*read_roots_override*/ None,
/*write_roots_override*/ None,
)
}
@ -421,7 +421,11 @@ fn emit_windows_sandbox_setup_failure_metrics(
if let Some(message) = message_tag.as_deref() {
failure_tags.push(("message", message));
}
let _ = metrics.counter(elevated_setup_failure_metric_name(_err), 1, &failure_tags);
let _ = metrics.counter(
elevated_setup_failure_metric_name(_err),
/*inc*/ 1,
&failure_tags,
);
}
} else {
let _ = metrics.counter(

View file

@ -2901,7 +2901,7 @@ impl App {
Ok(()) => {
session_telemetry.counter(
"codex.windows_sandbox.elevated_setup_success",
1,
/*inc*/ 1,
&[],
);
AppEvent::EnableWindowsSandboxForAgentMode {
@ -2931,7 +2931,7 @@ impl App {
codex_core::windows_sandbox::elevated_setup_failure_metric_name(
&err,
),
1,
/*inc*/ 1,
&tags,
);
tracing::error!(
@ -2972,7 +2972,7 @@ impl App {
) {
session_telemetry.counter(
"codex.windows_sandbox.legacy_setup_preflight_failed",
1,
/*inc*/ 1,
&[],
);
tracing::warn!(
@ -2997,7 +2997,7 @@ impl App {
self.chat_widget
.add_to_history(history_cell::new_info_event(
format!("Granting sandbox read access to {path} ..."),
None,
/*hint*/ None,
));
let policy = self.config.permissions.sandbox_policy.get().clone();
@ -3072,11 +3072,13 @@ impl App {
match builder.apply().await {
Ok(()) => {
if elevated_enabled {
self.config.set_windows_sandbox_enabled(false);
self.config.set_windows_elevated_sandbox_enabled(true);
self.config.set_windows_sandbox_enabled(/*value*/ false);
self.config
.set_windows_elevated_sandbox_enabled(/*value*/ true);
} else {
self.config.set_windows_sandbox_enabled(true);
self.config.set_windows_elevated_sandbox_enabled(false);
self.config.set_windows_sandbox_enabled(/*value*/ true);
self.config
.set_windows_elevated_sandbox_enabled(/*value*/ false);
}
self.chat_widget.set_windows_sandbox_mode(
self.config.permissions.windows_sandbox_mode,
@ -6454,7 +6456,7 @@ guardian_approval = true
make_header(true),
Arc::new(crate::history_cell::new_info_event(
"startup tip that used to replay".to_string(),
None,
/*hint*/ None,
)) as Arc<dyn HistoryCell>,
user_cell("Tell me a long story about a town with a dark lighthouse."),
agent_cell(story_part_one),

View file

@ -4536,7 +4536,7 @@ impl ChatWidget {
self.session_telemetry.counter(
"codex.windows_sandbox.setup_elevated_sandbox_command",
1,
/*inc*/ 1,
&[],
);
self.app_event_tx
@ -7525,8 +7525,11 @@ impl ChatWidget {
return;
}
self.session_telemetry
.counter("codex.windows_sandbox.elevated_prompt_shown", 1, &[]);
self.session_telemetry.counter(
"codex.windows_sandbox.elevated_prompt_shown",
/*inc*/ 1,
&[],
);
let mut header = ColumnRenderable::new();
header.push(*Box::new(
@ -7545,7 +7548,11 @@ impl ChatWidget {
name: "Set up default sandbox (requires Administrator permissions)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
accept_otel.counter("codex.windows_sandbox.elevated_prompt_accept", 1, &[]);
accept_otel.counter(
"codex.windows_sandbox.elevated_prompt_accept",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
});
@ -7557,7 +7564,11 @@ impl ChatWidget {
name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
legacy_otel.counter("codex.windows_sandbox.elevated_prompt_use_legacy", 1, &[]);
legacy_otel.counter(
"codex.windows_sandbox.elevated_prompt_use_legacy",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
preset: legacy_preset.clone(),
});
@ -7569,7 +7580,11 @@ impl ChatWidget {
name: "Quit".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
quit_otel.counter("codex.windows_sandbox.elevated_prompt_quit", 1, &[]);
quit_otel.counter(
"codex.windows_sandbox.elevated_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
@ -7619,7 +7634,11 @@ impl ChatWidget {
let otel = self.session_telemetry.clone();
let preset = elevated_preset;
move |tx| {
otel.counter("codex.windows_sandbox.fallback_retry_elevated", 1, &[]);
otel.counter(
"codex.windows_sandbox.fallback_retry_elevated",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
});
@ -7635,7 +7654,11 @@ impl ChatWidget {
let otel = self.session_telemetry.clone();
let preset = legacy_preset;
move |tx| {
otel.counter("codex.windows_sandbox.fallback_use_legacy", 1, &[]);
otel.counter(
"codex.windows_sandbox.fallback_use_legacy",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
preset: preset.clone(),
});
@ -7648,7 +7671,11 @@ impl ChatWidget {
name: "Quit".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
quit_otel.counter("codex.windows_sandbox.fallback_prompt_quit", 1, &[]);
quit_otel.counter(
"codex.windows_sandbox.fallback_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
@ -7688,11 +7715,12 @@ impl ChatWidget {
// While elevated sandbox setup runs, prevent typing so the user doesn't
// accidentally queue messages that will run under an unexpected mode.
self.bottom_pane.set_composer_input_enabled(
false,
/*enabled*/ false,
Some("Input disabled until setup completes.".to_string()),
);
self.bottom_pane.ensure_status_indicator();
self.bottom_pane.set_interrupt_hint_visible(false);
self.bottom_pane
.set_interrupt_hint_visible(/*visible*/ false);
self.set_status(
"Setting up sandbox...".to_string(),
Some("Hang tight, this may take a few minutes".to_string()),
@ -7708,7 +7736,8 @@ impl ChatWidget {
#[cfg(target_os = "windows")]
pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {
self.bottom_pane.set_composer_input_enabled(true, None);
self.bottom_pane
.set_composer_input_enabled(/*enabled*/ true, /*placeholder*/ None);
self.bottom_pane.hide_status_indicator();
self.request_redraw();
}

View file

@ -979,8 +979,10 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
let remote_url = "https://example.com/remote-only.png".to_string();
chat.set_remote_image_urls(vec![remote_url.clone()]);
chat.bottom_pane
.set_composer_input_enabled(false, Some("Input disabled for test.".to_string()));
chat.bottom_pane.set_composer_input_enabled(
/*enabled*/ false,
Some("Input disabled for test.".to_string()),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

View file

@ -1271,7 +1271,7 @@ mod tests {
let temp_dir = TempDir::new()?;
let mut config = build_config(&temp_dir).await?;
config.active_project = ProjectConfig { trust_level: None };
config.set_windows_sandbox_enabled(false);
config.set_windows_sandbox_enabled(/*value*/ false);
let should_show = should_show_trust_screen(&config);
assert!(
@ -1287,7 +1287,7 @@ mod tests {
let temp_dir = TempDir::new()?;
let mut config = build_config(&temp_dir).await?;
config.active_project = ProjectConfig { trust_level: None };
config.set_windows_sandbox_enabled(true);
config.set_windows_sandbox_enabled(/*value*/ true);
let should_show = should_show_trust_screen(&config);
if cfg!(target_os = "windows") {

View file

@ -354,7 +354,7 @@ mod tests {
StatusDetailsCapitalization::CapitalizeFirst,
STATUS_DETAILS_DEFAULT_MAX_LINES,
);
w.set_interrupt_hint_visible(false);
w.set_interrupt_hint_visible(/*visible*/ false);
// Freeze time-dependent rendering (elapsed + spinner) to keep the snapshot stable.
w.is_paused = true;

View file

@ -1363,18 +1363,18 @@ impl App {
let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config);
self.app_event_tx.send(AppEvent::CodexOp(
AppCommand::override_turn_context(
None,
None,
None,
None,
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
None,
None,
None,
None,
None,
None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
/*service_tier*/ None,
/*collaboration_mode*/ None,
/*personality*/ None,
)
.into_core(),
));
@ -3785,7 +3785,7 @@ impl App {
Ok(()) => {
session_telemetry.counter(
"codex.windows_sandbox.elevated_setup_success",
1,
/*inc*/ 1,
&[],
);
AppEvent::EnableWindowsSandboxForAgentMode {
@ -3815,7 +3815,7 @@ impl App {
codex_core::windows_sandbox::elevated_setup_failure_metric_name(
&err,
),
1,
/*inc*/ 1,
&tags,
);
tracing::error!(
@ -3856,7 +3856,7 @@ impl App {
) {
session_telemetry.counter(
"codex.windows_sandbox.legacy_setup_preflight_failed",
1,
/*inc*/ 1,
&[],
);
tracing::warn!(
@ -3881,7 +3881,7 @@ impl App {
self.chat_widget
.add_to_history(history_cell::new_info_event(
format!("Granting sandbox read access to {path} ..."),
None,
/*hint*/ None,
));
let policy = self.config.permissions.sandbox_policy.get().clone();
@ -3956,11 +3956,13 @@ impl App {
match builder.apply().await {
Ok(()) => {
if elevated_enabled {
self.config.set_windows_sandbox_enabled(false);
self.config.set_windows_elevated_sandbox_enabled(true);
self.config.set_windows_sandbox_enabled(/*value*/ false);
self.config
.set_windows_elevated_sandbox_enabled(/*value*/ true);
} else {
self.config.set_windows_sandbox_enabled(true);
self.config.set_windows_elevated_sandbox_enabled(false);
self.config.set_windows_sandbox_enabled(/*value*/ true);
self.config
.set_windows_elevated_sandbox_enabled(/*value*/ false);
}
self.chat_widget.set_windows_sandbox_mode(
self.config.permissions.windows_sandbox_mode,
@ -3972,18 +3974,18 @@ impl App {
{
self.app_event_tx.send(AppEvent::CodexOp(
AppCommand::override_turn_context(
None,
None,
None,
None,
/*cwd*/ None,
/*approval_policy*/ None,
/*approvals_reviewer*/ None,
/*sandbox_policy*/ None,
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
None,
None,
None,
None,
None,
None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
/*service_tier*/ None,
/*collaboration_mode*/ None,
/*personality*/ None,
)
.into(),
));
@ -3998,18 +4000,18 @@ impl App {
} else {
self.app_event_tx.send(AppEvent::CodexOp(
AppCommand::override_turn_context(
None,
/*cwd*/ None,
Some(preset.approval),
Some(self.config.approvals_reviewer),
Some(preset.sandbox.clone()),
#[cfg(target_os = "windows")]
Some(windows_sandbox_level),
None,
None,
None,
None,
None,
None,
/*model*/ None,
/*effort*/ None,
/*summary*/ None,
/*service_tier*/ None,
/*collaboration_mode*/ None,
/*personality*/ None,
)
.into(),
));

View file

@ -4699,7 +4699,7 @@ impl ChatWidget {
self.session_telemetry.counter(
"codex.windows_sandbox.setup_elevated_sandbox_command",
1,
/*inc*/ 1,
&[],
);
self.app_event_tx
@ -8707,8 +8707,11 @@ impl ChatWidget {
return;
}
self.session_telemetry
.counter("codex.windows_sandbox.elevated_prompt_shown", 1, &[]);
self.session_telemetry.counter(
"codex.windows_sandbox.elevated_prompt_shown",
/*inc*/ 1,
&[],
);
let mut header = ColumnRenderable::new();
header.push(*Box::new(
@ -8727,7 +8730,11 @@ impl ChatWidget {
name: "Set up default sandbox (requires Administrator permissions)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
accept_otel.counter("codex.windows_sandbox.elevated_prompt_accept", 1, &[]);
accept_otel.counter(
"codex.windows_sandbox.elevated_prompt_accept",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
});
@ -8739,7 +8746,11 @@ impl ChatWidget {
name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
legacy_otel.counter("codex.windows_sandbox.elevated_prompt_use_legacy", 1, &[]);
legacy_otel.counter(
"codex.windows_sandbox.elevated_prompt_use_legacy",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
preset: legacy_preset.clone(),
});
@ -8751,7 +8762,11 @@ impl ChatWidget {
name: "Quit".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
quit_otel.counter("codex.windows_sandbox.elevated_prompt_quit", 1, &[]);
quit_otel.counter(
"codex.windows_sandbox.elevated_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
@ -8801,7 +8816,11 @@ impl ChatWidget {
let otel = self.session_telemetry.clone();
let preset = elevated_preset;
move |tx| {
otel.counter("codex.windows_sandbox.fallback_retry_elevated", 1, &[]);
otel.counter(
"codex.windows_sandbox.fallback_retry_elevated",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxElevatedSetup {
preset: preset.clone(),
});
@ -8817,7 +8836,11 @@ impl ChatWidget {
let otel = self.session_telemetry.clone();
let preset = legacy_preset;
move |tx| {
otel.counter("codex.windows_sandbox.fallback_use_legacy", 1, &[]);
otel.counter(
"codex.windows_sandbox.fallback_use_legacy",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::BeginWindowsSandboxLegacySetup {
preset: preset.clone(),
});
@ -8830,7 +8853,11 @@ impl ChatWidget {
name: "Quit".to_string(),
description: None,
actions: vec![Box::new(move |tx| {
quit_otel.counter("codex.windows_sandbox.fallback_prompt_quit", 1, &[]);
quit_otel.counter(
"codex.windows_sandbox.fallback_prompt_quit",
/*inc*/ 1,
&[],
);
tx.send(AppEvent::Exit(ExitMode::ShutdownFirst));
})],
dismiss_on_select: true,
@ -8870,11 +8897,12 @@ impl ChatWidget {
// While elevated sandbox setup runs, prevent typing so the user doesn't
// accidentally queue messages that will run under an unexpected mode.
self.bottom_pane.set_composer_input_enabled(
false,
/*enabled*/ false,
Some("Input disabled until setup completes.".to_string()),
);
self.bottom_pane.ensure_status_indicator();
self.bottom_pane.set_interrupt_hint_visible(false);
self.bottom_pane
.set_interrupt_hint_visible(/*visible*/ false);
self.set_status(
"Setting up sandbox...".to_string(),
Some("Hang tight, this may take a few minutes".to_string()),
@ -8890,7 +8918,8 @@ impl ChatWidget {
#[cfg(target_os = "windows")]
pub(crate) fn clear_windows_sandbox_setup_status(&mut self) {
self.bottom_pane.set_composer_input_enabled(true, None);
self.bottom_pane
.set_composer_input_enabled(/*enabled*/ true, /*placeholder*/ None);
self.bottom_pane.hide_status_indicator();
self.request_redraw();
}

View file

@ -1003,8 +1003,10 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
let remote_url = "https://example.com/remote-only.png".to_string();
chat.set_remote_image_urls(vec![remote_url.clone()]);
chat.bottom_pane
.set_composer_input_enabled(false, Some("Input disabled for test.".to_string()));
chat.bottom_pane.set_composer_input_enabled(
/*enabled*/ false,
Some("Input disabled for test.".to_string()),
);
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

View file

@ -352,7 +352,7 @@ mod tests {
StatusDetailsCapitalization::CapitalizeFirst,
STATUS_DETAILS_DEFAULT_MAX_LINES,
);
w.set_interrupt_hint_visible(false);
w.set_interrupt_hint_visible(/*visible*/ false);
// Freeze time-dependent rendering (elapsed + spinner) to keep the snapshot stable.
w.is_paused = true;

View file

@ -172,7 +172,7 @@ impl PsuedoCon {
si.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
si.StartupInfo.hStdError = INVALID_HANDLE_VALUE;
let mut attrs = ProcThreadAttributeList::with_capacity(1)?;
let mut attrs = ProcThreadAttributeList::with_capacity(/*num_attributes*/ 1)?;
attrs.set_pty(self.con)?;
si.lpAttributeList = attrs.as_mut_ptr();

View file

@ -275,7 +275,12 @@ unsafe fn ensure_allow_mask_aces_with_inheritance_impl(
let (p_dacl, p_sd) = fetch_dacl_handle(path)?;
let mut entries: Vec<EXPLICIT_ACCESS_W> = Vec::new();
for sid in sids {
if dacl_mask_allows(p_dacl, &[*sid], allow_mask, true) {
if dacl_mask_allows(
p_dacl,
&[*sid],
allow_mask,
/*require_all_bits*/ true,
) {
continue;
}
entries.push(EXPLICIT_ACCESS_W {

View file

@ -81,7 +81,7 @@ unsafe fn path_has_world_write_allow(path: &Path) -> Result<bool> {
let mut world = world_sid()?;
let psid_world = world.as_mut_ptr() as *mut c_void;
let write_mask = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES;
path_mask_allows(path, &[psid_world], write_mask, false)
path_mask_allows(path, &[psid_world], write_mask, /*require_all_bits*/ false)
}
pub fn audit_everyone_writable(

View file

@ -76,7 +76,9 @@ pub fn create_conpty(cols: i16, rows: i16) -> Result<ConptyInstance> {
hpc: hpc as HANDLE,
input_write: input_write as HANDLE,
output_read: output_read as HANDLE,
_desktop: LaunchDesktop::prepare(false, None)?,
_desktop: LaunchDesktop::prepare(
/*use_private_desktop*/ false, /*logs_base_dir*/ None,
)?,
})
}
@ -108,8 +110,8 @@ pub fn spawn_conpty_process_as_user(
let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
si.StartupInfo.lpDesktop = desktop.startup_info_desktop();
let conpty = create_conpty(80, 24)?;
let mut attrs = ProcThreadAttributeList::new(1)?;
let conpty = create_conpty(/*cols*/ 80, /*rows*/ 24)?;
let mut attrs = ProcThreadAttributeList::new(/*attr_count*/ 1)?;
attrs.set_pseudoconsole(conpty.hpc)?;
si.lpAttributeList = attrs.as_mut_ptr();

View file

@ -289,7 +289,7 @@ fn spawn_ipc_process(
&req.env,
stdin_mode,
StderrMode::Separate,
false,
/*use_private_desktop*/ false,
)?;
(
pipe_handles.process,

View file

@ -159,7 +159,7 @@ pub fn apply_no_network_to_env(env_map: &mut HashMap<String, String>) -> Result<
.entry("GIT_ALLOW_PROTOCOLS".into())
.or_insert_with(|| "".into());
let base = ensure_denybin(&["ssh", "scp"], None)?;
let base = ensure_denybin(&["ssh", "scp"], /*denybin_dir*/ None)?;
for tool in ["curl", "wget"] {
for ext in [".bat", ".cmd"] {
let p = base.join(format!("{}{}", tool, ext));

View file

@ -229,7 +229,7 @@ pub fn spawn_process_with_pipes(
argv,
cwd,
env_map,
None,
/*logs_base_dir*/ None,
stdio,
use_private_desktop,
)

View file

@ -153,7 +153,7 @@ fn apply_read_acls(
let builtin_has = read_mask_allows_or_log(
root,
subjects.rx_psids,
None,
/*label*/ None,
access_mask,
access_label,
refresh_errors,
@ -215,7 +215,7 @@ fn read_mask_allows_or_log(
refresh_errors: &mut Vec<String>,
log: &mut File,
) -> Result<bool> {
match path_mask_allows(root, psids, read_mask, true) {
match path_mask_allows(root, psids, read_mask, /*require_all_bits*/ true) {
Ok(has) => Ok(has),
Err(e) => {
let label_suffix = label
@ -653,25 +653,26 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
("sandbox_group", sandbox_group_psid),
(cap_label, cap_psid_for_root),
] {
let has = match path_mask_allows(root, &[psid], write_mask, true) {
Ok(h) => h,
Err(e) => {
refresh_errors.push(format!(
"write mask check failed on {} for {label}: {}",
root.display(),
e
));
log_line(
log,
&format!(
"write mask check failed on {} for {label}: {}; continuing",
let has =
match path_mask_allows(root, &[psid], write_mask, /*require_all_bits*/ true) {
Ok(h) => h,
Err(e) => {
refresh_errors.push(format!(
"write mask check failed on {} for {label}: {}",
root.display(),
e
),
)?;
false
}
};
));
log_line(
log,
&format!(
"write mask check failed on {} for {label}: {}; continuing",
root.display(),
e
),
)?;
false
}
};
if !has {
need_grant = true;
}

View file

@ -91,8 +91,8 @@ pub fn run_setup_refresh(
command_cwd,
env_map,
codex_home,
None,
None,
/*read_roots_override*/ None,
/*write_roots_override*/ None,
)
}

View file

@ -30,7 +30,7 @@ fmt:
fix *args:
cargo clippy --fix --tests --allow-dirty "$@"
clippy:
clippy *args:
cargo clippy --tests "$@"
install:
@ -89,6 +89,10 @@ write-hooks-schema:
# Run the argument-comment Dylint checks across codex-rs.
[no-cd]
argument-comment-lint *args:
./tools/argument-comment-lint/run-prebuilt-linter.sh "$@"
[no-cd]
argument-comment-lint-from-source *args:
./tools/argument-comment-lint/run.sh "$@"
# Tail logs from the state SQLite database

View file

@ -73,21 +73,71 @@ GitHub releases also publish a DotSlash file named
x64. The published package contains a small runner executable, a bundled
`cargo-dylint`, and the prebuilt lint library.
Run the lint against `codex-rs` from the repo root:
The package is not a full Rust toolchain. Running the prebuilt path still
requires the pinned nightly toolchain to be installed via `rustup`:
```bash
rustup toolchain install nightly-2025-09-18 \
--component llvm-tools-preview \
--component rustc-dev \
--component rust-src
```
The checked-in DotSlash file lives at `tools/argument-comment-lint/argument-comment-lint`.
`run-prebuilt-linter.sh` resolves that file via `dotslash` and is the path used by
`just clippy`, `just argument-comment-lint`, and the Rust CI job. The
source-build path remains available in `run.sh` for people
iterating on the lint crate itself.
The Unix archive layout is:
```text
argument-comment-lint/
bin/
argument-comment-lint
cargo-dylint
lib/
libargument_comment_lint@nightly-2025-09-18-<target>.dylib|so
```
On Windows the same layout is published as a `.zip`, with `.exe` and `.dll`
filenames instead.
DotSlash resolves the package entrypoint to `argument-comment-lint/bin/argument-comment-lint`
(or `.exe` on Windows). That runner finds the sibling bundled `cargo-dylint`
binary and the single packaged Dylint library under `lib/`, normalizes the
host-qualified nightly filename to the plain `nightly-2025-09-18` channel when
needed, and then invokes `cargo-dylint dylint --lib-path <that-library>` with
the repo's default `DYLINT_RUSTFLAGS` and `CARGO_INCREMENTAL=0` settings.
The checked-in `run-prebuilt-linter.sh` wrapper uses the fetched package
contents directly so the current checked-in alpha artifact works the same way.
It also makes sure the `rustup` shims stay ahead of any direct toolchain
`cargo` binary on `PATH`, and sets `RUSTUP_HOME` from `rustup show home` when
the environment does not already provide it. That extra `RUSTUP_HOME` export is
required for the current Windows Dylint driver path.
If you are changing the lint crate itself, use the source-build wrapper:
```bash
./tools/argument-comment-lint/run.sh -p codex-core
```
Run the lint against `codex-rs` from the repo root:
```bash
./tools/argument-comment-lint/run-prebuilt-linter.sh -p codex-core
just argument-comment-lint -p codex-core
```
If no package selection is provided, `run.sh` defaults to checking the
If no package selection is provided, `run-prebuilt-linter.sh` defaults to checking the
`codex-rs` workspace with `--workspace --no-deps`.
Repo runs also promote `uncommented_anonymous_literal_argument` to an error by
default:
```bash
./tools/argument-comment-lint/run.sh -p codex-core
./tools/argument-comment-lint/run-prebuilt-linter.sh -p codex-core
```
The wrapper does that by setting `DYLINT_RUSTFLAGS`, and it leaves an explicit
@ -105,5 +155,5 @@ CARGO_INCREMENTAL=1 \
To expand target coverage for an ad hoc run:
```bash
./tools/argument-comment-lint/run.sh -p codex-core -- --all-targets
./tools/argument-comment-lint/run-prebuilt-linter.sh -p codex-core -- --all-targets
```

View file

@ -0,0 +1,79 @@
#!/usr/bin/env dotslash
{
"name": "argument-comment-lint",
"platforms": {
"macos-aarch64": {
"size": 3402747,
"hash": "blake3",
"digest": "a11669d2f184a2c6f226cedce1bf10d1ec478d53413c42fe80d17dd873fdb2d7",
"format": "tar.gz",
"path": "argument-comment-lint/bin/argument-comment-lint",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.117.0-alpha.2/argument-comment-lint-aarch64-apple-darwin.tar.gz"
},
{
"type": "github-release",
"repo": "https://github.com/openai/codex",
"tag": "rust-v0.117.0-alpha.2",
"name": "argument-comment-lint-aarch64-apple-darwin.tar.gz"
}
]
},
"linux-x86_64": {
"size": 3869711,
"hash": "blake3",
"digest": "1015f4ba07d57edc5ec79c8f6709ddc1516f64c903e909820437a4b89d8d853a",
"format": "tar.gz",
"path": "argument-comment-lint/bin/argument-comment-lint",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.117.0-alpha.2/argument-comment-lint-x86_64-unknown-linux-gnu.tar.gz"
},
{
"type": "github-release",
"repo": "https://github.com/openai/codex",
"tag": "rust-v0.117.0-alpha.2",
"name": "argument-comment-lint-x86_64-unknown-linux-gnu.tar.gz"
}
]
},
"linux-aarch64": {
"size": 3759446,
"hash": "blake3",
"digest": "91f2a31e6390ca728ad09ae1aa6b6f379c67d996efcc22956001df89f068af5b",
"format": "tar.gz",
"path": "argument-comment-lint/bin/argument-comment-lint",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.117.0-alpha.2/argument-comment-lint-aarch64-unknown-linux-gnu.tar.gz"
},
{
"type": "github-release",
"repo": "https://github.com/openai/codex",
"tag": "rust-v0.117.0-alpha.2",
"name": "argument-comment-lint-aarch64-unknown-linux-gnu.tar.gz"
}
]
},
"windows-x86_64": {
"size": 3244599,
"hash": "blake3",
"digest": "dc711c6d85b1cabbe52447dda3872deb20c2e64b155da8be0ecb207c7c391683",
"format": "zip",
"path": "argument-comment-lint/bin/argument-comment-lint.exe",
"providers": [
{
"url": "https://github.com/openai/codex/releases/download/rust-v0.117.0-alpha.2/argument-comment-lint-x86_64-pc-windows-msvc.zip"
},
{
"type": "github-release",
"repo": "https://github.com/openai/codex",
"tag": "rust-v0.117.0-alpha.2",
"name": "argument-comment-lint-x86_64-pc-windows-msvc.zip"
}
]
}
}
}

View file

@ -0,0 +1,164 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
manifest_path="$repo_root/codex-rs/Cargo.toml"
dotslash_manifest="$repo_root/tools/argument-comment-lint/argument-comment-lint"
has_manifest_path=false
has_package_selection=false
has_library_selection=false
has_no_deps=false
expect_value=""
for arg in "$@"; do
if [[ -n "$expect_value" ]]; then
case "$expect_value" in
manifest_path)
has_manifest_path=true
;;
package_selection)
has_package_selection=true
;;
library_selection)
has_library_selection=true
;;
esac
expect_value=""
continue
fi
case "$arg" in
--)
break
;;
--manifest-path)
expect_value="manifest_path"
;;
--manifest-path=*)
has_manifest_path=true
;;
-p|--package)
expect_value="package_selection"
;;
--package=*)
has_package_selection=true
;;
--lib|--lib-path)
expect_value="library_selection"
;;
--lib=*|--lib-path=*)
has_library_selection=true
;;
--workspace)
has_package_selection=true
;;
--no-deps)
has_no_deps=true
;;
esac
done
lint_args=()
if [[ "$has_manifest_path" == false ]]; then
lint_args+=(--manifest-path "$manifest_path")
fi
if [[ "$has_package_selection" == false ]]; then
lint_args+=(--workspace)
fi
if [[ "$has_no_deps" == false ]]; then
lint_args+=(--no-deps)
fi
lint_args+=("$@")
if ! command -v dotslash >/dev/null 2>&1; then
cat >&2 <<EOF
argument-comment-lint prebuilt wrapper requires dotslash.
Install dotslash, or use:
./tools/argument-comment-lint/run.sh ...
EOF
exit 1
fi
if command -v rustup >/dev/null 2>&1; then
rustup_bin_dir="$(dirname "$(command -v rustup)")"
path_entries=()
while IFS= read -r entry; do
[[ -n "$entry" && "$entry" != "$rustup_bin_dir" ]] && path_entries+=("$entry")
done < <(printf '%s\n' "${PATH//:/$'\n'}")
PATH="$rustup_bin_dir"
if ((${#path_entries[@]} > 0)); then
PATH+=":$(IFS=:; echo "${path_entries[*]}")"
fi
export PATH
if [[ -z "${RUSTUP_HOME:-}" ]]; then
rustup_home="$(rustup show home 2>/dev/null || true)"
if [[ -n "$rustup_home" ]]; then
export RUSTUP_HOME="$rustup_home"
fi
fi
fi
package_entrypoint="$(dotslash -- fetch "$dotslash_manifest")"
bin_dir="$(cd "$(dirname "$package_entrypoint")" && pwd)"
package_root="$(cd "$bin_dir/.." && pwd)"
library_dir="$package_root/lib"
cargo_dylint="$bin_dir/cargo-dylint"
if [[ ! -x "$cargo_dylint" ]]; then
cargo_dylint="$bin_dir/cargo-dylint.exe"
fi
if [[ ! -x "$cargo_dylint" ]]; then
echo "bundled cargo-dylint executable not found under $bin_dir" >&2
exit 1
fi
shopt -s nullglob
libraries=("$library_dir"/*@*)
shopt -u nullglob
if [[ ${#libraries[@]} -eq 0 ]]; then
echo "no packaged Dylint library found in $library_dir" >&2
exit 1
fi
if [[ ${#libraries[@]} -ne 1 ]]; then
echo "expected exactly one packaged Dylint library in $library_dir" >&2
exit 1
fi
library_path="${libraries[0]}"
library_filename="$(basename "$library_path")"
normalized_library_path="$library_path"
library_ext=".${library_filename##*.}"
library_stem="${library_filename%.*}"
if [[ "$library_stem" =~ ^(.+@nightly-[0-9]{4}-[0-9]{2}-[0-9]{2})-.+$ ]]; then
normalized_library_filename="${BASH_REMATCH[1]}$library_ext"
temp_dir="$(mktemp -d "${TMPDIR:-/tmp}/argument-comment-lint.XXXXXX")"
normalized_library_path="$temp_dir/$normalized_library_filename"
cp "$library_path" "$normalized_library_path"
fi
if [[ -n "${DYLINT_RUSTFLAGS:-}" ]]; then
if [[ "$DYLINT_RUSTFLAGS" != *"-D uncommented-anonymous-literal-argument"* ]]; then
DYLINT_RUSTFLAGS+=" -D uncommented-anonymous-literal-argument"
fi
if [[ "$DYLINT_RUSTFLAGS" != *"-A unknown_lints"* ]]; then
DYLINT_RUSTFLAGS+=" -A unknown_lints"
fi
else
DYLINT_RUSTFLAGS="-D uncommented-anonymous-literal-argument -A unknown_lints"
fi
export DYLINT_RUSTFLAGS
if [[ -z "${CARGO_INCREMENTAL:-}" ]]; then
export CARGO_INCREMENTAL=0
fi
command=("$cargo_dylint" dylint --lib-path "$normalized_library_path")
if [[ "$has_library_selection" == false ]]; then
command+=(--all)
fi
command+=("${lint_args[@]}")
exec "${command[@]}"

View file

@ -5,6 +5,7 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
lint_path="$repo_root/tools/argument-comment-lint"
manifest_path="$repo_root/codex-rs/Cargo.toml"
toolchain_channel="nightly-2025-09-18"
strict_lint="uncommented-anonymous-literal-argument"
noise_lint="unknown_lints"
@ -14,6 +15,42 @@ has_no_deps=false
has_library_selection=false
expect_value=""
ensure_local_prerequisites() {
if ! command -v cargo-dylint >/dev/null 2>&1 || ! command -v dylint-link >/dev/null 2>&1; then
cat >&2 <<EOF
argument-comment-lint source wrapper requires cargo-dylint and dylint-link.
Install them with:
cargo install --locked cargo-dylint dylint-link
EOF
exit 1
fi
if ! rustup toolchain list | grep -q "^${toolchain_channel}"; then
cat >&2 <<EOF
argument-comment-lint source wrapper requires the ${toolchain_channel} toolchain with rustc-dev support.
Install it with:
rustup toolchain install ${toolchain_channel} \\
--component llvm-tools-preview \\
--component rustc-dev \\
--component rust-src
EOF
exit 1
fi
}
set_default_env() {
if [[ "${DYLINT_RUSTFLAGS:-}" != *"$strict_lint"* ]]; then
export DYLINT_RUSTFLAGS="${DYLINT_RUSTFLAGS:+${DYLINT_RUSTFLAGS} }-D $strict_lint"
fi
if [[ "${DYLINT_RUSTFLAGS:-}" != *"$noise_lint"* ]]; then
export DYLINT_RUSTFLAGS="${DYLINT_RUSTFLAGS:+${DYLINT_RUSTFLAGS} }-A $noise_lint"
fi
if [[ -z "${CARGO_INCREMENTAL:-}" ]]; then
export CARGO_INCREMENTAL=0
fi
}
for arg in "$@"; do
if [[ -n "$expect_value" ]]; then
case "$expect_value" in
@ -62,30 +99,25 @@ for arg in "$@"; do
esac
done
lint_args=()
if [[ "$has_manifest_path" == false ]]; then
lint_args+=(--manifest-path "$manifest_path")
fi
if [[ "$has_package_selection" == false ]]; then
lint_args+=(--workspace)
fi
if [[ "$has_no_deps" == false ]]; then
lint_args+=(--no-deps)
fi
lint_args+=("$@")
ensure_local_prerequisites
set_default_env
cmd=(cargo dylint --path "$lint_path")
if [[ "$has_library_selection" == false ]]; then
cmd+=(--all)
fi
if [[ "$has_manifest_path" == false ]]; then
cmd+=(--manifest-path "$manifest_path")
fi
if [[ "$has_package_selection" == false ]]; then
cmd+=(--workspace)
fi
if [[ "$has_no_deps" == false ]]; then
cmd+=(--no-deps)
fi
cmd+=("$@")
if [[ "${DYLINT_RUSTFLAGS:-}" != *"$strict_lint"* ]]; then
export DYLINT_RUSTFLAGS="${DYLINT_RUSTFLAGS:+${DYLINT_RUSTFLAGS} }-D $strict_lint"
fi
if [[ "${DYLINT_RUSTFLAGS:-}" != *"$noise_lint"* ]]; then
export DYLINT_RUSTFLAGS="${DYLINT_RUSTFLAGS:+${DYLINT_RUSTFLAGS} }-A $noise_lint"
fi
if [[ -z "${CARGO_INCREMENTAL:-}" ]]; then
export CARGO_INCREMENTAL=0
fi
cmd+=("${lint_args[@]}")
exec "${cmd[@]}"

View file

@ -5,6 +5,8 @@ use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::ExitCode;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
fn main() -> ExitCode {
match run() {
@ -33,7 +35,7 @@ fn run() -> Result<ExitCode, String> {
})?;
let cargo_dylint = bin_dir.join(cargo_dylint_binary_name());
let library_dir = package_root.join("lib");
let library_path = find_bundled_library(&library_dir)?;
let library_path = prepare_library_path_for_dylint(&find_bundled_library(&library_dir)?)?;
ensure_exists(&cargo_dylint, "bundled cargo-dylint executable")?;
ensure_exists(
@ -49,7 +51,7 @@ fn run() -> Result<ExitCode, String> {
command.arg("--all");
}
command.args(&args);
set_default_env(&mut command);
set_default_env(&mut command)?;
let status = command
.status()
@ -80,7 +82,7 @@ fn has_library_selection(args: &[OsString]) -> bool {
false
}
fn set_default_env(command: &mut Command) {
fn set_default_env(command: &mut Command) -> Result<(), String> {
if let Some(flags) = env::var_os("DYLINT_RUSTFLAGS") {
let mut flags = flags.to_string_lossy().to_string();
append_flag_if_missing(&mut flags, "-D uncommented-anonymous-literal-argument");
@ -96,6 +98,14 @@ fn set_default_env(command: &mut Command) {
if env::var_os("CARGO_INCREMENTAL").is_none() {
command.env("CARGO_INCREMENTAL", "0");
}
if env::var_os("RUSTUP_HOME").is_none()
&& let Some(rustup_home) = infer_rustup_home()?
{
command.env("RUSTUP_HOME", rustup_home);
}
Ok(())
}
fn append_flag_if_missing(flags: &mut String, flag: &str) {
@ -117,6 +127,28 @@ fn cargo_dylint_binary_name() -> &'static str {
}
}
fn infer_rustup_home() -> Result<Option<OsString>, String> {
let output = Command::new("rustup")
.args(["show", "home"])
.output()
.map_err(|err| format!("failed to query rustup home via `rustup show home`: {err}"))?;
if !output.status.success() {
return Err(format!(
"`rustup show home` failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
let home = String::from_utf8(output.stdout)
.map_err(|err| format!("`rustup show home` returned invalid UTF-8: {err}"))?;
let home = home.trim();
if home.is_empty() {
Ok(None)
} else {
Ok(Some(OsString::from(home)))
}
}
fn ensure_exists(path: &Path, label: &str) -> Result<(), String> {
if path.exists() {
Ok(())
@ -158,7 +190,90 @@ fn find_bundled_library(library_dir: &Path) -> Result<PathBuf, String> {
Ok(first)
}
fn prepare_library_path_for_dylint(library_path: &Path) -> Result<PathBuf, String> {
let Some(normalized_filename) = normalize_nightly_library_filename(library_path) else {
return Ok(library_path.to_path_buf());
};
let temp_dir = env::temp_dir().join(format!(
"argument-comment-lint-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|err| format!("failed to compute timestamp for temp dir: {err}"))?
.as_nanos()
));
fs::create_dir_all(&temp_dir).map_err(|err| {
format!(
"failed to create temporary directory {}: {err}",
temp_dir.display()
)
})?;
let normalized_path = temp_dir.join(normalized_filename);
fs::copy(library_path, &normalized_path).map_err(|err| {
format!(
"failed to copy packaged library {} to {}: {err}",
library_path.display(),
normalized_path.display()
)
})?;
Ok(normalized_path)
}
fn normalize_nightly_library_filename(library_path: &Path) -> Option<String> {
let stem = library_path.file_stem()?.to_string_lossy();
let extension = library_path.extension()?.to_string_lossy();
let (lib_name, toolchain) = stem.rsplit_once('@')?;
let normalized_toolchain = normalize_nightly_toolchain(toolchain)?;
Some(format!("{lib_name}@{normalized_toolchain}.{extension}"))
}
fn normalize_nightly_toolchain(toolchain: &str) -> Option<String> {
let parts: Vec<_> = toolchain.split('-').collect();
if parts.len() > 4
&& parts[0] == "nightly"
&& parts[1].len() == 4
&& parts[2].len() == 2
&& parts[3].len() == 2
&& parts[1..4]
.iter()
.all(|part| part.chars().all(|ch| ch.is_ascii_digit()))
{
Some(format!("nightly-{}-{}-{}", parts[1], parts[2], parts[3]))
} else {
None
}
}
fn exit_code_from_status(code: Option<i32>) -> ExitCode {
code.and_then(|value| u8::try_from(value).ok())
.map_or_else(|| ExitCode::from(1), ExitCode::from)
}
#[cfg(test)]
mod tests {
use super::normalize_nightly_library_filename;
use std::path::Path;
#[test]
fn strips_host_triple_from_nightly_filename() {
assert_eq!(
normalize_nightly_library_filename(Path::new(
"libargument_comment_lint@nightly-2025-09-18-aarch64-apple-darwin.dylib"
)),
Some(String::from(
"libargument_comment_lint@nightly-2025-09-18.dylib"
))
);
}
#[test]
fn leaves_unqualified_nightly_filename_alone() {
assert_eq!(
normalize_nightly_library_filename(Path::new(
"libargument_comment_lint@nightly-2025-09-18.dylib"
)),
None
);
}
}