diff --git a/.github/workflows/github-cxx-qt-tests.yml b/.github/workflows/github-cxx-qt-tests.yml index ed7362164..4c77695dc 100644 --- a/.github/workflows/github-cxx-qt-tests.yml +++ b/.github/workflows/github-cxx-qt-tests.yml @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - uses: DavidAnson/markdownlint-cli2-action@v18 + - uses: DavidAnson/markdownlint-cli2-action@v19 with: globs: '**/*.md' diff --git a/CHANGELOG.md b/CHANGELOG.md index c8721299f..6c4b62060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `QDateTime::from_string` to parse `QDateTime` from a `QString`. - Support for further types: `QUuid` ### Fixed diff --git a/crates/cxx-qt-build/src/dir.rs b/crates/cxx-qt-build/src/dir.rs index 992337ae0..666ac3f2e 100644 --- a/crates/cxx-qt-build/src/dir.rs +++ b/crates/cxx-qt-build/src/dir.rs @@ -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) -> Result<()> { let result = std::fs::remove_dir_all(&path); @@ -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, + dest: impl AsRef, +) -> Result { + 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, + dest: impl AsRef, +) -> Result { + deep_copy_directory(source.as_ref(), dest.as_ref()) +} + +#[cfg(not(unix))] +fn deep_copy_directory(source: &Path, dest: &Path) -> Result { + 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 { + 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); + } +} diff --git a/crates/cxx-qt-build/src/lib.rs b/crates/cxx-qt-build/src/lib.rs index eebb5f789..144723a61 100644 --- a/crates/cxx-qt-build/src/lib.rs +++ b/crates/cxx-qt-build/src/lib.rs @@ -17,6 +17,7 @@ mod diagnostics; use diagnostics::{Diagnostic, GeneratedError}; pub mod dir; +use dir::INCLUDE_VERB; mod dependencies; pub use dependencies::Interface; @@ -630,49 +631,28 @@ impl CxxQtBuilder { } } - fn symlink_directory(target: impl AsRef, link: impl AsRef) -> 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) => { 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:?}"); + } } } } @@ -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; + }; + 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() + ) + }; } } diff --git a/crates/cxx-qt-lib/.gitattributes b/crates/cxx-qt-lib/.gitattributes new file mode 100644 index 000000000..16d6e4879 --- /dev/null +++ b/crates/cxx-qt-lib/.gitattributes @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2021 Klarälvdalens Datakonsult AB, a KDAB Group company +# SPDX-FileContributor: Joshua Booth +# +# SPDX-License-Identifier: MIT OR Apache-2.0 + +src/core/qlist/qlist_*.rs linguist-generated=true +src/core/qset/qset_*.rs linguist-generated=true +src/core/qvariant/qvariant_*.rs linguist-generated=true +src/core/qvector/qvector_*.rs linguist-generated=true diff --git a/crates/cxx-qt-lib/include/core/qdatetime.h b/crates/cxx-qt-lib/include/core/qdatetime.h index 9c0530962..5032ddc37 100644 --- a/crates/cxx-qt-lib/include/core/qdatetime.h +++ b/crates/cxx-qt-lib/include/core/qdatetime.h @@ -68,5 +68,7 @@ ::std::int64_t qdatetimeToSecsSinceEpoch(const QDateTime& datetime); void qdatetimeSetTimeZone(QDateTime& datetime, const QTimeZone& timeZone); +QDateTime +qdatetimeFromQString(const QString& string, Qt::DateFormat format); } } diff --git a/crates/cxx-qt-lib/include/core/qstringlist.h b/crates/cxx-qt-lib/include/core/qstringlist.h index 5f6370225..f1f60fb3c 100644 --- a/crates/cxx-qt-lib/include/core/qstringlist.h +++ b/crates/cxx-qt-lib/include/core/qstringlist.h @@ -25,6 +25,10 @@ struct IsRelocatable : ::std::true_type namespace rust { namespace cxxqtlib1 { +const QList& +qstringlistAsQListQStringRef(const QStringList& list); +QList& +qstringlistAsQListQStringRef(QStringList& list); QStringList qstringlistFromQListQString(const QList& list); QList diff --git a/crates/cxx-qt-lib/src/core/qdatetime.cpp b/crates/cxx-qt-lib/src/core/qdatetime.cpp index 74ac9c0c7..ac1fce127 100644 --- a/crates/cxx-qt-lib/src/core/qdatetime.cpp +++ b/crates/cxx-qt-lib/src/core/qdatetime.cpp @@ -154,5 +154,11 @@ qdatetimeSetTimeZone(QDateTime& datetime, const QTimeZone& timeZone) #endif } +QDateTime +qdatetimeFromQString(const QString& string, const Qt::DateFormat format) +{ + return QDateTime::fromString(string, format); +} + } } diff --git a/crates/cxx-qt-lib/src/core/qdatetime.rs b/crates/cxx-qt-lib/src/core/qdatetime.rs index 6ef13f910..3882eb7e2 100644 --- a/crates/cxx-qt-lib/src/core/qdatetime.rs +++ b/crates/cxx-qt-lib/src/core/qdatetime.rs @@ -14,6 +14,7 @@ mod ffi { unsafe extern "C++" { include!("cxx-qt-lib/qt.h"); type TimeSpec = crate::TimeSpec; + type DateFormat = crate::DateFormat; } unsafe extern "C++" { @@ -169,6 +170,8 @@ mod ffi { fn qdatetimeToSecsSinceEpoch(datetime: &QDateTime) -> i64; #[rust_name = "qdatetime_settimezone"] fn qdatetimeSetTimeZone(datetime: &mut QDateTime, time_zone: &QTimeZone); + #[rust_name = "qdatetime_from_string"] + fn qdatetimeFromQString(string: &QString, format: DateFormat) -> QDateTime; } #[namespace = "rust::cxxqtlib1"] @@ -298,6 +301,16 @@ impl QDateTime { ffi::qdatetime_from_secs_since_epoch(secs, time_zone) } + /// Returns the datetime represented in the string as a QDateTime using the format given, or None if this is not possible. + pub fn from_string(string: &ffi::QString, format: ffi::DateFormat) -> Option { + let date = ffi::qdatetime_from_string(string, format); + if date.is_valid() { + Some(date) + } else { + None + } + } + /// Returns the number of milliseconds from this datetime to the other datetime. /// If the other datetime is earlier than this datetime, the value returned is negative. pub fn msecs_to(&self, other: &Self) -> i64 { diff --git a/crates/cxx-qt-lib/src/core/qstringlist.cpp b/crates/cxx-qt-lib/src/core/qstringlist.cpp index 12382c657..a5ea48a60 100644 --- a/crates/cxx-qt-lib/src/core/qstringlist.cpp +++ b/crates/cxx-qt-lib/src/core/qstringlist.cpp @@ -36,6 +36,18 @@ static_assert(QTypeInfo::isRelocatable); namespace rust { namespace cxxqtlib1 { +const QList& +qstringlistAsQListQStringRef(const QStringList& list) +{ + return static_cast&>(list); +} + +QList& +qstringlistAsQListQStringRef(QStringList& list) +{ + return static_cast&>(list); +} + QStringList qstringlistFromQListQString(const QList& list) { diff --git a/crates/cxx-qt-lib/src/core/qstringlist.rs b/crates/cxx-qt-lib/src/core/qstringlist.rs index 8616e6002..22d9d3886 100644 --- a/crates/cxx-qt-lib/src/core/qstringlist.rs +++ b/crates/cxx-qt-lib/src/core/qstringlist.rs @@ -5,6 +5,7 @@ use crate::{QList, QString}; use core::mem::MaybeUninit; use cxx::{type_id, ExternType}; +use std::ops::{Deref, DerefMut}; #[cxx::bridge] mod ffi { @@ -79,6 +80,11 @@ mod ffi { #[namespace = "rust::cxxqtlib1"] unsafe extern "C++" { + #[doc(hidden)] + #[rust_name = "qstringlist_as_qlist_qstring_ref"] + fn qstringlistAsQListQStringRef(list: &QStringList) -> &QList_QString; + #[rust_name = "qstringlist_as_qlist_qstring_ref_mut"] + fn qstringlistAsQListQStringRef(list: &mut QStringList) -> &mut QList_QString; #[doc(hidden)] #[rust_name = "qstringlist_from_qlist_qstring"] fn qstringlistFromQListQString(list: &QList_QString) -> QStringList; @@ -174,6 +180,20 @@ impl From<&QStringList> for QList { } } +impl Deref for QStringList { + type Target = QList; + + fn deref(&self) -> &Self::Target { + ffi::qstringlist_as_qlist_qstring_ref(self) + } +} + +impl DerefMut for QStringList { + fn deref_mut(&mut self) -> &mut Self::Target { + ffi::qstringlist_as_qlist_qstring_ref_mut(self) + } +} + // Safety: // // Static checks on the C++ side to ensure the size is the same. @@ -181,3 +201,18 @@ unsafe impl ExternType for QStringList { type Id = type_id!("QStringList"); type Kind = cxx::kind::Trivial; } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn deref() { + let mut list = QStringList::default(); + list.append(QString::from("element")); + assert_eq!( + list.get(0).map(|s| s.to_string()), + Some("element".to_owned()) + ); + } +}