Skip to content

Commit d0806b6

Browse files
authored
On non-Unix platforms, use deep copying rather than symlinking (KDAB#1136)
This removes the need to enable developer mode when building CXX-Qt on Windows. Even though this may reduce performance, people have stumbled over this often enough that it's definitely worth-while to change!
1 parent f5627f8 commit d0806b6

File tree

2 files changed

+110
-46
lines changed

2 files changed

+110
-46
lines changed

crates/cxx-qt-build/src/dir.rs

+84-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,19 @@
88
use crate::{crate_name, module_name_from_uri};
99
use std::io::Result;
1010
use std::{
11-
env,
11+
env, fs,
1212
path::{Path, PathBuf},
1313
};
1414

15+
/// On Unix platforms, included files are symlinked into destination folders.
16+
/// On non-Unix platforms, due to poor support for symlinking, included files are deep copied.
17+
#[cfg(unix)]
18+
pub(crate) const INCLUDE_VERB: &str = "create symlink";
19+
/// On Unix platforms, included files are symlinked into destination folders.
20+
/// On non-Unix platforms, due to poor support for symlinking, included files are deep copied.
21+
#[cfg(not(unix))]
22+
pub(crate) const INCLUDE_VERB: &str = "deep copy files";
23+
1524
// Clean a directory by removing it and recreating it.
1625
pub(crate) fn clean(path: impl AsRef<Path>) -> Result<()> {
1726
let result = std::fs::remove_dir_all(&path);
@@ -81,3 +90,77 @@ pub(crate) fn out() -> PathBuf {
8190
pub(crate) fn is_exporting() -> bool {
8291
export().is_some()
8392
}
93+
94+
#[cfg(unix)]
95+
pub(crate) fn symlink_or_copy_directory(
96+
source: impl AsRef<Path>,
97+
dest: impl AsRef<Path>,
98+
) -> Result<bool> {
99+
match std::os::unix::fs::symlink(&source, &dest) {
100+
Ok(()) => Ok(true),
101+
Err(e) if e.kind() != std::io::ErrorKind::AlreadyExists => Err(e),
102+
// Two dependencies may be reexporting the same shared dependency, which will
103+
// result in conflicting symlinks.
104+
// Try detecting this by resolving the symlinks and checking whether this leads us
105+
// to the same paths. If so, it's the same include path for the same prefix, which
106+
// is fine.
107+
Err(_) => Ok(fs::canonicalize(source)? == fs::canonicalize(dest)?),
108+
}
109+
}
110+
111+
#[cfg(not(unix))]
112+
pub(crate) fn symlink_or_copy_directory(
113+
source: impl AsRef<Path>,
114+
dest: impl AsRef<Path>,
115+
) -> Result<bool> {
116+
deep_copy_directory(source.as_ref(), dest.as_ref())
117+
}
118+
119+
#[cfg(not(unix))]
120+
fn deep_copy_directory(source: &Path, dest: &Path) -> Result<bool> {
121+
fs::create_dir_all(dest)?;
122+
for entry in fs::read_dir(source)? {
123+
let entry = entry?;
124+
let source_path = entry.path();
125+
let dest_path = dest.join(entry.file_name());
126+
if entry.file_type()?.is_dir() {
127+
if deep_copy_directory(&source_path, &dest_path)? {
128+
continue;
129+
}
130+
return Ok(false);
131+
}
132+
if !dest_path.try_exists()? {
133+
fs::copy(&source_path, &dest_path)?;
134+
} else if files_conflict(&source_path, &dest_path)? {
135+
return Ok(false);
136+
}
137+
}
138+
Ok(true)
139+
}
140+
141+
#[cfg(not(unix))]
142+
fn files_conflict(source: &Path, dest: &Path) -> Result<bool> {
143+
use fs::File;
144+
use std::io::{BufRead, BufReader};
145+
let source = File::open(source)?;
146+
let dest = File::open(dest)?;
147+
if source.metadata()?.len() != dest.metadata()?.len() {
148+
return Ok(true);
149+
}
150+
let mut source = BufReader::new(source);
151+
let mut dest = BufReader::new(dest);
152+
loop {
153+
let source_bytes = source.fill_buf()?;
154+
let bytes_len = source_bytes.len();
155+
let dest_bytes = dest.fill_buf()?;
156+
let bytes_len = bytes_len.min(dest_bytes.len());
157+
if bytes_len == 0 {
158+
return Ok(false);
159+
}
160+
if source_bytes[..bytes_len] != dest_bytes[..bytes_len] {
161+
return Ok(true);
162+
}
163+
source.consume(bytes_len);
164+
dest.consume(bytes_len);
165+
}
166+
}

crates/cxx-qt-build/src/lib.rs

+26-45
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mod diagnostics;
1717
use diagnostics::{Diagnostic, GeneratedError};
1818

1919
pub mod dir;
20+
use dir::INCLUDE_VERB;
2021

2122
mod dependencies;
2223
pub use dependencies::Interface;
@@ -630,49 +631,28 @@ impl CxxQtBuilder {
630631
}
631632
}
632633

633-
fn symlink_directory(target: impl AsRef<Path>, link: impl AsRef<Path>) -> std::io::Result<()> {
634-
#[cfg(unix)]
635-
let result = std::os::unix::fs::symlink(target, link);
636-
637-
#[cfg(windows)]
638-
let result = std::os::windows::fs::symlink_dir(target, link);
639-
640-
// TODO: If it's neither unix nor windows, we should probably just deep-copy the
641-
// dependency headers into our own include directory.
642-
#[cfg(not(any(unix, windows)))]
643-
panic!("Cxx-Qt-build: Unsupported platform! Only unix and windows are currently supported! Please file a bug report in the CXX-Qt repository.");
644-
645-
result
646-
}
647-
648634
// A dependency can specify which of its own include paths it wants to export.
649-
// Set up each of these exported include paths as symlinks in our own include directory.
635+
// Set up each of these exported include paths as symlinks in our own include directory,
636+
// or deep copy the files if the platform does not support symlinks.
650637
fn include_dependency(&mut self, dependency: &Dependency) {
638+
let header_root = dir::header_root();
639+
let dependency_root = dependency.path.join("include");
651640
for include_prefix in &dependency.manifest.exported_include_prefixes {
652641
// setup include directory
653-
let target = dependency.path.join("include").join(include_prefix);
654-
655-
let symlink = dir::header_root().join(include_prefix);
656-
if symlink.exists() {
657-
// Two dependencies may be reexporting the same shared dependency, which will
658-
// result in conflicting symlinks.
659-
// Try detecting this by resolving the symlinks and checking whether this leads us
660-
// to the same paths. If so, it's the same include path for the same prefix, which
661-
// is fine.
662-
let symlink =
663-
std::fs::canonicalize(symlink).expect("Failed to canonicalize symlink!");
664-
let target =
665-
std::fs::canonicalize(target).expect("Failed to canonicalize symlink target!");
666-
if symlink != target {
642+
let source = dependency_root.join(include_prefix);
643+
let dest = header_root.join(include_prefix);
644+
645+
match dir::symlink_or_copy_directory(source, dest) {
646+
Ok(true) => (),
647+
Ok(false) => {
667648
panic!(
668649
"Conflicting include_prefixes for {include_prefix}!\nDependency {dep_name} conflicts with existing include path",
669650
dep_name = dependency.manifest.name,
670651
);
671652
}
672-
} else {
673-
Self::symlink_directory(target, symlink).unwrap_or_else(|_| {
674-
panic!("Could not create symlink for include_prefix {include_prefix}!")
675-
});
653+
Err(e) => {
654+
panic!("Could not {INCLUDE_VERB} for include_prefix {include_prefix}: {e:?}");
655+
}
676656
}
677657
}
678658
}
@@ -1019,17 +999,18 @@ impl CxxQtBuilder {
1019999
}
10201000

10211001
fn write_interface_include_dirs(&self) {
1022-
if let Some(interface) = &self.public_interface {
1023-
for (header_dir, symlink) in &interface.exported_include_directories {
1024-
Self::symlink_directory(header_dir, dir::header_root().join(symlink))
1025-
.unwrap_or_else(|_| {
1026-
panic!(
1027-
"Failed to create symlink `{}` for export_include_directory: {}",
1028-
symlink,
1029-
header_dir.to_string_lossy()
1030-
)
1031-
});
1032-
}
1002+
let Some(interface) = &self.public_interface else {
1003+
return;
1004+
};
1005+
let header_root = dir::header_root();
1006+
for (header_dir, dest) in &interface.exported_include_directories {
1007+
let dest_dir = header_root.join(dest);
1008+
if let Err(e) = dir::symlink_or_copy_directory(header_dir, dest_dir) {
1009+
panic!(
1010+
"Failed to {INCLUDE_VERB} `{dest}` for export_include_directory `{dir_name}`: {e:?}",
1011+
dir_name = header_dir.to_string_lossy()
1012+
)
1013+
};
10331014
}
10341015
}
10351016

0 commit comments

Comments
 (0)