Skip to content

Add support for Cygwin#2730

Merged
briansmith merged 3 commits intobriansmith:mainfrom
500-internal-server-error:fix-cygwin-build
Mar 23, 2026
Merged

Add support for Cygwin#2730
briansmith merged 3 commits intobriansmith:mainfrom
500-internal-server-error:fix-cygwin-build

Conversation

@500-internal-server-error
Copy link
Copy Markdown
Contributor

@500-internal-server-error 500-internal-server-error commented Oct 25, 2025

cargo test --release --all-features passes on x86_64-pc-windows-gnu, x86_64-pc-cygwin, and x86_64-unknown-linux-gnu, at least on my machine.

@ognevny
Copy link
Copy Markdown

ognevny commented Nov 11, 2025

thanks!

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

Rebased and fixed merge conflict.

build.rs Outdated
fn join_components_with_forward_slashes(path: &Path) -> OsString {
let parts = path.components().map(|c| c.as_os_str()).collect::<Vec<_>>();
let mut parts = path.components().map(|c| c.as_os_str()).collect::<Vec<_>>();
if env::var(&env::HOST).unwrap().contains("cygwin") {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the problem that you are trying to solve here? Could you add a #[test] that demonstrates it? Isn't it something that would also affect regular Windows builds?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is explained in the comment below. Without this hack, the build hangs and eventually fails. \\path in Windows denotes a UNC path (which can be an IP or a hostname), which Cygwin preserves in its emulation of Unix paths (in this case, turning it into //path), as allowed by POSIX. Thus, //path will try to access a network share with a hostname of path (looking like a hang in the meantime) and fail. This often trips up scripts that assume the Linux behavior. This is not a problem for Windows builds because it uses the traditional DOS paths. What the #[test] be for? Checking the input and output of the path transformation?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the input and output of the path transformation?

Yes, in particular for an input that does the wrong thing without your patch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it seems I cannot put #[test]s inside build.rs directly. Do you have a suggestion on where to put it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've used a simple assert_eq! and assert_ne! for now.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored build.rs to make it more testable in PR #2807.

Branch b/join_components_with_forward_slashes-2, based on that PR, demonstrates where to put the tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the refactor has moved/transformed the problem too:

  1. that function is needed/used only at build time
  2. but it is tested like it will be used at runtime
  3. and now it cannot test the host, which is the important bit

For now I've used the target (and thus assuming HOST == TARGET / target_os) but I don't think this is ideal.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

Can squash/cleanup commits if desired.

build.rs Outdated
define_env! { pub CARGO_PKG_VERSION_PATCH: SetByCargo }
define_env! { pub CARGO_PKG_VERSION_PRE: SetByCargo }
define_env! { pub DEBUG: SetByCargo }
define_env! { pub HOST: SetByCargo }
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might still be needed, the build.rs refactoring has moved/transformed the problem as explained elsewhere.

build.rs Outdated
fn join_components_with_forward_slashes(path: &Path) -> OsString {
let parts = path.components().map(|c| c.as_os_str()).collect::<Vec<_>>();
let mut parts = path.components().map(|c| c.as_os_str()).collect::<Vec<_>>();
if env::var(&env::HOST).unwrap().contains("cygwin") {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored build.rs to make it more testable in PR #2807.

Branch b/join_components_with_forward_slashes-2, based on that PR, demonstrates where to put the tests.

@500-internal-server-error 500-internal-server-error force-pushed the fix-cygwin-build branch 4 times, most recently from fb322c7 to 2e385a1 Compare March 18, 2026 12:23
@briansmith
Copy link
Copy Markdown
Owner

Please check out PR #2808 and see if that resolves the path mangling issue you can into. IIUC, basically we were always rewriting a leading slash ("/") or backslash ("\") into two forward slashes ("//"), and instead we should just rewrite them to a single forward slash, regardless of OS. (And also there are some kinds of paths that we shouldn't try to rewrite, "verbatim" paths.)

@briansmith
Copy link
Copy Markdown
Owner

rust-lang/rust#141864 has details on Cygwin path handling and Cygwin's implementation of std::path.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

I would like to understand, though, why are we even rewriting paths? Unfortunately the original commit doesn't tell much about why, and the original PR review only reports a vague failure, with a dead reference to a CI run. I would love to know more details about the root cause of that failure, I don't think it is likely patching paths was the only/correct solution.

@briansmith
Copy link
Copy Markdown
Owner

I would like to understand, though, why are we even rewriting paths? Unfortunately the original commit doesn't tell much about why, and the original PR review only reports a vague failure, with a dead reference to a CI run. I would love to know more details about the root cause of that failure, I don't think it is likely patching paths was the only/correct solution.

I think it boils down to an unwillingness to modify the Perl code to do the right thing, or to even read its regex-y stuff to understand why it failed in that scenario. I don't even really know any more.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

And also there are some kinds of paths that we shouldn't try to rewrite, "verbatim" paths.

Do we really need to handle them? AFAICT we always get a traditional DOS path.

unwillingness to modify the Perl code

The pattern is indeed pervasive, and modifications are probably ideally done upstream.

even read its regex-y stuff

It seems to be a handrolled dirname to use as a starting point to find x86_64-xlate.pl.

understand why it failed in that scenario

That method of locating x86_64-xlate.pl has some assumptions built into it that can fail, however the lack of a more detailed explanation of the failure is not making it easy for me to determine what actually failed. Personally I would try to just ignore the problem and see if it comes back but I can understand if you're unwilling to do that for the sake of a simpler solution.

@briansmith
Copy link
Copy Markdown
Owner

OK, go ahead and rebase this on main and see if that simplifies things for you.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

Hmm, I was thinking as simple as a dumb byte replace, simply flipping any \ into a /. Given only traditional DOS paths have been observed, would that be alright with you?

@briansmith
Copy link
Copy Markdown
Owner

Hmm, I was thinking as simple as a dumb byte replace, simply flipping any \ into a /.

My expectation that the new logic that's already in the main branch should also work well enough for Cygwin. Before that change, it replaced r#"C:\foo"# with r#"C:/foo"# and "/" with "//", respectively; after that change, the results are "c:/foo" and "/", respectively. Thus, your issue with "//" should no longer occur, and the other weirdness you experienced should be gone too. So I am hoping there is little or nothing you need to do.

Given only traditional DOS paths have been observed, would that be alright with you?

In the main branch we already punt on all verbatim paths. My understanding is that the new handling absolute paths should be good for what you need, and the new support for UNCs shouldn't get in the way of your work either. I.e. I am hoping you don't need to change anything in the new path.rs.

@briansmith
Copy link
Copy Markdown
Owner

Also, to help understand the philosophy here, my goal is to have as few Cygwin-specific bits as possible. In general we try to not have OS- and Arch- specific logic unless there is a performance benefit to it.

That is why, for example, I just fixed the path mangling for all targets instead of adding a workaround specific to Cygwin; the improvement was, AFAICT, more aesthetic than it was practically useful for other targets, but that aesthetic improvement fixes (IIUC) the problem Cygwin had, in a universal way.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

So I am hoping there is little or nothing you need to do.

Unfortunately, that is not the case, with no changes, this is what happens:

thread 'join_components_with_forward_slashes_tests' (2) panicked at tests/build_path_tests.rs:367:5:
assertion `left == right` failed
  left: [("\\", "/", "\\"), ("\\\\", "/", "\\\\"), ("\\\\foo", "/foo", "\\\\foo"), ("\\\\foo\\bar", "//foo/bar/", "\\\\foo\\bar"), ("\\\\server\\share", "//server/share/", "\\\\server\\share"), ("\\\\server\\share\\", "//server/share/", "\\\\server\\share\\"), ("\\\\server\\share\\foo", "//server/share/foo", "\\\\server\\share\\foo"), ("\\\\server\\share\\foo\\bar", "//server/share/foo/bar", "\\\\server\\share\\foo\\bar"), ("\\\\server\\share\\foo\\bar\\", "//server/share/foo/bar", "\\\\server\\share\\foo\\bar\\"), ("//server\\share", "//server/share/", "/server\\share"), ("//server\\share/", "//server/share/", "/server\\share"), ("//server\\share/foo", "//server/share/foo", "/server\\share/foo"), ("//server\\share/foo/bar", "//server/share/foo/bar", "/server\\share/foo/bar"), ("//server\\share/foo/bar/", "//server/share/foo/bar", "/server\\share/foo/bar"), ("\\\\server\\share", "//server/share/", "\\\\server\\share"), ("\\\\server\\share/", "//server/share/", "\\\\server\\share"), ("\\\\server\\share/foo", "//server/share/foo", "\\\\server\\share/foo"), ("\\\\server\\share/foo/bar", "//server/share/foo/bar", "\\\\server\\share/foo/bar"), ("\\\\server\\share/foo/bar/", "//server/share/foo/bar", "\\\\server\\share/foo/bar"), ("//server/share\\", "//server/share/", "/server/share\\"), ("//server/share\\foo", "//server/share/foo", "/server/share\\foo"), ("//server/share/foo\\bar", "//server/share/foo/bar", "/server/share/foo\\bar"), ("//server/share\\foo/bar/", "//server/share/foo/bar", "/server/share\\foo/bar"), ("C:\\foo", "C:/foo", "C:\\foo"), ("a\\b", "a/b", "a\\b"), ("\\a\\b", "/a/b", "\\a\\b"), (".\\b", "./b", ".\\b")]
 right: []

And if I change

pub const TARGET_SUPPORTS_UNCS_AND_BACKSLASHES: bool = cfg!(target_os = "windows");
into

-pub const TARGET_SUPPORTS_UNCS_AND_BACKSLASHES: bool = cfg!(target_os = "windows");
+pub const TARGET_SUPPORTS_UNCS_AND_BACKSLASHES: bool = cfg!(any(target_os = "windows", target_os = "cygwin"));

then I get less but still some failures:

thread 'join_components_with_forward_slashes_tests' (2) panicked at tests/build_path_tests.rs:367:5:
assertion `left == right` failed
  left: [("//foo/bar", "/foo/bar", "//foo/bar/"), ("//server/share", "/server/share", "//server/share/"), ("//server/share/", "/server/share", "//server/share/"), ("//server/share/foo", "/server/share/foo", "//server/share/foo"), ("//server/share/foo/bar", "/server/share/foo/bar", "//server/share/foo/bar"), ("//server/share/foo/bar/", "/server/share/foo/bar", "//server/share/foo/bar")]
 right: []

Also, to help understand the philosophy here

Sure, I can understand that.

I just fixed the path mangling for all targets

My idea was something like:

pub fn join_components_with_forward_slashes(path: &Path) -> OsString {
    unsafe {
        OsString::from_encoded_bytes_unchecked(
            path
                .as_os_str()
                .to_owned()
                .into_encoded_bytes()
                .iter()
                .map(|&x| if x == b'\\' { b'/' } else { x })
                .collect()
        )
    }
}

I was also questioning the utility of this function in #2730 (comment), because I tried something like:

pub fn join_components_with_forward_slashes(path: &Path) -> OsString {
    path.as_os_str().to_owned()
}

and found no issues with my local setup. Then again I only have x86 machines, but I don't see why it should be arch specific problem.

@briansmith
Copy link
Copy Markdown
Owner

("//foo/bar", "/foo/bar", "//foo/bar/")

It seems like libstd on Windows parses this as a UNC prefix "\foo\bar" follow by a "root", but on Cygwin it parses it in a Unixy way.

Which version of the Rust toolchain are you using? It seems like the handling of Windows prefixes was added in 1.89?

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

Which version of the Rust toolchain are you using? It seems like the handling of Windows prefixes was added in 1.89?

$ rustc -vV
rustc 1.94.0 (4a4ef493e 2026-03-02) (Rev4, Built by MSYS2 project)
binary: rustc
commit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db
commit-date: 2026-03-02
host: x86_64-pc-cygwin
release: 1.94.0
LLVM version: 21.1.8

@briansmith
Copy link
Copy Markdown
Owner

Well, "//server/share/foo" => "/server/share/foo" is wrong. Is it wrong because of a bug in my code or is the Cygwin std::path implementation somehow not parsing "//server/share" as a UNC prefix?

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

500-internal-server-error commented Mar 21, 2026

This code:

fn main() {
    println!("{:?}", std::path::Path::new("//server/path").components());
}

prints Components([RootDir, Normal("server"), Normal("path")]) on x86_64-pc-cygwin and x86_64-unknown-linux-gnu but Components([Prefix(PrefixComponent { raw: "//server/path", parsed: UNC("server", "path") }), RootDir]) on x86_64-pc-windows-gnu. However, the Cygwin runtime will treat it like a Windows UNC path. So it's probably the Rust std::path implementation for Cygwin that is wrong/incomplete. However, adding such support is going to introduce even more wrinkles and OS/platform specific code, which is why I suggested the simple byte replace.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

I'll push my rebase in the meantime.

Copy link
Copy Markdown
Owner

@briansmith briansmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #2811 is intended to address the weird parsing of "//" on Cygwin.

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

Rebased.

Copy link
Copy Markdown
Owner

@briansmith briansmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this is just about as minimal as I can expect. Just one (x3) nit here.

Does the new logic in path.rs work for you?

@500-internal-server-error
Copy link
Copy Markdown
Contributor Author

500-internal-server-error commented Mar 22, 2026

Does the new logic in path.rs work for you?

cargo test --all-features passes on x86_64-pc-windows-gnu and x86_64-pc-cygwin, at least on my machine.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.49%. Comparing base (56ac0fb) to head (fc857a7).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2730   +/-   ##
=======================================
  Coverage   96.49%   96.49%           
=======================================
  Files         207      207           
  Lines       20430    20430           
  Branches      520      520           
=======================================
  Hits        19713    19713           
  Misses        599      599           
  Partials      118      118           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Owner

@briansmith briansmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@briansmith briansmith merged commit 12e0ad1 into briansmith:main Mar 23, 2026
161 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants