diff --git a/.gitignore b/.gitignore index 28c8063ba..c4da92783 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .vscode/ **/target/ **/coverage/ -**/build*/ +**/build/ cargo/* **/mdbook/* **/mdbook-linkcheck/* diff --git a/crates/qt-build-utils/src/lib.rs b/crates/qt-build-utils/src/lib.rs index fffec4f9e..847304649 100644 --- a/crates/qt-build-utils/src/lib.rs +++ b/crates/qt-build-utils/src/lib.rs @@ -33,6 +33,12 @@ mod parse_cflags; mod platform; pub use platform::QtPlatformLinker; +mod qml; +pub use qml::{QmlDirBuilder, QmlPluginCppBuilder, QmlUri}; + +mod qrc; +pub use qrc::{QResource, QResourceFile, QResources}; + mod tool; pub use tool::{ MocArguments, MocProducts, QmlCacheArguments, QmlCacheProducts, QtTool, QtToolMoc, @@ -44,7 +50,6 @@ mod utils; use std::{ env, fs::File, - io::Write, path::{Path, PathBuf}, }; @@ -150,7 +155,11 @@ impl QtBuild { qml_files: &[impl AsRef], qrc_files: &[impl AsRef], ) -> QmlModuleRegistrationFiles { - let qml_uri_dirs = uri.replace('.', "/"); + let qml_uri = QmlUri::new(uri.split('.')); + let qml_uri_dirs = qml_uri.as_dirs(); + let qml_uri_underscores = qml_uri.as_underscores(); + let plugin_type_info = "plugin.qmltypes"; + let plugin_class_name = format!("{}_plugin", qml_uri_underscores); let out_dir = env::var("OUT_DIR").unwrap(); let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils")); @@ -159,65 +168,59 @@ impl QtBuild { let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs); std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory"); - let qml_uri_underscores = uri.replace('.', "_"); - let qmltypes_path = qml_module_dir.join("plugin.qmltypes"); - let plugin_class_name = format!("{qml_uri_underscores}_plugin"); + let qmltypes_path = qml_module_dir.join(plugin_type_info); // Generate qmldir file let qmldir_file_path = qml_module_dir.join("qmldir"); { - let mut qmldir = File::create(&qmldir_file_path).expect("Could not create qmldir file"); - write!( - qmldir, - "module {uri} -optional plugin {plugin_name} -classname {plugin_class_name} -typeinfo plugin.qmltypes -prefer :/qt/qml/{qml_uri_dirs}/ -" - ) - .expect("Could not write qmldir file"); + let mut file = File::create(&qmldir_file_path).expect("Could not create qmldir file"); + QmlDirBuilder::new(qml_uri.clone()) + .plugin(plugin_name, true) + .class_name(&plugin_class_name) + .type_info(plugin_type_info) + .write(&mut file) + .expect("Could not write qmldir file"); } // Generate .qrc file and run rcc on it let qrc_path = qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc")); { - fn qrc_file_line(file_path: &impl AsRef) -> String { - let path_display = file_path.as_ref().display(); - format!( - " {}\n", - path_display, - std::fs::canonicalize(file_path) - .unwrap_or_else(|_| panic!("Could not canonicalize path {path_display}")) - .display() - ) - } - - let mut qml_files_qrc = String::new(); - for file_path in qml_files { - qml_files_qrc.push_str(&qrc_file_line(file_path)); - } - for file_path in qrc_files { - qml_files_qrc.push_str(&qrc_file_line(file_path)); - } - - let mut qrc = File::create(&qrc_path).expect("Could not create qrc file"); let qml_module_dir_str = qml_module_dir.to_str().unwrap(); - write!( - qrc, - r#" - - {qml_module_dir_str} - - -{qml_files_qrc} - {qml_module_dir_str}/qmldir - - -"# - ) - .expect("Could note write qrc file"); + let qml_uri_dirs_prefix = format!("/qt/qml/{qml_uri_dirs}"); + let mut qrc = File::create(&qrc_path).expect("Could not create qrc file"); + QResources::new() + .resource(QResource::new().prefix("/".to_string()).file( + QResourceFile::new(qml_module_dir_str).alias(qml_uri_dirs_prefix.clone()), + )) + .resource({ + let mut resource = QResource::new().prefix(qml_uri_dirs_prefix.clone()).file( + QResourceFile::new(format!("{qml_module_dir_str}/qmldir")) + .alias("qmldir".to_string()), + ); + + fn resource_add_path(resource: QResource, path: &Path) -> QResource { + let resolved = std::fs::canonicalize(path) + .unwrap_or_else(|_| { + panic!("Could not canonicalize path {}", path.display()) + }) + .display() + .to_string(); + resource + .file(QResourceFile::new(resolved).alias(path.display().to_string())) + } + + for path in qml_files { + resource = resource_add_path(resource, path.as_ref()); + } + for path in qrc_files { + resource = resource_add_path(resource, path.as_ref()); + } + + resource + }) + .write(&mut qrc) + .expect("Could note write qrc file"); } // Run qmlcachegen @@ -264,58 +267,12 @@ prefer :/qt/qml/{qml_uri_dirs}/ let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp")); let include_path; { - let mut declarations = Vec::default(); - let mut usages = Vec::default(); - - let mut generate_usage = |return_type: &str, function_name: &str| { - declarations.push(format!("extern {return_type} {function_name}();")); - usages.push(format!("volatile auto {function_name}_usage = &{function_name};\nQ_UNUSED({function_name}_usage);")); - }; - - // This function is generated by qmltyperegistrar - generate_usage("void", &format!("qml_register_types_{qml_uri_underscores}")); - generate_usage( - "int", - &format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"), - ); - - if !qml_files.is_empty() && !qmlcachegen_file_paths.is_empty() { - generate_usage( - "int", - &format!("qInitResources_qmlcache_{qml_uri_underscores}"), - ); - } - let declarations = declarations.join("\n"); - let usages = usages.join("\n"); - - std::fs::write( - &qml_plugin_cpp_path, - format!( - r#" -#include - -// TODO: Add missing handling for GHS (Green Hills Software compiler) that is in -// https://code.qt.io/cgit/qt/qtbase.git/plain/src/corelib/global/qtsymbolmacros.h -{declarations} - -class {plugin_class_name} : public QQmlEngineExtensionPlugin -{{ - Q_OBJECT - Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlEngineExtensionInterface") - -public: - {plugin_class_name}(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent) - {{ - {usages} - }} -}}; - -// The moc-generated cpp file doesn't compile on its own; it needs to be #included here. -#include "moc_{plugin_class_name}.cpp.cpp" -"#, - ), - ) - .expect("Failed to write plugin definition"); + let mut file = File::create(&qml_plugin_cpp_path) + .expect("Could not create plugin definition file"); + QmlPluginCppBuilder::new(qml_uri, plugin_class_name.clone()) + .qml_cache(!qml_files.is_empty() && !qmlcachegen_file_paths.is_empty()) + .write(&mut file) + .expect("Failed to write plugin definition"); let moc_product = self.moc().compile( &qml_plugin_cpp_path, diff --git a/crates/qt-build-utils/src/qml/mod.rs b/crates/qt-build-utils/src/qml/mod.rs new file mode 100644 index 000000000..0992522e7 --- /dev/null +++ b/crates/qt-build-utils/src/qml/mod.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod qmldir; +pub use qmldir::QmlDirBuilder; + +mod qmlplugincpp; +pub use qmlplugincpp::QmlPluginCppBuilder; + +mod qmluri; +pub use qmluri::QmlUri; diff --git a/crates/qt-build-utils/src/qml/qmldir.rs b/crates/qt-build-utils/src/qml/qmldir.rs new file mode 100644 index 000000000..c32d252a5 --- /dev/null +++ b/crates/qt-build-utils/src/qml/qmldir.rs @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::QmlUri; + +use std::io; + +/// QML module definition files builder +/// +/// A qmldir file is a plain-text file that contains the commands +pub struct QmlDirBuilder { + class_name: Option, + plugin: Option<(bool, String)>, + type_info: Option, + uri: QmlUri, +} + +impl QmlDirBuilder { + /// Construct a [QmlDirBuilder] using the give [QmlUri] for the + /// module identifier + pub fn new(uri: QmlUri) -> Self { + Self { + class_name: None, + plugin: None, + type_info: None, + uri, + } + } + + /// Writer the resultant qmldir text file contents + pub fn write(self, writer: &mut impl io::Write) -> io::Result<()> { + // Module is mandatory + writeln!(writer, "module {}", self.uri.as_dots())?; + + // Plugin, classname, and typeinfo are optional + if let Some((optional, name)) = self.plugin { + if optional { + writeln!(writer, "optional plugin {name}")?; + } else { + writeln!(writer, "plugin {name}")?; + } + } + + if let Some(name) = self.class_name { + writeln!(writer, "classname {name}")?; + } + + if let Some(file) = self.type_info { + writeln!(writer, "typeinfo {file}")?; + } + + // Prefer is always specified for now + writeln!(writer, "prefer :/qt/qml/{}/", self.uri.as_dirs()) + } + + /// Provides the class name of the C++ plugin used by the module. + /// + /// This information is required for all the QML modules that depend on a + /// C++ plugin for additional functionality. Qt Quick applications built + /// with static linking cannot resolve the module imports without this + /// information. + // + // TODO: is required for C++ plugins, is it required when plugin? + pub fn class_name(mut self, class_name: impl Into) -> Self { + self.class_name = Some(class_name.into()); + self + } + + /// Declares a plugin to be made available by the module. + /// + /// optional denotes that the plugin itself does not contain any relevant code + /// and only serves to load a library it links to. If given, and if any types + /// for the module are already available, indicating that the library has been + /// loaded by some other means, QML will not load the plugin. + /// + /// name is the plugin library name. This is usually not the same as the file + /// name of the plugin binary, which is platform dependent. For example, the + /// library MyAppTypes would produce libMyAppTypes.so on Linux and MyAppTypes.dll + /// on Windows. + /// + /// Only zero or one plugin is supported, otherwise a panic will occur. + pub fn plugin(mut self, name: impl Into, optional: bool) -> Self { + // Only support zero or one plugin for now + // it is not recommended to have more than one anyway + if self.plugin.is_some() { + panic!("Only zero or one plugin is supported currently"); + } + + self.plugin = Some((optional, name.into())); + self + } + + /// Declares a type description file for the module that can be read by QML + /// tools such as Qt Creator to access information about the types defined + /// by the module's plugins. File is the (relative) file name of a + /// .qmltypes file. + pub fn type_info(mut self, file: impl Into) -> Self { + self.type_info = Some(file.into()); + self + } + + // TODO: add further optional entries + // object type declaration + // internal object type declaration + // javascript resource definition + // module dependencies declaration + // module import declaration + // designer support declaration +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn qml_dir() { + let mut result = Vec::new(); + QmlDirBuilder::new(QmlUri::new(["com", "kdab"])) + .class_name("C") + .plugin("P", true) + .type_info("T") + .write(&mut result) + .unwrap(); + assert_eq!( + String::from_utf8(result).unwrap(), + "module com.kdab +optional plugin P +classname C +typeinfo T +prefer :/qt/qml/com/kdab/ +" + ); + } +} diff --git a/crates/qt-build-utils/src/qml/qmlplugincpp.rs b/crates/qt-build-utils/src/qml/qmlplugincpp.rs new file mode 100644 index 000000000..ddbe5c324 --- /dev/null +++ b/crates/qt-build-utils/src/qml/qmlplugincpp.rs @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::io; + +use crate::QmlUri; + +/// A builder for representing a QML Extension Plugin C++ code +pub struct QmlPluginCppBuilder { + plugin_class_name: String, + qml_cache: bool, + uri: QmlUri, +} + +impl QmlPluginCppBuilder { + /// Construct a [QmlPluginCppBuilder] from a uri and plugin class name + pub fn new(uri: QmlUri, plugin_class_name: impl Into) -> Self { + // TODO: validate plugin class name + + Self { + plugin_class_name: plugin_class_name.into(), + qml_cache: false, + uri, + } + } + + /// Whether to enable qmlcache methods + pub fn qml_cache(mut self, enabled: bool) -> Self { + self.qml_cache = enabled; + self + } + + /// Write the resultant QML extension plugin C++ contents + pub fn write(self, writer: &mut impl io::Write) -> io::Result<()> { + let plugin_class_name = self.plugin_class_name; + let qml_uri_underscores = self.uri.as_underscores(); + + let mut declarations = Vec::default(); + let mut usages = Vec::default(); + + let mut generate_usage = |return_type: &str, function_name: &str| { + declarations.push(format!("extern {return_type} {function_name}();")); + usages.push(format!("volatile auto {function_name}_usage = &{function_name};\nQ_UNUSED({function_name}_usage);")); + }; + + // This function is generated by qmltyperegistrar + generate_usage("void", &format!("qml_register_types_{qml_uri_underscores}")); + generate_usage( + "int", + &format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"), + ); + + if self.qml_cache { + generate_usage( + "int", + &format!("qInitResources_qmlcache_{qml_uri_underscores}"), + ); + } + let declarations = declarations.join("\n"); + let usages = usages.join("\n"); + write!( + writer, + r#" +#include + +// TODO: Add missing handling for GHS (Green Hills Software compiler) that is in +// https://code.qt.io/cgit/qt/qtbase.git/plain/src/corelib/global/qtsymbolmacros.h +{declarations} + +class {plugin_class_name} : public QQmlEngineExtensionPlugin +{{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlEngineExtensionInterface") + +public: + {plugin_class_name}(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent) + {{ + {usages} + }} +}}; + +// The moc-generated cpp file doesn't compile on its own; it needs to be #included here. +#include "moc_{plugin_class_name}.cpp.cpp" +"# + ) + } +} diff --git a/crates/qt-build-utils/src/qml/qmluri.rs b/crates/qt-build-utils/src/qml/qmluri.rs new file mode 100644 index 000000000..a5ed791a6 --- /dev/null +++ b/crates/qt-build-utils/src/qml/qmluri.rs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/// A builder for representing a QML uri +#[derive(Clone)] +pub struct QmlUri { + uri: Vec, +} + +impl From<&str> for QmlUri { + fn from(value: &str) -> Self { + Self::new(value.split('.')) + } +} + +impl QmlUri { + /// Construct a [QmlUri] from a given string + /// + /// If the uri segments are not alphanumeric this will panic + pub fn new(uri: impl IntoIterator>) -> Self { + let uri: Vec<_> = uri.into_iter().map(Into::into).collect(); + + // Only allow alphanumeric uri parts for now + if uri.iter().any(|part| { + part.chars() + .any(|c| !(c.is_ascii_alphanumeric() || c == '_')) + }) { + panic!("QML uri parts must be alphanumeric: {uri:?}"); + } + + Self { uri } + } + + /// Retrieve the QML uri in directory form + pub fn as_dirs(&self) -> String { + self.uri.join("/") + } + + /// Retrieve the QML uri in dot form + pub fn as_dots(&self) -> String { + self.uri.join(".") + } + + /// Retrieve the QML uri in underscore form + pub fn as_underscores(&self) -> String { + self.uri.join("_") + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn uri() { + assert_eq!(QmlUri::from("a.b.c").uri, ["a", "b", "c"]); + assert_eq!(QmlUri::new(["a", "b", "c"]).uri, ["a", "b", "c"]); + } + + #[test] + #[should_panic] + fn uri_invalid() { + QmlUri::new(["a,b"]); + } + + #[test] + fn as_n() { + let uri = QmlUri::new(["a", "b", "c_d"]); + assert_eq!(uri.as_dirs(), "a/b/c_d"); + assert_eq!(uri.as_dots(), "a.b.c_d"); + assert_eq!(uri.as_underscores(), "a_b_c_d"); + } +} diff --git a/crates/qt-build-utils/src/qrc.rs b/crates/qt-build-utils/src/qrc.rs new file mode 100644 index 000000000..94ffde65f --- /dev/null +++ b/crates/qt-build-utils/src/qrc.rs @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use std::{ + io, + path::{Path, PathBuf}, +}; + +/// An individial `` line within a [QResource] +pub struct QResourceFile { + alias: Option, + // TODO: compression + // TODO: empty + path: PathBuf, +} + +impl> From for QResourceFile { + fn from(value: T) -> Self { + Self::new(value) + } +} + +impl QResourceFile { + /// Construct a [QResourceFile] + pub fn new(path: impl AsRef) -> Self { + Self { + alias: None, + path: path.as_ref().to_path_buf(), + } + } + + /// Specify an alias for the [QResourceFile] + pub fn alias(mut self, alias: impl Into) -> Self { + self.alias = Some(alias.into()); + self + } + + fn write(self, writer: &mut impl io::Write) -> io::Result<()> { + let alias = self + .alias + .map(|alias| format!(" alias=\"{}\"", alias.escape_default())) + .unwrap_or_default(); + let path = self.path.to_string_lossy(); + write!(writer, "{path}") + } +} + +/// A `` block within a [QResources] +pub struct QResource { + language: Option, + prefix: Option, + files: Vec, +} + +impl Default for QResource { + fn default() -> Self { + Self::new() + } +} + +impl> From for QResource { + fn from(value: T) -> Self { + Self::new().file(value) + } +} + +impl QResource { + /// Construct a [QResource] + pub fn new() -> Self { + Self { + language: None, + prefix: None, + files: vec![], + } + } + + /// Add a [QResourceFile] to the [QResource] + pub fn file>(mut self, file: T) -> Self { + self.files.push(file.into()); + self + } + + /// Add multiple [QResourceFile] to the [QResource] + pub fn files>(mut self, files: impl IntoIterator) -> Self { + for file in files.into_iter() { + self.files.push(file.into()); + } + self + } + + /// Specify a language for the `` + pub fn language(mut self, language: impl Into) -> Self { + self.language = Some(language.into()); + self + } + + /// Specify a prefix for the `` + pub fn prefix(mut self, prefix: impl Into) -> Self { + self.prefix = Some(prefix.into()); + self + } + + fn write(self, writer: &mut impl io::Write) -> io::Result<()> { + let language = self + .language + .map(|language| format!(" language=\"{}\"", language.escape_default())) + .unwrap_or_default(); + let prefix = self + .prefix + .map(|prefix| format!(" prefix=\"{}\"", prefix.escape_default())) + .unwrap_or_default(); + + write!(writer, "")?; + for file in self.files.into_iter() { + file.write(writer)?; + } + write!(writer, "") + } +} + +/// A helper for building Qt resource collection files +pub struct QResources { + resources: Vec, +} + +impl Default for QResources { + fn default() -> Self { + Self::new() + } +} + +impl>> From for QResources { + fn from(value: T) -> Self { + Self::new().resource(QResource::new().files(value)) + } +} + +impl QResources { + /// Construct a [QResource] + pub fn new() -> Self { + Self { resources: vec![] } + } + + /// Add a [QResource] to the [QResources] + pub fn resource>(mut self, resource: T) -> Self { + self.resources.push(resource.into()); + self + } + + /// Add multiple [QResource] to the [QResources] + pub fn resources>(mut self, resources: impl IntoIterator) -> Self { + for resource in resources.into_iter() { + self.resources.push(resource.into()); + } + self + } + + /// Convert to a string representation + pub fn write(self, writer: &mut impl io::Write) -> io::Result<()> { + write!(writer, "")?; + for resource in self.resources.into_iter() { + resource.write(writer)?; + } + write!(writer, "") + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn resource_file() { + let mut result = Vec::new(); + QResourceFile::new("path") + .alias("alias") + .write(&mut result) + .unwrap(); + assert_eq!( + String::from_utf8(result).unwrap(), + "path" + ); + } + + #[test] + fn resource() { + let mut result = Vec::new(); + QResource::new() + .language("language") + .prefix("prefix") + .write(&mut result) + .unwrap(); + assert_eq!( + String::from_utf8(result).unwrap(), + "" + ); + } + + #[test] + fn resources() { + let mut result = Vec::new(); + QResources::new() + .resources(["a", "b"]) + .resource( + QResource::new() + .prefix("prefix") + .files(["c", "d"]) + .file(QResourceFile::new("e").alias("alias")), + ) + .write(&mut result) + .unwrap(); + assert_eq!( + String::from_utf8(result).unwrap(), + "abcde" + ); + } + + #[test] + fn resources_from_files() { + let mut result = Vec::new(); + QResources::from(["a", "b"]).write(&mut result).unwrap(); + assert_eq!( + String::from_utf8(result).unwrap(), + "ab" + ); + } +}