Repository: dtolnay/cxx (language: rust)

Issue:
Build fails on Windows when `cxx.h` is already present in the `targets` folder due to cache
<!--  ❤️  -->

On Windows, the build fails when a project is built with a cache and `cxx.h` is already present. It fails to copy `cxx.h`. 


https://github.com/SunDoge/simdjson-rust/runs/8049220520?check_suite_focus=true#step:6:71

```ps1
error: failed to run custom build command for `simdjson-rust v0.2.0-alpha (D:\a\simdjson-rust\simdjson-rust)`

Caused by:
  process didn't exit successfully: `D:\a\simdjson-rust\simdjson-rust\target\debug\build\simdjson-rust-d635beff2633e3ed\build-script-build` (exit code: 1)
  --- stderr


  cxxbridge error: Failed to copy `C:\Users\runneradmin\.cargo\registry\src\github.com-1ecc6299db9ec823\cxx-1.0.73\include\cxx.h` -> `D:\a\simdjson-rust\simdjson-rust\target\debug\build\simdjson-rust-457ffe0621787f66\out\cxxbridge\include\rust\cxx.h`

  Caused by:
      The filename, directory name, or volume label syntax is incorrect. (os error 123)

```

https://github.com/SunDoge/simdjson-rust/issues/16


Discussion / hints:
Sigh &mdash; apparently `std::fs::copy` on Windows cannot copy between two different drive letters? What a strange error message from the OS in that case. I don't have access to Windows but I am able to reproduce this issue in GitHub Actions.

I tried adding some logging to look at whether the paths involved exists. The path we are copying `from` does exist, as expected. The path we are copying `to` does not exist, as expected. The `parent` directory of the path `to` does exist, and is a directory, as expected.

```console
from = "C:\\Users\\runneradmin\\.cargo\\git\\checkouts\\cxx-be839b8f5bc98a2b\\1b1f78a\\include\\cxx.h"
from.exists() = true
to = "D:\\a\\simdjson-rust\\simdjson-rust\\target\\debug\\build\\simdjson-rust-73b1080797fedd25\\out\\cxxbridge\\include\\rust\\cxx.h"
to.exists() = false
parent = "D:\\a\\simdjson-rust\\simdjson-rust\\target\\debug\\build\\simdjson-rust-73b1080797fedd25\\out\\cxxbridge\\include\\rust"
parent.exists() = true
parent.is_dir() = true

cxxbridge error: Failed to copy `C:\Users\runneradmin\.cargo\git\checkouts\cxx-be839b8f5bc98a2b\1b1f78a\include\cxx.h` -> `D:\a\simdjson-rust\simdjson-rust\target\debug\build\simdjson-rust-73b1080797fedd25\out\cxxbridge\include\rust\cxx.h`

Caused by:
    The filename, directory name, or volume label syntax is incorrect. (os error 123)
```
Correction, in the above, the path:
`D:\\a\\simdjson-rust\\simdjson-rust\\target\\debug\\build\\simdjson-rust-73b1080797fedd25\\out\\cxxbridge\\include\\rust\\cxx.h`
*does* exist, and is a symlink, where `read_link` on it reports that it points to:
`\\??\\C:\\Users\\runneradmin\\.cargo\\git\\checkouts\\cxx-be839b8f5bc98a2b\\88c2051\\include\\cxx.h`
which *does not* exist, which is why `exists()` reports false on the link. We are using `.exists()` incorrectly and should be using something like `.symlink_metadata().is_ok()`.

Fix patch:
diff --git a/gen/build/src/out.rs b/gen/build/src/out.rs
index 5fe957c97..a52aab258 100644
--- a/gen/build/src/out.rs
+++ b/gen/build/src/out.rs
@@ -8,7 +8,7 @@ pub(crate) fn write(path: impl AsRef<Path>, content: &[u8]) -> Result<()> {
     let path = path.as_ref();
 
     let mut create_dir_error = None;
-    if path.exists() {
+    if fs::exists(path) {
         if let Ok(existing) = fs::read(path) {
             if existing == content {
                 // Avoid bumping modified time with unchanged contents.
@@ -34,7 +34,7 @@ pub(crate) fn symlink_file(original: impl AsRef<Path>, link: impl AsRef<Path>) -
     let link = link.as_ref();
 
     let mut create_dir_error = None;
-    if link.exists() {
+    if fs::exists(link) {
         best_effort_remove(link);
     } else {
         let parent = link.parent().unwrap();
@@ -68,7 +68,7 @@ pub(crate) fn symlink_dir(original: impl AsRef<Path>, link: impl AsRef<Path>) ->
     let link = link.as_ref();
 
     let mut create_dir_error = None;
-    if link.exists() {
+    if fs::exists(link) {
         best_effort_remove(link);
     } else {
         let parent = link.parent().unwrap();
@@ -86,24 +86,34 @@ pub(crate) fn symlink_dir(original: impl AsRef<Path>, link: impl AsRef<Path>) ->
 fn best_effort_remove(path: &Path) {
     use std::fs;
 
-    let file_type = match if cfg!(windows) {
+    if cfg!(windows) {
         // On Windows, the correct choice of remove_file vs remove_dir needs to
         // be used according to what the symlink *points to*. Trying to use
         // remove_file to remove a symlink which points to a directory fails
         // with "Access is denied".
-        fs::metadata(path)
+        if let Ok(metadata) = fs::metadata(path) {
+            if metadata.is_dir() {
+                let _ = fs::remove_dir_all(path);
+            } else {
+                let _ = fs::remove_file(path);
+            }
+        } else if fs::symlink_metadata(path).is_ok() {
+            // The symlink might exist but be dangling, in which case there is
+            // no standard way to determine what "kind" of symlink it is. Try
+            // deleting both ways.
+            if fs::remove_dir_all(path).is_err() {
+                let _ = fs::remove_file(path);
+            }
+        }
     } else {
         // On non-Windows, we check metadata not following symlinks. All
         // symlinks are removed using remove_file.
-        fs::symlink_metadata(path)
-    } {
-        Ok(metadata) => metadata.file_type(),
-        Err(_) => return,
-    };
-
-    if file_type.is_dir() {
-        let _ = fs::remove_dir_all(path);
-    } else {
-        let _ = fs::remove_file(path);
+        if let Ok(metadata) = fs::symlink_metadata(path) {
+            if metadata.is_dir() {
+                let _ = fs::remove_dir_all(path);
+            } else {
+                let _ = fs::remove_file(path);
+            }
+        }
     }
 }
diff --git a/gen/src/fs.rs b/gen/src/fs.rs
index bfda58294..7bc3bbcba 100644
--- a/gen/src/fs.rs
+++ b/gen/src/fs.rs
@@ -68,6 +68,13 @@ pub(crate) fn current_dir() -> Result<PathBuf> {
     }
 }
 
+pub(crate) fn exists(path: impl AsRef<Path>) -> bool {
+    let path = path.as_ref();
+    // If path is a symlink, this returns true, regardless of whether the
+    // symlink points to a path that exists.
+    std::fs::symlink_metadata(path).is_ok()
+}
+
 pub(crate) fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
     let path = path.as_ref();
     match std::fs::read(path) {


Test patch: