cxx_qt_build/
dir.rs

1// SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Leon Matthes <leon.matthes@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6//! This modules contains information about the paths where artifacts are placed by cxx-qt-build.
7
8use crate::{crate_name, module_name_from_uri};
9use std::io::Result;
10use std::{
11    env, fs,
12    path::{Path, PathBuf},
13};
14
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)]
18pub(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))]
22pub(crate) const INCLUDE_VERB: &str = "deep copy files";
23
24pub(crate) fn gen() -> PathBuf {
25    // Use a short name due to the Windows file path limit!
26    out().join("cxxqtgen")
27}
28
29// Clean a directory by removing it and recreating it.
30pub(crate) fn clean(path: impl AsRef<Path>) -> Result<()> {
31    let result = std::fs::remove_dir_all(&path);
32    if let Err(err) = result {
33        // If the path doesn't exist that's fine, otherwise we want to panic
34        if err.kind() != std::io::ErrorKind::NotFound {
35            return Err(err);
36        }
37    }
38
39    std::fs::create_dir_all(path)?;
40    Ok(())
41}
42
43/// The target directory, namespaced by crate
44pub(crate) fn crate_target() -> PathBuf {
45    let path = target();
46    if is_exporting() {
47        path.join("crates").join(crate_name())
48    } else {
49        // If we're not exporting, use a shortened path
50        // The paths for files in the OUT_DIR can get pretty long, especially if combined with
51        // Corrosion/CMake.
52        // This is an issue, as Windows has a maximum path length of 260 characters.
53        // The OUT_DIR is already namespaced by crate name, so we don't need to prefix again.
54        // See also: https://github.com/KDAB/cxx-qt/issues/1237
55        path
56    }
57}
58
59/// The target directory, namespaced by plugin
60pub(crate) fn module_target(module_uri: &str) -> PathBuf {
61    module_export(module_uri).unwrap_or_else(|| {
62        out()
63            // Use a short name due to the Windows file path limit!
64            .join("cxxqtqml")
65            .join(module_name_from_uri(module_uri))
66    })
67}
68
69/// The export directory, namespaced by QML module
70///
71/// In conctrast to the crate_export directory, this is `Some` for downstream dependencies as well.
72/// This allows CMake to import QML modules from dependencies.
73///
74/// TODO: This may conflict if two dependencies are building QML modules with the same name!
75/// We should probably include a lockfile here to avoid this.
76pub(crate) fn module_export(module_uri: &str) -> Option<PathBuf> {
77    // In contrast to crate_export, we don't need to check for the specific crate here.
78    // QML modules should always be exported.
79    env::var("CXX_QT_EXPORT_DIR")
80        .ok()
81        .map(PathBuf::from)
82        .map(|dir| {
83            dir.join("qml_modules")
84                .join(module_name_from_uri(module_uri))
85        })
86}
87
88/// The target directory or another directory where we can write files that will be shared
89/// between crates.
90pub(crate) fn target() -> PathBuf {
91    if let Some(export) = export() {
92        return export;
93    }
94
95    // Use a short name due to the Windows file path limit!
96    out().join("cxxqtbuild")
97}
98
99/// The export directory, if one was specified through the environment.
100/// Note that this is not namspaced by crate.
101pub(crate) fn export() -> Option<PathBuf> {
102    // Make sure to synchronize the naming of these variables with CMake!
103    let export_flag = format!("CXX_QT_EXPORT_CRATE_{}", crate_name());
104    // We only want to export this crate if it is the specific crate that CMake is looking for and
105    // not any of that crates dependencies.
106    // This should avoid issues where multiple configurations of the same crate are being built in
107    // parallel by Cargo.
108    // CMake should usually only have a single configuration in the same build directory (and therefore
109    // export directory) so there should never be more than one configuration writing to that same
110    // export directory.
111    if env::var(export_flag).is_ok() {
112        env::var("CXX_QT_EXPORT_DIR").ok().map(PathBuf::from)
113    } else {
114        None
115    }
116}
117
118/// The include directory is namespaced by crate name when exporting for a C++ build system,
119/// but for using cargo build without a C++ build system, OUT_DIR is already namespaced by crate name.
120pub(crate) fn header_root() -> PathBuf {
121    crate_target().join("include")
122}
123
124/// The OUT_DIR, converted into a PathBuf
125pub(crate) fn out() -> PathBuf {
126    env::var("OUT_DIR").unwrap().into()
127}
128
129pub(crate) fn is_exporting() -> bool {
130    export().is_some()
131}
132
133#[cfg(unix)]
134pub(crate) fn symlink_or_copy_directory(
135    source: impl AsRef<Path>,
136    dest: impl AsRef<Path>,
137) -> Result<bool> {
138    match std::os::unix::fs::symlink(&source, &dest) {
139        Ok(()) => Ok(true),
140        Err(e) if e.kind() != std::io::ErrorKind::AlreadyExists => Err(e),
141        // Two dependencies may be reexporting the same shared dependency, which will
142        // result in conflicting symlinks.
143        // Try detecting this by resolving the symlinks and checking whether this leads us
144        // to the same paths. If so, it's the same include path for the same prefix, which
145        // is fine.
146        Err(_) => Ok(fs::canonicalize(source)? == fs::canonicalize(dest)?),
147    }
148}
149
150#[cfg(not(unix))]
151pub(crate) fn symlink_or_copy_directory(
152    source: impl AsRef<Path>,
153    dest: impl AsRef<Path>,
154) -> Result<bool> {
155    deep_copy_directory(source.as_ref(), dest.as_ref())
156}
157
158#[cfg(not(unix))]
159fn deep_copy_directory(source: &Path, dest: &Path) -> Result<bool> {
160    fs::create_dir_all(dest)?;
161    for entry in fs::read_dir(source)? {
162        let entry = entry?;
163        let source_path = entry.path();
164        let dest_path = dest.join(entry.file_name());
165        if entry.file_type()?.is_dir() {
166            if deep_copy_directory(&source_path, &dest_path)? {
167                continue;
168            }
169            return Ok(false);
170        }
171        if !dest_path.try_exists()? {
172            fs::copy(&source_path, &dest_path)?;
173        } else if files_conflict(&source_path, &dest_path)? {
174            return Ok(false);
175        }
176    }
177    Ok(true)
178}
179
180#[cfg(not(unix))]
181fn files_conflict(source: &Path, dest: &Path) -> Result<bool> {
182    use fs::File;
183    use std::io::{BufRead, BufReader};
184    let source = File::open(source)?;
185    let dest = File::open(dest)?;
186    if source.metadata()?.len() != dest.metadata()?.len() {
187        return Ok(true);
188    }
189    let mut source = BufReader::new(source);
190    let mut dest = BufReader::new(dest);
191    loop {
192        let source_bytes = source.fill_buf()?;
193        let bytes_len = source_bytes.len();
194        let dest_bytes = dest.fill_buf()?;
195        let bytes_len = bytes_len.min(dest_bytes.len());
196        if bytes_len == 0 {
197            return Ok(false);
198        }
199        if source_bytes[..bytes_len] != dest_bytes[..bytes_len] {
200            return Ok(true);
201        }
202        source.consume(bytes_len);
203        dest.consume(bytes_len);
204    }
205}