Skip to content

Commit 6bc6686

Browse files
authored
qt-build-utils: add QmlDir and QmlUri builders (#1338)
* qt-build-utils: add QmlDir and QmlUri builders * qt-build-utils: add a QResourceBuilder * qt-build-utils: add QmlPluginCppBuilder for the extension plugin * qt-build-utils: move builder into qml/ and qrc/ * qt-build-utils: allow underscores in QML uris * qt-build-utils: fix docs pointing to old QmlUri name
1 parent f9e0926 commit 6bc6686

File tree

7 files changed

+602
-103
lines changed

7 files changed

+602
-103
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
.vscode/
88
**/target/
99
**/coverage/
10-
**/build*/
10+
**/build/
1111
cargo/*
1212
**/mdbook/*
1313
**/mdbook-linkcheck/*

crates/qt-build-utils/src/lib.rs

Lines changed: 59 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ mod parse_cflags;
3333
mod platform;
3434
pub use platform::QtPlatformLinker;
3535

36+
mod qml;
37+
pub use qml::{QmlDirBuilder, QmlPluginCppBuilder, QmlUri};
38+
39+
mod qrc;
40+
pub use qrc::{QResource, QResourceFile, QResources};
41+
3642
mod tool;
3743
pub use tool::{
3844
MocArguments, MocProducts, QmlCacheArguments, QmlCacheProducts, QtTool, QtToolMoc,
@@ -44,7 +50,6 @@ mod utils;
4450
use std::{
4551
env,
4652
fs::File,
47-
io::Write,
4853
path::{Path, PathBuf},
4954
};
5055

@@ -150,7 +155,11 @@ impl QtBuild {
150155
qml_files: &[impl AsRef<Path>],
151156
qrc_files: &[impl AsRef<Path>],
152157
) -> QmlModuleRegistrationFiles {
153-
let qml_uri_dirs = uri.replace('.', "/");
158+
let qml_uri = QmlUri::new(uri.split('.'));
159+
let qml_uri_dirs = qml_uri.as_dirs();
160+
let qml_uri_underscores = qml_uri.as_underscores();
161+
let plugin_type_info = "plugin.qmltypes";
162+
let plugin_class_name = format!("{}_plugin", qml_uri_underscores);
154163

155164
let out_dir = env::var("OUT_DIR").unwrap();
156165
let qt_build_utils_dir = PathBuf::from(format!("{out_dir}/qt-build-utils"));
@@ -159,65 +168,59 @@ impl QtBuild {
159168
let qml_module_dir = qt_build_utils_dir.join("qml_modules").join(&qml_uri_dirs);
160169
std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");
161170

162-
let qml_uri_underscores = uri.replace('.', "_");
163-
let qmltypes_path = qml_module_dir.join("plugin.qmltypes");
164-
let plugin_class_name = format!("{qml_uri_underscores}_plugin");
171+
let qmltypes_path = qml_module_dir.join(plugin_type_info);
165172

166173
// Generate qmldir file
167174
let qmldir_file_path = qml_module_dir.join("qmldir");
168175
{
169-
let mut qmldir = File::create(&qmldir_file_path).expect("Could not create qmldir file");
170-
write!(
171-
qmldir,
172-
"module {uri}
173-
optional plugin {plugin_name}
174-
classname {plugin_class_name}
175-
typeinfo plugin.qmltypes
176-
prefer :/qt/qml/{qml_uri_dirs}/
177-
"
178-
)
179-
.expect("Could not write qmldir file");
176+
let mut file = File::create(&qmldir_file_path).expect("Could not create qmldir file");
177+
QmlDirBuilder::new(qml_uri.clone())
178+
.plugin(plugin_name, true)
179+
.class_name(&plugin_class_name)
180+
.type_info(plugin_type_info)
181+
.write(&mut file)
182+
.expect("Could not write qmldir file");
180183
}
181184

182185
// Generate .qrc file and run rcc on it
183186
let qrc_path =
184187
qml_module_dir.join(format!("qml_module_resources_{qml_uri_underscores}.qrc"));
185188
{
186-
fn qrc_file_line(file_path: &impl AsRef<Path>) -> String {
187-
let path_display = file_path.as_ref().display();
188-
format!(
189-
" <file alias=\"{}\">{}</file>\n",
190-
path_display,
191-
std::fs::canonicalize(file_path)
192-
.unwrap_or_else(|_| panic!("Could not canonicalize path {path_display}"))
193-
.display()
194-
)
195-
}
196-
197-
let mut qml_files_qrc = String::new();
198-
for file_path in qml_files {
199-
qml_files_qrc.push_str(&qrc_file_line(file_path));
200-
}
201-
for file_path in qrc_files {
202-
qml_files_qrc.push_str(&qrc_file_line(file_path));
203-
}
204-
205-
let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
206189
let qml_module_dir_str = qml_module_dir.to_str().unwrap();
207-
write!(
208-
qrc,
209-
r#"<RCC>
210-
<qresource prefix="/">
211-
<file alias="/qt/qml/{qml_uri_dirs}">{qml_module_dir_str}</file>
212-
</qresource>
213-
<qresource prefix="/qt/qml/{qml_uri_dirs}">
214-
{qml_files_qrc}
215-
<file alias="qmldir">{qml_module_dir_str}/qmldir</file>
216-
</qresource>
217-
</RCC>
218-
"#
219-
)
220-
.expect("Could note write qrc file");
190+
let qml_uri_dirs_prefix = format!("/qt/qml/{qml_uri_dirs}");
191+
let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
192+
QResources::new()
193+
.resource(QResource::new().prefix("/".to_string()).file(
194+
QResourceFile::new(qml_module_dir_str).alias(qml_uri_dirs_prefix.clone()),
195+
))
196+
.resource({
197+
let mut resource = QResource::new().prefix(qml_uri_dirs_prefix.clone()).file(
198+
QResourceFile::new(format!("{qml_module_dir_str}/qmldir"))
199+
.alias("qmldir".to_string()),
200+
);
201+
202+
fn resource_add_path(resource: QResource, path: &Path) -> QResource {
203+
let resolved = std::fs::canonicalize(path)
204+
.unwrap_or_else(|_| {
205+
panic!("Could not canonicalize path {}", path.display())
206+
})
207+
.display()
208+
.to_string();
209+
resource
210+
.file(QResourceFile::new(resolved).alias(path.display().to_string()))
211+
}
212+
213+
for path in qml_files {
214+
resource = resource_add_path(resource, path.as_ref());
215+
}
216+
for path in qrc_files {
217+
resource = resource_add_path(resource, path.as_ref());
218+
}
219+
220+
resource
221+
})
222+
.write(&mut qrc)
223+
.expect("Could note write qrc file");
221224
}
222225

223226
// Run qmlcachegen
@@ -264,58 +267,12 @@ prefer :/qt/qml/{qml_uri_dirs}/
264267
let qml_plugin_cpp_path = qml_plugin_dir.join(format!("{plugin_class_name}.cpp"));
265268
let include_path;
266269
{
267-
let mut declarations = Vec::default();
268-
let mut usages = Vec::default();
269-
270-
let mut generate_usage = |return_type: &str, function_name: &str| {
271-
declarations.push(format!("extern {return_type} {function_name}();"));
272-
usages.push(format!("volatile auto {function_name}_usage = &{function_name};\nQ_UNUSED({function_name}_usage);"));
273-
};
274-
275-
// This function is generated by qmltyperegistrar
276-
generate_usage("void", &format!("qml_register_types_{qml_uri_underscores}"));
277-
generate_usage(
278-
"int",
279-
&format!("qInitResources_qml_module_resources_{qml_uri_underscores}_qrc"),
280-
);
281-
282-
if !qml_files.is_empty() && !qmlcachegen_file_paths.is_empty() {
283-
generate_usage(
284-
"int",
285-
&format!("qInitResources_qmlcache_{qml_uri_underscores}"),
286-
);
287-
}
288-
let declarations = declarations.join("\n");
289-
let usages = usages.join("\n");
290-
291-
std::fs::write(
292-
&qml_plugin_cpp_path,
293-
format!(
294-
r#"
295-
#include <QtQml/qqmlextensionplugin.h>
296-
297-
// TODO: Add missing handling for GHS (Green Hills Software compiler) that is in
298-
// https://code.qt.io/cgit/qt/qtbase.git/plain/src/corelib/global/qtsymbolmacros.h
299-
{declarations}
300-
301-
class {plugin_class_name} : public QQmlEngineExtensionPlugin
302-
{{
303-
Q_OBJECT
304-
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlEngineExtensionInterface")
305-
306-
public:
307-
{plugin_class_name}(QObject *parent = nullptr) : QQmlEngineExtensionPlugin(parent)
308-
{{
309-
{usages}
310-
}}
311-
}};
312-
313-
// The moc-generated cpp file doesn't compile on its own; it needs to be #included here.
314-
#include "moc_{plugin_class_name}.cpp.cpp"
315-
"#,
316-
),
317-
)
318-
.expect("Failed to write plugin definition");
270+
let mut file = File::create(&qml_plugin_cpp_path)
271+
.expect("Could not create plugin definition file");
272+
QmlPluginCppBuilder::new(qml_uri, plugin_class_name.clone())
273+
.qml_cache(!qml_files.is_empty() && !qmlcachegen_file_paths.is_empty())
274+
.write(&mut file)
275+
.expect("Failed to write plugin definition");
319276

320277
let moc_product = self.moc().compile(
321278
&qml_plugin_cpp_path,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
2+
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
3+
//
4+
// SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
mod qmldir;
7+
pub use qmldir::QmlDirBuilder;
8+
9+
mod qmlplugincpp;
10+
pub use qmlplugincpp::QmlPluginCppBuilder;
11+
12+
mod qmluri;
13+
pub use qmluri::QmlUri;
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
2+
// SPDX-FileContributor: Andrew Hayzen <[email protected]>
3+
//
4+
// SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
use crate::QmlUri;
7+
8+
use std::io;
9+
10+
/// QML module definition files builder
11+
///
12+
/// A qmldir file is a plain-text file that contains the commands
13+
pub struct QmlDirBuilder {
14+
class_name: Option<String>,
15+
plugin: Option<(bool, String)>,
16+
type_info: Option<String>,
17+
uri: QmlUri,
18+
}
19+
20+
impl QmlDirBuilder {
21+
/// Construct a [QmlDirBuilder] using the give [QmlUri] for the
22+
/// module identifier
23+
pub fn new(uri: QmlUri) -> Self {
24+
Self {
25+
class_name: None,
26+
plugin: None,
27+
type_info: None,
28+
uri,
29+
}
30+
}
31+
32+
/// Writer the resultant qmldir text file contents
33+
pub fn write(self, writer: &mut impl io::Write) -> io::Result<()> {
34+
// Module is mandatory
35+
writeln!(writer, "module {}", self.uri.as_dots())?;
36+
37+
// Plugin, classname, and typeinfo are optional
38+
if let Some((optional, name)) = self.plugin {
39+
if optional {
40+
writeln!(writer, "optional plugin {name}")?;
41+
} else {
42+
writeln!(writer, "plugin {name}")?;
43+
}
44+
}
45+
46+
if let Some(name) = self.class_name {
47+
writeln!(writer, "classname {name}")?;
48+
}
49+
50+
if let Some(file) = self.type_info {
51+
writeln!(writer, "typeinfo {file}")?;
52+
}
53+
54+
// Prefer is always specified for now
55+
writeln!(writer, "prefer :/qt/qml/{}/", self.uri.as_dirs())
56+
}
57+
58+
/// Provides the class name of the C++ plugin used by the module.
59+
///
60+
/// This information is required for all the QML modules that depend on a
61+
/// C++ plugin for additional functionality. Qt Quick applications built
62+
/// with static linking cannot resolve the module imports without this
63+
/// information.
64+
//
65+
// TODO: is required for C++ plugins, is it required when plugin?
66+
pub fn class_name(mut self, class_name: impl Into<String>) -> Self {
67+
self.class_name = Some(class_name.into());
68+
self
69+
}
70+
71+
/// Declares a plugin to be made available by the module.
72+
///
73+
/// optional denotes that the plugin itself does not contain any relevant code
74+
/// and only serves to load a library it links to. If given, and if any types
75+
/// for the module are already available, indicating that the library has been
76+
/// loaded by some other means, QML will not load the plugin.
77+
///
78+
/// name is the plugin library name. This is usually not the same as the file
79+
/// name of the plugin binary, which is platform dependent. For example, the
80+
/// library MyAppTypes would produce libMyAppTypes.so on Linux and MyAppTypes.dll
81+
/// on Windows.
82+
///
83+
/// Only zero or one plugin is supported, otherwise a panic will occur.
84+
pub fn plugin(mut self, name: impl Into<String>, optional: bool) -> Self {
85+
// Only support zero or one plugin for now
86+
// it is not recommended to have more than one anyway
87+
if self.plugin.is_some() {
88+
panic!("Only zero or one plugin is supported currently");
89+
}
90+
91+
self.plugin = Some((optional, name.into()));
92+
self
93+
}
94+
95+
/// Declares a type description file for the module that can be read by QML
96+
/// tools such as Qt Creator to access information about the types defined
97+
/// by the module's plugins. File is the (relative) file name of a
98+
/// .qmltypes file.
99+
pub fn type_info(mut self, file: impl Into<String>) -> Self {
100+
self.type_info = Some(file.into());
101+
self
102+
}
103+
104+
// TODO: add further optional entries
105+
// object type declaration
106+
// internal object type declaration
107+
// javascript resource definition
108+
// module dependencies declaration
109+
// module import declaration
110+
// designer support declaration
111+
}
112+
113+
#[cfg(test)]
114+
mod test {
115+
use super::*;
116+
117+
#[test]
118+
fn qml_dir() {
119+
let mut result = Vec::new();
120+
QmlDirBuilder::new(QmlUri::new(["com", "kdab"]))
121+
.class_name("C")
122+
.plugin("P", true)
123+
.type_info("T")
124+
.write(&mut result)
125+
.unwrap();
126+
assert_eq!(
127+
String::from_utf8(result).unwrap(),
128+
"module com.kdab
129+
optional plugin P
130+
classname C
131+
typeinfo T
132+
prefer :/qt/qml/com/kdab/
133+
"
134+
);
135+
}
136+
}

0 commit comments

Comments
 (0)