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 crate::{utils, QmlUri, QtInstallation, QtTool};

use std::{
    path::{Path, PathBuf},
    process::Command,
};

/// Paths to files generated by [QtToolMoc::compile]
pub struct MocProducts {
    /// Generated C++ file
    pub cpp: PathBuf,
    /// Generated JSON file
    pub metatypes_json: PathBuf,
}

/// Arguments for a Qt moc invocation.
/// See: [QtToolMoc::compile]
#[derive(Default, Clone)]
pub struct MocArguments {
    uri: Option<QmlUri>,
    include_paths: Vec<PathBuf>,
}

impl MocArguments {
    /// Should be passed if the input_file is part of a QML module
    pub fn uri(mut self, uri: impl Into<QmlUri>) -> Self {
        self.uri = Some(uri.into());
        self
    }

    /// Returns the assigned URI, if any.
    pub fn get_uri(&self) -> Option<&QmlUri> {
        self.uri.as_ref()
    }

    /// Additional include path to pass to moc
    pub fn include_path(mut self, include_path: impl AsRef<Path>) -> Self {
        self.include_paths.push(include_path.as_ref().to_owned());
        self
    }

    /// Additional include paths to pass to moc.
    pub fn include_paths(
        mut self,
        include_paths: impl IntoIterator<Item = impl AsRef<Path>>,
    ) -> Self {
        self.include_paths.extend(
            include_paths
                .into_iter()
                .map(|path| path.as_ref().to_owned()),
        );
        self
    }
}

/// A wrapper around the [moc](https://doc.qt.io/qt-6/moc.html) tool
pub struct QtToolMoc {
    executable: PathBuf,
    qt_include_paths: Vec<PathBuf>,
    qt_framework_paths: Vec<PathBuf>,
}

impl QtToolMoc {
    /// Construct a [QtToolMoc] from a given [QtInstallation]
    pub fn new(qt_installation: &dyn QtInstallation, qt_modules: &[String]) -> Self {
        let executable = qt_installation
            .try_find_tool(QtTool::Moc)
            .expect("Could not find moc");

        // Ensure that the executable works
        utils::check_executable_help(&executable).unwrap();

        let qt_include_paths = qt_installation.include_paths(qt_modules);
        let qt_framework_paths = qt_installation.framework_paths(qt_modules);

        Self {
            executable,
            qt_include_paths,
            qt_framework_paths,
        }
    }

    /// Run moc on a C++ header file and save the output into [cargo's OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html).
    /// The return value contains the path to the generated C++ file, which can then be passed to [cc::Build::files](https://docs.rs/cc/latest/cc/struct.Build.html#method.file),
    /// as well as the path to the generated metatypes.json file, which can be used for QML modules.
    pub fn compile(&self, input_file: impl AsRef<Path>, arguments: MocArguments) -> MocProducts {
        let input_path = input_file.as_ref();
        // Put all the moc files into one place, this can then be added to the include path
        let moc_dir = QtTool::Moc.writable_path();
        std::fs::create_dir_all(&moc_dir).expect("Could not create moc dir");
        let output_path = moc_dir.join(format!(
            "moc_{}.cpp",
            input_path.file_name().unwrap().to_str().unwrap()
        ));

        let metatypes_json_path = PathBuf::from(&format!("{}.json", output_path.display()));

        let mut include_args = vec![];
        // Qt includes
        for include_path in self
            .qt_include_paths
            .iter()
            .chain(arguments.include_paths.iter())
        {
            include_args.push(format!("-I{}", include_path.display()));
        }

        // Qt frameworks for macOS
        for framework_path in &self.qt_framework_paths {
            include_args.push(format!("-F{}", framework_path.display()));
        }

        let mut cmd = Command::new(&self.executable);

        if let Some(uri) = arguments.uri {
            cmd.arg(format!("-Muri={uri}", uri = uri.as_dots()));
        }

        cmd.args(include_args);
        cmd.arg(input_path.to_str().unwrap())
            .arg("-o")
            .arg(output_path.to_str().unwrap())
            .arg("--output-json");
        let cmd = cmd
            // Binaries should work without environment and this prevents
            // LD_LIBRARY_PATH from causing different Qt version clashes
            .env_clear()
            .output()
            .unwrap_or_else(|_| panic!("moc failed for {}", input_path.display()));

        if !cmd.status.success() {
            panic!(
                "moc failed for {}:\n{}",
                input_path.display(),
                String::from_utf8_lossy(&cmd.stderr)
            );
        }

        MocProducts {
            cpp: output_path,
            metatypes_json: metatypes_json_path,
        }
    }
}