qt_build_utils/qml/
qmldir.rs

1// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
2// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5
6use crate::qml::{QmlFile, QmlUri};
7
8use std::ffi::OsStr;
9use std::io;
10
11/// QML module definition files builder
12///
13/// A qmldir file is a plain-text file that contains the commands
14pub struct QmlDirBuilder {
15    class_name: Option<String>,
16    depends: Vec<QmlUri>,
17    plugin: Option<(bool, String)>,
18    type_info: Option<String>,
19    uri: QmlUri,
20    qml_files: Vec<QmlFile>,
21}
22
23impl QmlDirBuilder {
24    /// Construct a [QmlDirBuilder] using the give [QmlUri] for the
25    /// module identifier
26    pub fn new(uri: QmlUri) -> Self {
27        Self {
28            class_name: None,
29            depends: vec![],
30            plugin: None,
31            type_info: None,
32            qml_files: vec![],
33            uri,
34        }
35    }
36
37    /// Writer the resultant qmldir text file contents
38    pub fn write(self, writer: &mut impl io::Write) -> io::Result<()> {
39        // Module is mandatory
40        writeln!(writer, "module {}", self.uri.as_dots())?;
41
42        // Plugin, classname, and typeinfo are optional
43        if let Some((optional, name)) = self.plugin {
44            if optional {
45                writeln!(writer, "optional plugin {name}")?;
46            } else {
47                writeln!(writer, "plugin {name}")?;
48            }
49        }
50
51        if let Some(name) = self.class_name {
52            writeln!(writer, "classname {name}")?;
53        }
54
55        if let Some(file) = self.type_info {
56            writeln!(writer, "typeinfo {file}")?;
57        }
58
59        for depend in self.depends {
60            writeln!(writer, "depends {depend}")?;
61        }
62
63        // Prefer is always specified for now
64        writeln!(writer, "prefer :/qt/qml/{}/", self.uri.as_dirs())?;
65
66        for qml_file in &self.qml_files {
67            let is_qml_file = qml_file
68                .get_path()
69                .extension()
70                .map(|ext| ext.eq_ignore_ascii_case("qml"))
71                .unwrap_or_default();
72
73            if !is_qml_file {
74                panic!(
75                    "QML file does not end with .qml: {}",
76                    qml_file.get_path().display(),
77                );
78            }
79
80            let path = qml_file.get_path().display();
81            let qml_component_name = qml_file
82                .get_path()
83                .file_stem()
84                .and_then(OsStr::to_str)
85                .expect("Could not get qml file stem");
86
87            let singleton = if qml_file.is_singleton() {
88                "singleton "
89            } else {
90                ""
91            };
92
93            // Qt6 simply uses version 254.0 if no specific version is provided
94            let version = if let Some((major, minor)) = qml_file.get_version() {
95                format!("{}.{}", major, minor)
96            } else {
97                "254.0".to_string()
98            };
99
100            writeln!(writer, "{singleton}{qml_component_name} {version} {path}",)
101                .expect("Could not write qmldir file");
102        }
103
104        Ok(())
105    }
106
107    /// Provides the class name of the C++ plugin used by the module.
108    ///
109    /// This information is required for all the QML modules that depend on a
110    /// C++ plugin for additional functionality. Qt Quick applications built
111    /// with static linking cannot resolve the module imports without this
112    /// information.
113    //
114    // TODO: is required for C++ plugins, is it required when plugin?
115    pub fn class_name(mut self, class_name: impl Into<String>) -> Self {
116        self.class_name = Some(class_name.into());
117        self
118    }
119
120    /// Declares that this module depends on another
121    pub fn depend(mut self, depend: impl Into<QmlUri>) -> Self {
122        self.depends.push(depend.into());
123        self
124    }
125
126    /// Declares that this module depends on another
127    pub fn depends<T: Into<QmlUri>>(mut self, depends: impl IntoIterator<Item = T>) -> Self {
128        self.depends.extend(depends.into_iter().map(Into::into));
129        self
130    }
131
132    /// Declares a plugin to be made available by the module.
133    ///
134    /// optional denotes that the plugin itself does not contain any relevant code
135    /// and only serves to load a library it links to. If given, and if any types
136    /// for the module are already available, indicating that the library has been
137    /// loaded by some other means, QML will not load the plugin.
138    ///
139    /// name is the plugin library name. This is usually not the same as the file
140    /// name of the plugin binary, which is platform dependent. For example, the
141    /// library MyAppTypes would produce libMyAppTypes.so on Linux and MyAppTypes.dll
142    /// on Windows.
143    ///
144    /// Only zero or one plugin is supported, otherwise a panic will occur.
145    pub fn plugin(mut self, name: impl Into<String>, optional: bool) -> Self {
146        // Only support zero or one plugin for now
147        // it is not recommended to have more than one anyway
148        if self.plugin.is_some() {
149            panic!("Only zero or one plugin is supported currently");
150        }
151
152        self.plugin = Some((optional, name.into()));
153        self
154    }
155
156    /// Declares a list of .qml files that are part of the module.
157    pub fn qml_files(mut self, qml_files: impl IntoIterator<Item = impl Into<QmlFile>>) -> Self {
158        self.qml_files
159            .extend(qml_files.into_iter().map(|p| p.into()));
160        self
161    }
162
163    /// Declares a type description file for the module that can be read by QML
164    /// tools such as Qt Creator to access information about the types defined
165    /// by the module's plugins. File is the (relative) file name of a
166    /// .qmltypes file.
167    pub fn type_info(mut self, file: impl Into<String>) -> Self {
168        self.type_info = Some(file.into());
169        self
170    }
171
172    // TODO: add further optional entries
173    // object type declaration
174    // internal object type declaration
175    // javascript resource definition
176    // module import declaration
177    // designer support declaration
178}
179
180#[cfg(test)]
181mod test {
182    use super::*;
183
184    #[test]
185    fn qml_dir() {
186        let mut result = Vec::new();
187        QmlDirBuilder::new(QmlUri::new(["com", "kdab"]))
188            .class_name("C")
189            .depends(["QtQuick", "com.kdab.a"])
190            .plugin("P", true)
191            .type_info("T")
192            .qml_files(["qml/Test.qml"])
193            .qml_files([QmlFile::from("qml/MySingleton.qml")
194                .singleton(true)
195                .version(1, 0)])
196            .qml_files([QmlFile::from("../AnotherFile.qml").version(2, 123)])
197            .write(&mut result)
198            .unwrap();
199        assert_eq!(
200            String::from_utf8(result).unwrap(),
201            "module com.kdab
202optional plugin P
203classname C
204typeinfo T
205depends QtQuick
206depends com.kdab.a
207prefer :/qt/qml/com/kdab/
208Test 254.0 qml/Test.qml
209singleton MySingleton 1.0 qml/MySingleton.qml
210AnotherFile 2.123 ../AnotherFile.qml
211"
212        );
213    }
214}