Skip to content

Commit

Permalink
[WIP] register QML modules at build time
Browse files Browse the repository at this point in the history
This is a prerequisite for using qmlcachegen
KDAB#242
  • Loading branch information
Be-ing committed Aug 8, 2023
1 parent 3454e1d commit ff21007
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 85 deletions.
77 changes: 27 additions & 50 deletions crates/cxx-qt-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use std::{
path::{Path, PathBuf},
};

pub use qt_build_utils::QmlModule;

use cxx_qt_gen::{
parse_qt_file, write_cpp, write_rust, CppFragment, CxxQtItem, GeneratedCppBlocks,
GeneratedRustBlocks, Parser,
Expand All @@ -49,18 +51,6 @@ struct GeneratedCpp {
file_ident: String,
}

/// Metadata for registering a QML module
struct QmlModule {
/// The URI of the QML module
pub uri: String,
/// The minor version of the QML module
pub version_minor: usize,
/// The major version of the QML module
pub version_major: usize,
/// The .rs files with #[qml_element] attribute(s)
pub rust_files: Vec<PathBuf>,
}

impl GeneratedCpp {
/// Generate QObject and cxx header/source C++ file contents
pub fn new(rust_file_path: impl AsRef<Path>) -> Result<Self, Diagnostic> {
Expand Down Expand Up @@ -226,15 +216,15 @@ impl GeneratedCpp {

/// Generate C++ files from a given list of Rust files, returning the generated paths
fn generate_cxxqt_cpp_files(
rs_source: &[PathBuf],
rs_source: &[impl AsRef<Path>],
header_dir: impl AsRef<Path>,
) -> Vec<GeneratedCppFilePaths> {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();

let mut generated_file_paths: Vec<GeneratedCppFilePaths> = Vec::with_capacity(rs_source.len());
for rs_path in rs_source {
let cpp_directory = format!("{}/cxx-qt-gen/src", env::var("OUT_DIR").unwrap());
let path = format!("{manifest_dir}/{}", rs_path.display());
let path = format!("{manifest_dir}/{}", rs_path.as_ref().display());
println!("cargo:rerun-if-changed={path}");

let generated_code = match GeneratedCpp::new(&path) {
Expand All @@ -251,12 +241,12 @@ fn generate_cxxqt_cpp_files(
}

fn panic_duplicate_file_and_qml_module(
path: &Path,
path: impl AsRef<Path>,
uri: &str,
version_major: usize,
version_minor: usize,
) {
panic!("CXX-Qt bridge Rust file {} specified in QML module {uri} (version {version_major}.{version_minor}), but also specified via CxxQtBuilder::file. Bridge files must be specified via CxxQtBuilder::file or CxxQtBuilder::qml_module, but not both.", path.display());
panic!("CXX-Qt bridge Rust file {} specified in QML module {uri} (version {version_major}.{version_minor}), but also specified via CxxQtBuilder::file. Bridge files must be specified via CxxQtBuilder::file or CxxQtBuilder::qml_module, but not both.", path.as_ref().display());
}

/// Run cxx-qt's C++ code generator on Rust modules marked with the `cxx_qt::bridge` macro, compile
Expand Down Expand Up @@ -292,16 +282,24 @@ fn panic_duplicate_file_and_qml_module(
/// In addition to autogenerating and building QObject C++ subclasses, manually written QObject
/// subclasses can be parsed by moc and built using [CxxQtBuilder::qobject_header].
#[derive(Default)]
pub struct CxxQtBuilder {
pub struct CxxQtBuilder<'a, A, B>
where
A: AsRef<Path>,
B: AsRef<Path>,
{
rust_sources: Vec<PathBuf>,
qobject_headers: Vec<PathBuf>,
qrc_files: Vec<PathBuf>,
qt_modules: HashSet<String>,
qml_modules: Vec<QmlModule>,
qml_modules: Vec<QmlModule<'a, A, B>>,
cc_builder: cc::Build,
}

impl CxxQtBuilder {
impl<'a, A, B> CxxQtBuilder<'a, A, B>
where
A: AsRef<Path> + PartialEq,
B: AsRef<Path>,
{
/// Create a new builder
pub fn new() -> Self {
let mut qt_modules = HashSet::new();
Expand All @@ -322,8 +320,7 @@ impl CxxQtBuilder {

/// Specify rust file paths to parse through the cxx-qt marco
/// Relative paths are treated as relative to the path of your crate's Cargo.toml file
pub fn file(mut self, rust_source: impl AsRef<Path>) -> Self {
let rust_source = rust_source.as_ref().to_path_buf();
pub fn file(mut self, rust_source: A) -> Self {
for qml_module in &self.qml_modules {
if qml_module.rust_files.contains(&rust_source) {
panic_duplicate_file_and_qml_module(
Expand All @@ -334,6 +331,7 @@ impl CxxQtBuilder {
);
}
}
let rust_source = rust_source.as_ref().to_path_buf();
println!("cargo:rerun-if-changed={}", rust_source.display());
self.rust_sources.push(rust_source);
self
Expand Down Expand Up @@ -375,28 +373,8 @@ impl CxxQtBuilder {
}

/// Register a QML module at build time
pub fn qml_module(
mut self,
uri: &str,
version_major: usize,
version_minor: usize,
rust_files: &[impl AsRef<Path>],
) -> Self {
let rust_files: Vec<PathBuf> = rust_files
.iter()
.map(|p| p.as_ref().to_path_buf())
.collect();
for path in &rust_files {
if self.rust_sources.contains(path) {
panic_duplicate_file_and_qml_module(path, uri, version_major, version_minor);
}
}
self.qml_modules.push(QmlModule {
uri: uri.to_owned(),
version_major,
version_minor,
rust_files,
});
pub fn qml_module(mut self, qml_module: QmlModule<'a, A, B>) -> CxxQtBuilder<'a, A, B> {
self.qml_modules.push(qml_module);
self
}

Expand Down Expand Up @@ -532,6 +510,8 @@ impl CxxQtBuilder {

let mut cc_builder_whole_archive_files_added = false;

let lib_name = "cxx-qt-generated";

// Bridges for QML modules are handled separately because
// the metatypes_json generated by moc needs to be passed to qmltyperegistrar
for qml_module in self.qml_modules {
Expand All @@ -548,16 +528,13 @@ impl CxxQtBuilder {
}
}

let qml_type_registration_files = qtbuild.register_qml_types(
&qml_metatypes_json,
qml_module.version_major,
qml_module.version_minor,
&qml_module.uri,
);
let qml_type_registration_files =
qtbuild.register_qml_module(&qml_metatypes_json, &qml_module, lib_name);
self.cc_builder
.file(qml_type_registration_files.qmltyperegistrar);
self.cc_builder.file(qml_type_registration_files.plugin);
cc_builder_whole_archive.file(qml_type_registration_files.plugin_init);
cc_builder_whole_archive.file(qml_type_registration_files.qrc);
self.cc_builder.define("QT_STATICPLUGIN", None);
cc_builder_whole_archive_files_added = true;
}
Expand Down Expand Up @@ -599,6 +576,6 @@ impl CxxQtBuilder {
if cc_builder_whole_archive_files_added {
cc_builder_whole_archive.compile("qt-static-initializers");
}
self.cc_builder.compile("cxx-qt-gen");
self.cc_builder.compile(lib_name);
}
}
132 changes: 118 additions & 14 deletions crates/qt-build-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,51 @@ pub use versions::SemVer;

use thiserror::Error;

/// Metadata for registering a QML module
pub struct QmlModule<'a, A, B>
where
A: AsRef<Path>,
// Use a separate generic to allow using different types that impl AsRef<Path>
B: AsRef<Path>,
{
/// The URI of the QML module
pub uri: &'a str,
/// The minor version of the QML module
pub version_minor: usize,
/// The major version of the QML module
pub version_major: usize,
/// The .rs files with #[qml_element] attribute(s)
pub rust_files: &'a [A],
/// .qml files included in the module
pub qml_files: &'a [B],
/// Other QRC resources (such as images) included in the module
//
// Reuse the `A` generic from rust_files to allow the compiler to infer the
// type when constructing the struct with Default::default. Using a separate
// generic for this field would be more flexible, but it would require users
// to explicitly specify the type even for an empty slice (like `&[] as &[&str; 0]`)
// and an empty slice is likely desired in most cases; most users probably don't
// care about this field.
pub qrc_files: &'a [A],
}

impl<'a, A, B> Default for QmlModule<'a, A, B>
where
A: AsRef<Path>,
B: AsRef<Path>,
{
fn default() -> Self {
QmlModule {
uri: "com.example.cxx_qt_module",
version_major: 1,
version_minor: 0,
rust_files: &[],
qml_files: &[],
qrc_files: &[],
}
}
}

#[derive(Error, Debug)]
pub enum QtBuildError {
/// `QMAKE` environment variable was set but Qt was not detected
Expand Down Expand Up @@ -124,6 +169,8 @@ pub struct QmlTypeRegistrationFiles {
/// The compiled static library must be linked with [+whole-archive](https://doc.rust-lang.org/rustc/command-line-arguments.html#linking-modifiers-whole-archive)
/// or the linker will discard the generated static variables because they are not referenced from `main`.
pub plugin_init: PathBuf,
/// File generated by rcc for the QML plugin
pub qrc: PathBuf,
}

/// Helper for build.rs scripts using Qt
Expand Down Expand Up @@ -571,34 +618,91 @@ impl QtBuild {
}

/// Generate C++ files to automatically register a QML element at build time using the JSON output from [moc](Self::moc)
pub fn register_qml_types(
pub fn register_qml_module<'a, A, B>(
&mut self,
metatypes_json: &[impl AsRef<Path>],
major_version: usize,
minor_version: usize,
import_name: &str,
) -> QmlTypeRegistrationFiles {
qml_module: &QmlModule<'a, A, B>,
library_name: &str,
) -> QmlTypeRegistrationFiles
where
A: AsRef<Path>,
B: AsRef<Path>,
{
if self.qmltyperegistrar_executable.is_none() {
self.qmltyperegistrar_executable = Some(
self.get_qt_tool("qmltyperegistrar")
.expect("Could not find qmltyperegistrar"),
);
}

let uri = &qml_module.uri;
let qml_uri_dirs = uri.replace('.', "/");

let out_dir = env::var("OUT_DIR").unwrap();
let qml_module_dir = format!("{out_dir}/qml_modules/{qml_uri_dirs}");
std::fs::create_dir_all(&qml_module_dir).expect("Could not create QML module directory");

let qml_uri_cpp_symbol_safe = uri.replace('.', "_");
let typeinfo_path = format!("{qml_uri_cpp_symbol_safe}.qmltypes");
let plugin_class_name = format!("{qml_uri_cpp_symbol_safe}_plugin");

let mut qmldir =
File::create(format!("{qml_module_dir}/qmldir")).expect("Could not create qmldir file");
write!(
qmldir,
"module {uri}
optional plugin {library_name}
classname {plugin_class_name}
typeinfo {typeinfo_path}
prefer :/qt/qml/{qml_uri_dirs}/
"
)
.expect("Could not write qmldir file");

fn qrc_file_line(file_path: &impl AsRef<Path>) -> String {
format!(
" <file alias=\"{}\">{}</file>\n",
file_path.as_ref().display(),
std::fs::canonicalize(file_path).unwrap().display()
)
}

let mut qml_files_qrc = String::new();
for file_path in qml_module.qml_files {
qml_files_qrc.push_str(&qrc_file_line(file_path));
}
for file_path in qml_module.qrc_files {
qml_files_qrc.push_str(&qrc_file_line(file_path));
}

let qrc_path = format!("{qml_module_dir}/qml_module_resources.qrc");
let mut qrc = File::create(&qrc_path).expect("Could not create qrc file");
write!(
qrc,
r#"<RCC>
<qresource prefix="/">
<file alias="/qt/qml/{qml_uri_dirs}">{qml_module_dir}</file>
</qresource>
<qresource prefix="/qt/qml/{qml_uri_dirs}">
{qml_files_qrc}
<file alias="qmldir">{qml_module_dir}/qmldir</file>
</qresource>
</RCC>
"#
)
.expect("Could note write qrc file");
let qrc_output = self.qrc(&qrc_path);

let qml_uri_cpp_symbol_safe = import_name.replace('.', "_");
let output_path = PathBuf::from(&format!(
"{out_dir}/{qml_uri_cpp_symbol_safe}_qmltyperegistration.cpp"
));

let mut args = vec![
"--major-version".to_string(),
major_version.to_string(),
qml_module.version_major.to_string(),
"--minor-version".to_string(),
minor_version.to_string(),
qml_module.version_minor.to_string(),
"--import-name".to_string(),
import_name.to_string(),
uri.to_string(),
"-o".to_string(),
output_path.to_string_lossy().to_string(),
];
Expand All @@ -611,16 +715,15 @@ impl QtBuild {
let cmd = Command::new(self.qmltyperegistrar_executable.as_ref().unwrap())
.args(args)
.output()
.unwrap_or_else(|_| panic!("qmltyperegistrar failed for {import_name}"));
.unwrap_or_else(|_| panic!("qmltyperegistrar failed for {uri}"));

if !cmd.status.success() {
panic!(
"qmltyperegistrar failed for {import_name}:\n{}",
"qmltyperegistrar failed for {uri}:\n{}",
String::from_utf8_lossy(&cmd.stderr)
);
}

let plugin_class_name = format!("{qml_uri_cpp_symbol_safe}_plugin");
// This function is generated by qmltyperegistrar
let register_types_function = format!("qml_register_types_{qml_uri_cpp_symbol_safe}");

Expand Down Expand Up @@ -651,7 +754,7 @@ public:
"#
)
.unwrap();
self.moc(&qml_plugin_cpp_path, Some(import_name));
self.moc(&qml_plugin_cpp_path, Some(uri));

let qml_plugin_init_path = PathBuf::from(format!("{out_dir}/{plugin_class_name}_init.cpp"));
let mut qml_plugin_init = File::create(&qml_plugin_init_path).unwrap();
Expand All @@ -668,6 +771,7 @@ Q_IMPORT_PLUGIN({plugin_class_name});
qmltyperegistrar: output_path,
plugin: qml_plugin_cpp_path,
plugin_init: qml_plugin_init_path,
qrc: qrc_output,
}
}

Expand Down
12 changes: 7 additions & 5 deletions examples/cargo_without_cmake/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

// ANCHOR: book_cargo_executable_build_rs
use cxx_qt_build::CxxQtBuilder;
use cxx_qt_build::{CxxQtBuilder, QmlModule};

fn main() {
CxxQtBuilder::new()
Expand All @@ -14,10 +14,12 @@ fn main() {
// - Qt Qml is linked by enabling the qt_qml Cargo feature (default).
// - Qt Qml requires linking Qt Network on macOS
.qt_module("Network")
.qml_module("com.kdab.cxx_qt.demo", 1, 0, &["src/cxxqt_object.rs"])
// Generate C++ code from the .qrc file with the rcc tool
// https://doc.qt.io/qt-6/resources.html
.qrc("qml/qml.qrc")
.qml_module(QmlModule {
uri: "com.kdab.cxx_qt.demo",
rust_files: &["src/cxxqt_object.rs"],
qml_files: &["qml/main.qml"],
..Default::default()
})
.build();
}
// ANCHOR_END: book_cargo_executable_build_rs
Loading

0 comments on commit ff21007

Please sign in to comment.