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}