Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
.vscode/
**/target/
**/coverage/
**/build*/
**/build/
cargo/*
**/mdbook/*
**/mdbook-linkcheck/*
Expand Down
161 changes: 59 additions & 102 deletions crates/qt-build-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -44,7 +50,6 @@ mod utils;
use std::{
env,
fs::File,
io::Write,
path::{Path, PathBuf},
};

Expand Down Expand Up @@ -150,7 +155,11 @@ impl QtBuild {
qml_files: &[impl AsRef<Path>],
qrc_files: &[impl AsRef<Path>],
) -> 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"));
Expand All @@ -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<Path>) -> String {
let path_display = file_path.as_ref().display();
format!(
" <file alias=\"{}\">{}</file>\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#"<RCC>
<qresource prefix="/">
<file alias="/qt/qml/{qml_uri_dirs}">{qml_module_dir_str}</file>
</qresource>
<qresource prefix="/qt/qml/{qml_uri_dirs}">
{qml_files_qrc}
<file alias="qmldir">{qml_module_dir_str}/qmldir</file>
</qresource>
</RCC>
"#
)
.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
Expand Down Expand Up @@ -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 <QtQml/qqmlextensionplugin.h>

// 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,
Expand Down
13 changes: 13 additions & 0 deletions crates/qt-build-utils/src/qml/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
//
// 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;
136 changes: 136 additions & 0 deletions crates/qt-build-utils/src/qml/qmldir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
//
// 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<String>,
plugin: Option<(bool, String)>,
type_info: Option<String>,
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<String>) -> 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<String>, 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<String>) -> 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/
"
);
}
}
Loading
Loading