qt-build-utils 0.9.0

Build script helper for linking Qt libraries and using moc code generator. Intended to be used together with cc, cpp_build, or cxx_build
Documentation
// SPDX-FileCopyrightText: 2025 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
// SPDX-FileContributor: Andrew Hayzen <andrew.hayzen@kdab.com>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::io;

use crate::{Initializer, QmlUri};

/// The build type of a Qt plugin
#[non_exhaustive]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PluginType {
    /// A statically linked plugin
    Static,
    /// A dynamically linked plugin (sometimes called a MODULE plugin in Qt/CMake)
    ///
    /// Note: There can only be one dynamic plugin per dynamic library!
    Dynamic,
}

/// A builder for representing a QML Extension Plugin C++ code
pub struct QmlPluginCppBuilder {
    plugin_class_name: String,
    qml_cache: bool,
    uri: QmlUri,
    plugin_type: PluginType,
}

impl QmlPluginCppBuilder {
    /// Construct a [QmlPluginCppBuilder] from a uri and plugin class name
    pub fn new(uri: QmlUri, plugin_class_name: impl Into<String>) -> Self {
        // TODO: validate plugin class name

        Self {
            plugin_class_name: plugin_class_name.into(),
            qml_cache: false,
            plugin_type: PluginType::Static,
            uri,
        }
    }

    /// Whether to enable qmlcache methods
    pub fn qml_cache(mut self, enabled: bool) -> Self {
        self.qml_cache = enabled;
        self
    }

    /// Whether this is a static or dynamic plugin
    ///
    /// Note: For static plugins the QT_STATICPLUGIN definition should be set when compiling, for
    /// dynamic plugins the QT_PLUGIN definition should be set when compiling.
    /// See also: <https://doc.qt.io/qt-6/plugins-howto.html#creating-static-plugins>
    ///
    /// Note: There can only be one dynamic plugin per dynamic library!
    pub fn plugin_type(mut self, plugin_type: PluginType) -> Self {
        self.plugin_type = plugin_type;
        self
    }

    /// Write the resultant QML extension plugin C++ contents
    pub fn write(self, writer: &mut impl io::Write) -> io::Result<Initializer> {
        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");
        let init_fn_name = format!("init_cxx_qt_qml_module_{plugin_class_name}");
        write!(
            writer,
            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}
    }}
}};

extern "C" {{
    // "drive-by initialization" causes the plugin class to be included in static linking scenarios
    // Any function that is called from within this file causes the entire object file to be linked in.
    // Therefore we provide an empty function that can be called at any point to ensure this file is linked in.
    bool {init_fn_name}() {{
        return true;
    }}
}}

// The moc-generated cpp file doesn't compile on its own; it needs to be #included here.
#include "moc_{plugin_class_name}.cpp.cpp"
"#
        )?;
        let initializer = match self.plugin_type {
            PluginType::Static => Initializer {
                file: None,
                init_call: None,
                init_declaration: Some(format!(
                    "#include <QtPlugin>\nQ_IMPORT_PLUGIN({plugin_class_name});"
                )),
            },
            PluginType::Dynamic => Initializer::default_signature(&init_fn_name),
        };
        Ok(initializer)
    }
}