Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On non-Unix platforms, use deep copying rather than symlinking #1136

Merged
merged 19 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion crates/cxx-qt-build/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@
use crate::{crate_name, module_name_from_uri};
use std::io::Result;
use std::{
env,
env, fs,
path::{Path, PathBuf},
};

/// On Unix platforms, included files are symlinked into destination folders.
/// On non-Unix platforms, due to poor support for symlinking, included files are deep copied.
#[cfg(unix)]
pub(crate) const INCLUDE_VERB: &str = "create symlink";
/// On Unix platforms, included files are symlinked into destination folders.
/// On non-Unix platforms, due to poor support for symlinking, included files are deep copied.
#[cfg(not(unix))]
pub(crate) const INCLUDE_VERB: &str = "deep copy files";

// Clean a directory by removing it and recreating it.
pub(crate) fn clean(path: impl AsRef<Path>) -> Result<()> {
let result = std::fs::remove_dir_all(&path);
Expand Down Expand Up @@ -81,3 +90,77 @@ pub(crate) fn out() -> PathBuf {
pub(crate) fn is_exporting() -> bool {
export().is_some()
}

#[cfg(unix)]
pub(crate) fn symlink_or_copy_directory(
source: impl AsRef<Path>,
dest: impl AsRef<Path>,
) -> Result<bool> {
match std::os::unix::fs::symlink(&source, &dest) {
Ok(()) => Ok(true),
Err(e) if e.kind() != std::io::ErrorKind::AlreadyExists => Err(e),
// Two dependencies may be reexporting the same shared dependency, which will
// result in conflicting symlinks.
// Try detecting this by resolving the symlinks and checking whether this leads us
// to the same paths. If so, it's the same include path for the same prefix, which
// is fine.
Err(_) => Ok(fs::canonicalize(source)? == fs::canonicalize(dest)?),
}
}

#[cfg(not(unix))]
pub(crate) fn symlink_or_copy_directory(
source: impl AsRef<Path>,
dest: impl AsRef<Path>,
) -> Result<bool> {
deep_copy_directory(source.as_ref(), dest.as_ref())
}

#[cfg(not(unix))]
fn deep_copy_directory(source: &Path, dest: &Path) -> Result<bool> {
fs::create_dir_all(dest)?;
for entry in fs::read_dir(source)? {
let entry = entry?;
let source_path = entry.path();
let dest_path = dest.join(entry.file_name());
if entry.file_type()?.is_dir() {
if deep_copy_directory(&source_path, &dest_path)? {
continue;
}
return Ok(false);
}
if !dest_path.try_exists()? {
fs::copy(&source_path, &dest_path)?;
} else if files_conflict(&source_path, &dest_path)? {
return Ok(false);
}
}
Ok(true)
}

#[cfg(not(unix))]
fn files_conflict(source: &Path, dest: &Path) -> Result<bool> {
use fs::File;
use std::io::{BufRead, BufReader};
let source = File::open(source)?;
let dest = File::open(dest)?;
if source.metadata()?.len() != dest.metadata()?.len() {
return Ok(true);
}
let mut source = BufReader::new(source);
let mut dest = BufReader::new(dest);
loop {
let source_bytes = source.fill_buf()?;
let bytes_len = source_bytes.len();
let dest_bytes = dest.fill_buf()?;
let bytes_len = bytes_len.min(dest_bytes.len());
if bytes_len == 0 {
return Ok(false);
}
if source_bytes[..bytes_len] != dest_bytes[..bytes_len] {
return Ok(true);
}
source.consume(bytes_len);
dest.consume(bytes_len);
}
}
71 changes: 26 additions & 45 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod diagnostics;
use diagnostics::{Diagnostic, GeneratedError};

pub mod dir;
use dir::INCLUDE_VERB;

mod dependencies;
pub use dependencies::Interface;
Expand Down Expand Up @@ -630,49 +631,28 @@ impl CxxQtBuilder {
}
}

fn symlink_directory(target: impl AsRef<Path>, link: impl AsRef<Path>) -> std::io::Result<()> {
#[cfg(unix)]
let result = std::os::unix::fs::symlink(target, link);

#[cfg(windows)]
let result = std::os::windows::fs::symlink_dir(target, link);

// TODO: If it's neither unix nor windows, we should probably just deep-copy the
// dependency headers into our own include directory.
#[cfg(not(any(unix, windows)))]
panic!("Cxx-Qt-build: Unsupported platform! Only unix and windows are currently supported! Please file a bug report in the CXX-Qt repository.");

result
}

// A dependency can specify which of its own include paths it wants to export.
// Set up each of these exported include paths as symlinks in our own include directory.
// Set up each of these exported include paths as symlinks in our own include directory,
// or deep copy the files if the platform does not support symlinks.
fn include_dependency(&mut self, dependency: &Dependency) {
let header_root = dir::header_root();
let dependency_root = dependency.path.join("include");
for include_prefix in &dependency.manifest.exported_include_prefixes {
// setup include directory
let target = dependency.path.join("include").join(include_prefix);

let symlink = dir::header_root().join(include_prefix);
if symlink.exists() {
// Two dependencies may be reexporting the same shared dependency, which will
// result in conflicting symlinks.
// Try detecting this by resolving the symlinks and checking whether this leads us
// to the same paths. If so, it's the same include path for the same prefix, which
// is fine.
let symlink =
std::fs::canonicalize(symlink).expect("Failed to canonicalize symlink!");
let target =
std::fs::canonicalize(target).expect("Failed to canonicalize symlink target!");
if symlink != target {
let source = dependency_root.join(include_prefix);
let dest = header_root.join(include_prefix);

match dir::symlink_or_copy_directory(source, dest) {
Ok(true) => (),
Ok(false) => {
jnbooth marked this conversation as resolved.
Show resolved Hide resolved
panic!(
"Conflicting include_prefixes for {include_prefix}!\nDependency {dep_name} conflicts with existing include path",
dep_name = dependency.manifest.name,
);
}
} else {
Self::symlink_directory(target, symlink).unwrap_or_else(|_| {
panic!("Could not create symlink for include_prefix {include_prefix}!")
});
Err(e) => {
panic!("Could not {INCLUDE_VERB} for include_prefix {include_prefix}: {e:?}");
}
}
}
}
Expand Down Expand Up @@ -1019,17 +999,18 @@ impl CxxQtBuilder {
}

fn write_interface_include_dirs(&self) {
if let Some(interface) = &self.public_interface {
for (header_dir, symlink) in &interface.exported_include_directories {
Self::symlink_directory(header_dir, dir::header_root().join(symlink))
.unwrap_or_else(|_| {
panic!(
"Failed to create symlink `{}` for export_include_directory: {}",
symlink,
header_dir.to_string_lossy()
)
});
}
let Some(interface) = &self.public_interface else {
return;
};
jnbooth marked this conversation as resolved.
Show resolved Hide resolved
let header_root = dir::header_root();
for (header_dir, dest) in &interface.exported_include_directories {
let dest_dir = header_root.join(dest);
if let Err(e) = dir::symlink_or_copy_directory(header_dir, dest_dir) {
panic!(
"Failed to {INCLUDE_VERB} `{dest}` for export_include_directory `{dir_name}`: {e:?}",
dir_name = header_dir.to_string_lossy()
)
};
}
}

Expand Down
Loading