diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index f1965c50f..4fa79b6c9 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -201,6 +201,13 @@ Git, caches it under `CARGO_TARGET_DIR`, and sets `Z3_SYS_BUNDLED_DIR_OVERRIDE`. This bypasses the unauthenticated GitHub Contents API lookup that can fail with HTTP 403 on shared networks. +The repository-wide `mise run pre-commit` task is supported on Windows. +`tasks/rust.toml` and `tasks/test.toml` route compiler-bearing checks through +the wrapper for the native host target, while `tasks/markdown.toml` provides a +Windows-safe dependency setup command. Keep existing Unix `run` bodies +unchanged when adding `run_windows` behavior. Linux installer and packaging +asset tests skip explicitly; cross-platform checks continue to run. + ### Step 6: mise check on x86_64-pc-windows-msvc ```powershell diff --git a/.agents/skills/build-openshell-mxc-windows/reference.md b/.agents/skills/build-openshell-mxc-windows/reference.md index 8768f8970..285744820 100644 --- a/.agents/skills/build-openshell-mxc-windows/reference.md +++ b/.agents/skills/build-openshell-mxc-windows/reference.md @@ -165,6 +165,13 @@ After every cfg gate change, run `cargo check --workspace` on the Linux baseline The Windows build path is additive. Keep the repository's Linux `mise run ci`, default Cargo tasks, and Linux documentation unchanged. Windows automation lives in `tasks/windows.toml` and delegates to `tasks/scripts/windows-msvc.ps1`. +On Windows, `mise run pre-commit` routes `rust:check`, `rust:lint`, and +`test:rust` through the same wrapper for the host-native target. Shared task +definitions retain their Unix commands. Only Linux installer and +service/RPM-packaging tests skip. The Windows Clippy command allows unused +imports, dead code, and unused async functions caused by cfg-gated stubs; other +warnings remain errors. + | Task | Expected behavior | |---|---| | `mise run --skip-tools windows:check:x64` | Runs x64 MSVC `cargo check --workspace` | diff --git a/architecture/windows-msvc-build.md b/architecture/windows-msvc-build.md index 3049555da..c27a556e5 100644 --- a/architecture/windows-msvc-build.md +++ b/architecture/windows-msvc-build.md @@ -10,7 +10,9 @@ Podman, or VM runtime host. - Keep the Linux and macOS build paths unchanged. - Preserve gateway configuration parsing for all existing compute driver names. - Return clear unsupported errors when a Windows gateway is configured to use Docker, Kubernetes, Podman, or VM. -- Validate the Windows lane through mise tasks that are separate from the default Linux `ci` task. +- Keep dedicated `windows:*` validation tasks while allowing the repository-wide + `pre-commit` task to delegate compiler-bearing Rust checks to the native + Windows MSVC environment. ## Non-Goals @@ -59,6 +61,14 @@ Visual Studio's `VsDevCmd.bat` with `vswhere` or by enumerating installed release directories, validates the requested compiler and ARM64 Spectre libraries, adds rustup MSVC targets, clears inherited `RUSTC_WRAPPER`, and keeps build artifacts under the normal Cargo target tree. +On Windows, the generic `rust:check`, `rust:lint`, and `test:rust` tasks call +the same wrapper with the host-native MSVC target. The wrapper preserves the +Unix Cargo commands on Linux and macOS, excludes unsupported Windows runtime +packages, and runs the server test-support suite separately. Windows Clippy +continues to deny all warnings except unused imports, dead code, and unused +async functions caused by cfg-gated Windows stubs. Repository-wide pre-commit +skips only Linux-specific installer and packaging-asset tests; its +cross-platform Python, Markdown, license, and documentation checks still run. Test tasks require the Rust target architecture to match the Windows host, so an ARM64 test result is native coverage rather than x64 emulation coverage. By default it enables bundled Z3 for reproducible Windows builds. When diff --git a/crates/openshell-bootstrap/src/build_windows.rs b/crates/openshell-bootstrap/src/build_windows.rs index 6337919b2..93af1345d 100644 --- a/crates/openshell-bootstrap/src/build_windows.rs +++ b/crates/openshell-bootstrap/src/build_windows.rs @@ -8,6 +8,8 @@ use std::path::Path; use miette::Result; +// Keep this stub's signature aligned with the supported-platform implementation. +#[allow(clippy::implicit_hasher)] pub async fn build_local_image( _dockerfile_path: &Path, _tag: &str, diff --git a/crates/openshell-server/src/config_file.rs b/crates/openshell-server/src/config_file.rs index 3e984a891..39166333b 100644 --- a/crates/openshell-server/src/config_file.rs +++ b/crates/openshell-server/src/config_file.rs @@ -277,6 +277,7 @@ pub enum ConfigFileError { /// /// Returns `Ok(ConfigFile::default())` for an empty file (the gateway then /// falls back entirely to CLI/env/built-in defaults). +#[cfg_attr(target_os = "windows", allow(clippy::result_large_err))] pub fn load(path: &Path) -> Result { let contents = std::fs::read_to_string(path).map_err(|source| ConfigFileError::Io { path: path.to_path_buf(), diff --git a/crates/openshell-supervisor-network/src/identity.rs b/crates/openshell-supervisor-network/src/identity.rs index 5e89c3503..4f9088837 100644 --- a/crates/openshell-supervisor-network/src/identity.rs +++ b/crates/openshell-supervisor-network/src/identity.rs @@ -21,10 +21,8 @@ use tracing::debug; #[derive(Clone)] struct FileFingerprint { len: u64, - mtime_sec: i64, - mtime_nsec: i64, - ctime_sec: i64, - ctime_nsec: i64, + mtime: Option<(i64, i64)>, + ctime: Option<(i64, i64)>, #[cfg(unix)] dev: u64, #[cfg(unix)] @@ -33,12 +31,20 @@ struct FileFingerprint { impl FileFingerprint { fn from_metadata(metadata: &Metadata) -> Self { + #[cfg(unix)] + let (mtime, ctime) = ( + Some((metadata.mtime(), metadata.mtime_nsec())), + Some((metadata.ctime(), metadata.ctime_nsec())), + ); + #[cfg(not(unix))] + let (mtime, ctime) = ( + metadata.modified().ok().and_then(system_time_parts), + metadata.created().ok().and_then(system_time_parts), + ); Self { len: metadata.len(), - mtime_sec: metadata.mtime(), - mtime_nsec: metadata.mtime_nsec(), - ctime_sec: metadata.ctime(), - ctime_nsec: metadata.ctime_nsec(), + mtime, + ctime, #[cfg(unix)] dev: metadata.dev(), #[cfg(unix)] @@ -47,13 +53,22 @@ impl FileFingerprint { } } +#[cfg(not(unix))] +fn system_time_parts(time: std::time::SystemTime) -> Option<(i64, i64)> { + let duration = time.duration_since(std::time::UNIX_EPOCH).ok()?; + let seconds = i64::try_from(duration.as_secs()).ok()?; + Some((seconds, i64::from(duration.subsec_nanos()))) +} + impl PartialEq for FileFingerprint { fn eq(&self, other: &Self) -> bool { self.len == other.len - && self.mtime_sec == other.mtime_sec - && self.mtime_nsec == other.mtime_nsec - && self.ctime_sec == other.ctime_sec - && self.ctime_nsec == other.ctime_nsec + && self.mtime.is_some() + && other.mtime.is_some() + && self.mtime == other.mtime + && self.ctime.is_some() + && other.ctime.is_some() + && self.ctime == other.ctime && { #[cfg(unix)] { diff --git a/python/openshell/sandbox.py b/python/openshell/sandbox.py index b62a9c885..db0db85d8 100644 --- a/python/openshell/sandbox.py +++ b/python/openshell/sandbox.py @@ -1531,7 +1531,17 @@ class _OidcRefresher: f.write(payload) with contextlib.suppress(OSError): tmp_path.chmod(0o600) - tmp_path.replace(path) + for attempt in range(20): + try: + tmp_path.replace(path) + break + except PermissionError: + # Concurrent MoveFileExW calls can briefly lock the + # destination on Windows. Retry that transient sharing + # violation while preserving POSIX rename behavior. + if os.name != "nt" or attempt == 19: + raise + time.sleep(0.01) except BaseException: # Clean up our tmp on failure so we don't leave orphaned # `.oidc_token..tmp` files lying around. The replace diff --git a/scripts/update_license_headers.py b/scripts/update_license_headers.py index f56dbc293..0f2d87ddd 100755 --- a/scripts/update_license_headers.py +++ b/scripts/update_license_headers.py @@ -101,7 +101,7 @@ def find_repo_root() -> Path: def is_excluded(rel: Path) -> bool: """Return True if a path should be skipped.""" - rel_str = str(rel) + rel_str = rel.as_posix() # Exact filename exclusions. if rel.name in EXCLUDE_FILES: diff --git a/tasks/markdown.toml b/tasks/markdown.toml index fb070fd68..d0cba9d49 100644 --- a/tasks/markdown.toml +++ b/tasks/markdown.toml @@ -9,6 +9,7 @@ run = """ cd scripts/lint-mermaid npm ci --no-audit --no-fund --silent """ +run_windows = "npm --prefix scripts/lint-mermaid ci --no-audit --no-fund --silent" hide = true sources = ["scripts/lint-mermaid/package.json", "scripts/lint-mermaid/package-lock.json"] outputs = ["scripts/lint-mermaid/node_modules/.package-lock.json"] diff --git a/tasks/rust.toml b/tasks/rust.toml index c51fe4c05..4d4893cf8 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -6,6 +6,7 @@ ["rust:check"] description = "Check all Rust crates for errors" run = "cargo check --workspace" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 check native" hide = true ["rust:lint"] @@ -14,6 +15,7 @@ run = [ "cargo clippy --workspace --all-targets -- -D warnings", "cargo clippy --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings", ] +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 lint native" hide = true ["rust:format"] diff --git a/tasks/scripts/windows-msvc.ps1 b/tasks/scripts/windows-msvc.ps1 index c27e4f4bd..1331d5d12 100644 --- a/tasks/scripts/windows-msvc.ps1 +++ b/tasks/scripts/windows-msvc.ps1 @@ -6,11 +6,11 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] - [ValidateSet("check", "build", "test", "test-unsupported", "artifacts", "ci")] + [ValidateSet("check", "lint", "build", "test", "test-precommit", "test-unsupported", "artifacts", "ci")] [string] $Action, [Parameter(Position = 1)] - [ValidateSet("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc", "all")] + [ValidateSet("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc", "native", "all")] [string] $Target = "all", [string] $LogDir @@ -37,7 +37,9 @@ if ([string]::IsNullOrWhiteSpace($TargetDir)) { $TargetDir = Join-Path $RepoRoot "target" } -$UnsupportedDriverPackageExcludes = "--exclude openshell-driver-docker --exclude openshell-driver-kubernetes --exclude openshell-driver-podman --exclude openshell-driver-vm" +$UnsupportedDriverPackageExcludes = "--exclude openshell-driver-docker --exclude openshell-driver-kubernetes --exclude openshell-driver-podman --exclude openshell-driver-vm --exclude openshell-supervisor-process" +$WindowsClippyPackageExcludes = $UnsupportedDriverPackageExcludes +$WindowsClippyLintArgs = "-D warnings -A dead-code -A unused-imports -A clippy::unused-async" $BundledZ3WorkspaceFeatures = "--features openshell-prover/bundled-z3" $BundledZ3ServerFeatures = "--features openshell-server/bundled-z3,openshell-prover/bundled-z3" $BundledZ3Repository = "https://github.com/Z3Prover/z3.git" @@ -309,6 +311,12 @@ function Assert-NativeTestTarget([string] $RustTarget) { } function Get-SelectedTargets([string] $RequestedTarget) { + if ($RequestedTarget -eq "native") { + switch (Get-HostArch) { + "arm64" { return @("aarch64-pc-windows-msvc") } + default { return @("x86_64-pc-windows-msvc") } + } + } if ($RequestedTarget -eq "all") { $targets = @("x86_64-pc-windows-msvc") if ($env:OPENSHELL_MXC_SKIP_ARM64 -ne "1") { @@ -505,6 +513,17 @@ function Invoke-Check([string] $RustTarget) { -LogName "build-$RustTarget-check.log" } +function Invoke-Lint([string] $RustTarget) { + Invoke-VsCargo ` + -RustTarget $RustTarget ` + -CargoArgs "cargo clippy --workspace --all-targets --no-deps $WindowsClippyPackageExcludes --target $RustTarget $Z3WorkspaceFeatures -- $WindowsClippyLintArgs" ` + -LogName "lint-$RustTarget-workspace.log" + Invoke-VsCargo ` + -RustTarget $RustTarget ` + -CargoArgs "cargo clippy --manifest-path e2e/rust/Cargo.toml --all-targets --no-deps --target $RustTarget -- $WindowsClippyLintArgs" ` + -LogName "lint-$RustTarget-e2e.log" +} + function Invoke-Build([string] $RustTarget) { Invoke-VsCargo ` -RustTarget $RustTarget ` @@ -520,6 +539,18 @@ function Invoke-Test([string] $RustTarget) { -LogName "test-$RustTarget.log" } +function Invoke-PreCommitTest([string] $RustTarget) { + Assert-NativeTestTarget $RustTarget + Invoke-VsCargo ` + -RustTarget $RustTarget ` + -CargoArgs "cargo test --workspace --exclude openshell-server $UnsupportedDriverPackageExcludes --target $RustTarget --no-fail-fast $Z3WorkspaceFeatures" ` + -LogName "test-$RustTarget-precommit-workspace.log" + Invoke-VsCargo ` + -RustTarget $RustTarget ` + -CargoArgs "cargo test -p openshell-server --features test-support --target $RustTarget --no-fail-fast $Z3ServerFeatures" ` + -LogName "test-$RustTarget-precommit-server.log" +} + function Invoke-UnsupportedContractTests([string] $RustTarget) { Assert-NativeTestTarget $RustTarget @@ -579,13 +610,13 @@ if ($Action -eq "ci" -and (Get-HostArch) -ne "amd64") { } $targets = Get-SelectedTargets $Target -if ($Action -in @("test", "test-unsupported")) { +if ($Action -in @("test", "test-precommit", "test-unsupported")) { foreach ($rustTarget in $targets) { Assert-NativeTestTarget $rustTarget } } -if ($Action -in @("check", "build", "test", "test-unsupported", "ci")) { +if ($Action -in @("check", "lint", "build", "test", "test-precommit", "test-unsupported", "ci")) { $z3Features = Configure-Z3 $Z3WorkspaceFeatures = $z3Features.WorkspaceFeatures $Z3ServerFeatures = $z3Features.ServerFeatures @@ -601,6 +632,11 @@ switch ($Action) { Invoke-Check $rustTarget } } + "lint" { + foreach ($rustTarget in $targets) { + Invoke-Lint $rustTarget + } + } "build" { foreach ($rustTarget in $targets) { Invoke-Build $rustTarget @@ -612,6 +648,11 @@ switch ($Action) { Invoke-Test $rustTarget } } + "test-precommit" { + foreach ($rustTarget in $targets) { + Invoke-PreCommitTest $rustTarget + } + } "test-unsupported" { foreach ($rustTarget in $targets) { Invoke-UnsupportedContractTests $rustTarget diff --git a/tasks/test.toml b/tasks/test.toml index ed0d17d7a..f0f295c2e 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -21,6 +21,7 @@ hide = true ["test:install-sh"] description = "Run focused install.sh shell tests" run = "tasks/scripts/test-install-sh.sh" +run_windows = "echo Skipping test:install-sh: Linux glibc installer tests do not apply on Windows." hide = true ["test:build-env"] @@ -31,6 +32,7 @@ hide = true ["test:packaging-assets"] description = "Run static packaging asset tests" run = "tasks/scripts/test-packaging-assets.sh" +run_windows = "echo Skipping test:packaging-assets: Linux service and RPM assets do not apply on Windows." hide = true [e2e] @@ -57,6 +59,7 @@ run = [ "cargo test --workspace --exclude openshell-server", "cargo test -p openshell-server --features test-support", ] +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" hide = true ["test:python"]