Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b6bf1fc

Browse files
jdxclaude
andauthored
fix: add Windows support by guarding Unix-specific file permission APIs (jdx#349)
## Summary The v1.17.0 release workflow failed on Windows builds because Unix-specific file permission APIs were used without platform guards. ## Problem The Windows build was failing with errors like: ``` error[E0433]: failed to resolve: could not find `unix` in `os` ``` This occurred in two files: - `src/cli/migrate/pre_commit.rs:1707` - `use std::os::unix::fs::PermissionsExt` - `src/cli/util/check_executables_have_shebangs.rs:3` - `use std::os::unix::fs::PermissionsExt` ## Solution Added proper `#[cfg(unix)]` guards around all Unix-specific code: 1. **check_executables_have_shebangs.rs**: - Guarded `PermissionsExt` import with `#[cfg(unix)]` - Split `is_executable()` implementation: Unix checks execute bits, Windows returns false - Marked Unix-specific tests with `#[cfg(unix)]` 2. **pre_commit.rs**: - Wrapped entire `make_scripts_executable()` Unix implementation in `#[cfg(unix)]` block - Added Windows no-op implementation (Windows uses file associations, not execute bits) ## Behavior - **Unix/Linux/macOS**: No change in behavior - still checks/sets execute permissions - **Windows**: Functions compile and run but don't check/set Unix permissions (which don't exist on Windows) ## Test Plan - [x] Compiles successfully on macOS (tested locally) - [x] Will verify Windows builds pass in CI Fixes the Windows build failures in https://github.com/jdx/hk/actions/runs/18261671379 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Guards Unix-only permission code for Windows, enables staged rename detection in git status, and adds tests for renamed files. > > - **Compatibility (Windows)** > - Wrap Unix-specific permission logic with `#[cfg(unix)]` and provide Windows no-ops in `src/cli/migrate/pre_commit.rs::make_scripts_executable` and `src/cli/util/check_executables_have_shebangs.rs`. > - Gate `PermissionsExt` imports and Unix-specific tests with `#[cfg(unix)]`; on Windows `is_executable` returns `false`. > - **Git status improvements** > - Enable rename detection with libgit2 via `status_options.renames_head_to_index(true)` and shell fallback by removing `--no-renames` from `git status --porcelain`. > - **Tests** > - Add `test/staged_renamed_files.bats` to assert `git.staged_renamed_files` detection (single and multiple renames). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9a7c15e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude <[email protected]>
1 parent 25a4b15 commit b6bf1fc

File tree

2 files changed

+52
-29
lines changed

2 files changed

+52
-29
lines changed

src/cli/migrate/pre_commit.rs

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1704,46 +1704,56 @@ impl PreCommit {
17041704

17051705
/// Make Python scripts and other executable files in the vendor directory executable
17061706
fn make_scripts_executable(vendor_path: &Path) -> Result<()> {
1707-
use std::os::unix::fs::PermissionsExt;
1708-
1709-
// Find all Python files and shell scripts
1710-
if let Ok(entries) = std::fs::read_dir(vendor_path) {
1711-
for entry in entries.flatten() {
1712-
let path = entry.path();
1713-
if path.is_file() {
1714-
if let Some(ext) = path.extension() {
1715-
if ext == "py" || ext == "sh" {
1716-
// Make executable
1717-
if let Ok(metadata) = std::fs::metadata(&path) {
1718-
let mut perms = metadata.permissions();
1719-
perms.set_mode(perms.mode() | 0o111); // Add execute permission
1720-
let _ = std::fs::set_permissions(&path, perms);
1721-
}
1722-
}
1723-
}
1724-
}
1725-
}
1726-
}
1707+
#[cfg(unix)]
1708+
{
1709+
use std::os::unix::fs::PermissionsExt;
17271710

1728-
// Also check pre_commit_hooks subdirectory if it exists
1729-
let hooks_dir = vendor_path.join("pre_commit_hooks");
1730-
if hooks_dir.exists() {
1731-
if let Ok(entries) = std::fs::read_dir(&hooks_dir) {
1711+
// Find all Python files and shell scripts
1712+
if let Ok(entries) = std::fs::read_dir(vendor_path) {
17321713
for entry in entries.flatten() {
17331714
let path = entry.path();
17341715
if path.is_file() {
17351716
if let Some(ext) = path.extension() {
1736-
if ext == "py" {
1717+
if ext == "py" || ext == "sh" {
1718+
// Make executable
17371719
if let Ok(metadata) = std::fs::metadata(&path) {
17381720
let mut perms = metadata.permissions();
1739-
perms.set_mode(perms.mode() | 0o111);
1721+
perms.set_mode(perms.mode() | 0o111); // Add execute permission
17401722
let _ = std::fs::set_permissions(&path, perms);
17411723
}
17421724
}
17431725
}
17441726
}
17451727
}
17461728
}
1729+
1730+
// Also check pre_commit_hooks subdirectory if it exists
1731+
let hooks_dir = vendor_path.join("pre_commit_hooks");
1732+
if hooks_dir.exists() {
1733+
if let Ok(entries) = std::fs::read_dir(&hooks_dir) {
1734+
for entry in entries.flatten() {
1735+
let path = entry.path();
1736+
if path.is_file() {
1737+
if let Some(ext) = path.extension() {
1738+
if ext == "py" {
1739+
if let Ok(metadata) = std::fs::metadata(&path) {
1740+
let mut perms = metadata.permissions();
1741+
perms.set_mode(perms.mode() | 0o111);
1742+
let _ = std::fs::set_permissions(&path, perms);
1743+
}
1744+
}
1745+
}
1746+
}
1747+
}
1748+
}
1749+
}
1750+
}
1751+
1752+
#[cfg(not(unix))]
1753+
{
1754+
// On Windows, executable permissions are not managed the same way.
1755+
// Files with .py, .sh extensions are typically executed through their interpreters.
1756+
let _ = vendor_path; // Suppress unused variable warning
17471757
}
17481758

17491759
Ok(())

src/cli/util/check_executables_have_shebangs.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::Result;
22
use std::fs;
3+
#[cfg(unix)]
34
use std::os::unix::fs::PermissionsExt;
45
use std::path::PathBuf;
56

@@ -37,10 +38,19 @@ fn is_executable(path: &PathBuf) -> Result<bool> {
3738
return Ok(false);
3839
}
3940

40-
let permissions = metadata.permissions();
41+
#[cfg(unix)]
42+
{
43+
let permissions = metadata.permissions();
44+
// Check if any execute bit is set
45+
Ok(permissions.mode() & 0o111 != 0)
46+
}
4147

42-
// Check if any execute bit is set
43-
Ok(permissions.mode() & 0o111 != 0)
48+
#[cfg(not(unix))]
49+
{
50+
// On Windows, we can't reliably check execute permissions via file attributes.
51+
// Return false so we don't incorrectly flag files.
52+
Ok(false)
53+
}
4454
}
4555

4656
fn has_shebang(path: &PathBuf) -> Result<bool> {
@@ -59,6 +69,7 @@ fn has_shebang(path: &PathBuf) -> Result<bool> {
5969
mod tests {
6070
use super::*;
6171
use std::fs;
72+
#[cfg(unix)]
6273
use std::os::unix::fs::PermissionsExt;
6374
use tempfile::NamedTempFile;
6475

@@ -100,6 +111,7 @@ mod tests {
100111
}
101112

102113
#[test]
114+
#[cfg(unix)]
103115
fn test_is_executable() {
104116
let file = NamedTempFile::new().unwrap();
105117
fs::write(file.path(), b"#!/bin/bash\necho hello").unwrap();
@@ -114,6 +126,7 @@ mod tests {
114126
}
115127

116128
#[test]
129+
#[cfg(unix)]
117130
fn test_not_executable() {
118131
let file = NamedTempFile::new().unwrap();
119132
fs::write(file.path(), b"#!/bin/bash\necho hello").unwrap();

0 commit comments

Comments
 (0)