Skip to content

cxx-qt-build: return an Interface from CxxQtBuilder #1224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
136 changes: 2 additions & 134 deletions crates/cxx-qt-build/src/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,100 +7,9 @@

use serde::{Deserialize, Serialize};

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::path::PathBuf;

/// When generating a library with cxx-qt-build, the library may need to export certain flags or headers.
/// These are all specified by this Interface struct, which should be passed to the [crate::CxxQtBuilder::library] function.
pub struct Interface {
// The name of the links keys, whose CXX-Qt dependencies to reexport
pub(crate) reexport_links: HashSet<String>,
pub(crate) exported_include_prefixes: Vec<String>,
pub(crate) exported_include_directories: Vec<(PathBuf, String)>,
// TODO: In future, we want to also set up the include paths so that you can include anything
// from the crates source directory.
// Once this is done, this flag should indicate whether or not to export our own crates source
// directory to downstream dependencies?
// export_crate_directory: bool,
}

impl Default for Interface {
fn default() -> Self {
Self {
reexport_links: HashSet::new(),
exported_include_prefixes: vec![super::crate_name()],
exported_include_directories: Vec::new(),
}
}
}

impl Interface {
/// Export all headers with the given prefix to downstream dependencies
///
/// Note: This will overwrite any previously specified header_prefixes, including the default
/// header_prefix of this crate.
///
/// This function will panic if any of the given prefixes are already exported through the
/// [Self::export_include_directory] function.
pub fn export_include_prefixes<'a>(
mut self,
prefixes: impl IntoIterator<Item = &'a str>,
) -> Self {
let prefixes = prefixes.into_iter().map(|item| item.to_string()).collect();

let mut exported_prefixes = self
.exported_include_directories
.iter()
.map(|(_path, prefix)| prefix);
for prefix in &prefixes {
if let Some(duplicate) =
exported_prefixes.find(|exported_prefix| exported_prefix.starts_with(prefix))
{
panic!("Duplicate export_prefix! Cannot export `{prefix}`, as `{duplicate}` is already exported as an export_include_directory!");
}
}

self.exported_include_prefixes = prefixes;
self
}

/// Add a directory that will be added as an include directory under the given prefix.
///
/// The prefix will automatically be exported (see also: [Self::export_include_prefixes])
///
/// This function will panic if the given prefix is already exported.
pub fn export_include_directory(mut self, directory: impl AsRef<Path>, prefix: &str) -> Self {
let mut exported_prefixes = self.exported_include_prefixes.iter().chain(
self.exported_include_directories
.iter()
.map(|(_path, prefix)| prefix),
);
if let Some(duplicate) =
exported_prefixes.find(|exported_prefix| exported_prefix.starts_with(prefix))
{
panic!("Duplicate export_prefix! Cannot export `{prefix}`, as `{duplicate}` is already exported!");
}

self.exported_include_directories
.push((directory.as_ref().into(), prefix.to_owned()));
self
}

/// Reexport the dependency with the given link name.
/// This will make the dependency available to downstream dependencies.
///
/// Specifically it will reexport all include_prefixes of the given dependency
/// as well as any definitions made by that dependency.
///
/// Note that the link name may differ from the crate name.
/// Check your dependencies manifest file for the correct link name.
pub fn reexport_dependency(mut self, link_name: &str) -> Self {
self.reexport_links.insert(link_name.to_owned());
self
}
}

#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Default, Serialize, Deserialize)]
/// This struct is used by cxx-qt-build internally to propagate data through to downstream
/// dependencies
pub(crate) struct Manifest {
Expand Down Expand Up @@ -162,44 +71,3 @@ pub(crate) fn initializers(dependencies: &[Dependency]) -> Vec<qt_build_utils::I
.flat_map(|dep| dep.manifest.initializers.iter().cloned())
.collect()
}

pub(crate) fn all_include_prefixes(
interface: &Interface,
dependencies: &[Dependency],
) -> Vec<String> {
interface
.exported_include_prefixes
.iter()
.cloned()
.chain(
interface
.exported_include_directories
.iter()
.map(|(_path, prefix)| prefix.clone()),
)
.chain(
dependencies
.iter()
.flat_map(|dep| &dep.manifest.exported_include_prefixes)
.cloned(),
)
.collect()
}

pub(crate) fn reexported_dependencies(
interface: &Interface,
dependencies: &[Dependency],
) -> Vec<Dependency> {
let mut exported_dependencies = Vec::new();
for link_name in &interface.reexport_links {
if let Some(dependency) = dependencies
.iter()
.find(|dep| &dep.manifest.link_name == link_name)
{
exported_dependencies.push(dependency.clone());
} else {
panic!("Could not find dependency with link name `{link_name}` to reexport!");
}
}
exported_dependencies
}
4 changes: 4 additions & 0 deletions crates/cxx-qt-build/src/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ pub(crate) fn header_root() -> PathBuf {
crate_target().join("include")
}

pub(crate) fn header_root_interface() -> PathBuf {
target().join("include-interface")
}

/// The OUT_DIR, converted into a PathBuf
pub(crate) fn out() -> PathBuf {
env::var("OUT_DIR").unwrap().into()
Expand Down
210 changes: 210 additions & 0 deletions crates/cxx-qt-build/src/interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: 2024 Klarälvdalens Datakonsult AB, a KDAB Group company <[email protected]>
// SPDX-FileContributor: Leon Matthes <[email protected]>
//
// SPDX-License-Identifier: MIT OR Apache-2.0

//! This modules contains utilities for specifying interfaces with cxx-qt-build.

use std::collections::HashSet;

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

use crate::{dir, dir::INCLUDE_VERB, Dependency, Manifest};

/// When generating a library with cxx-qt-build, the library may need to export certain flags or headers.
/// These are all specified by this Interface struct.
pub struct Interface {
// The name of the links keys, whose CXX-Qt dependencies to reexport
pub(crate) reexport_links: HashSet<String>,
pub(crate) exported_include_prefixes: Vec<String>,
pub(crate) exported_include_directories: Vec<(PathBuf, String)>,
pub(crate) manifest: Manifest,
pub(crate) dependencies: Vec<Dependency>,
// TODO: In future, we want to also set up the include paths so that you can include anything
// from the crates source directory.
// Once this is done, this flag should indicate whether or not to export our own crates source
// directory to downstream dependencies?
// export_crate_directory: bool,
}

impl Default for Interface {
fn default() -> Self {
Self {
reexport_links: HashSet::new(),
exported_include_prefixes: vec![super::crate_name()],
exported_include_directories: Vec::new(),
manifest: Manifest::default(),
dependencies: Vec::new(),
}
}
}

impl Interface {
/// Export all headers with the given prefix to downstream dependencies
///
/// Note: This will overwrite any previously specified header_prefixes, including the default
/// header_prefix of this crate.
///
/// This function will panic if any of the given prefixes are already exported through the
/// [Self::export_include_directory] function.
pub fn export_include_prefixes<'a>(
mut self,
prefixes: impl IntoIterator<Item = &'a str>,
) -> Self {
let prefixes = prefixes.into_iter().map(|item| item.to_string()).collect();

let mut exported_prefixes = self
.exported_include_directories
.iter()
.map(|(_path, prefix)| prefix);
for prefix in &prefixes {
if let Some(duplicate) =
exported_prefixes.find(|exported_prefix| exported_prefix.starts_with(prefix))
{
panic!("Duplicate export_prefix! Cannot export `{prefix}`, as `{duplicate}` is already exported as an export_include_directory!");
}
}

self.exported_include_prefixes = prefixes;
self
}

/// Add a directory that will be added as an include directory under the given prefix.
///
/// The prefix will automatically be exported (see also: [Self::export_include_prefixes])
///
/// This function will panic if the given prefix is already exported.
pub fn export_include_directory(mut self, directory: impl AsRef<Path>, prefix: &str) -> Self {
let mut exported_prefixes = self.exported_include_prefixes.iter().chain(
self.exported_include_directories
.iter()
.map(|(_path, prefix)| prefix),
);
if let Some(duplicate) =
exported_prefixes.find(|exported_prefix| exported_prefix.starts_with(prefix))
{
panic!("Duplicate export_prefix! Cannot export `{prefix}`, as `{duplicate}` is already exported!");
}

self.exported_include_directories
.push((directory.as_ref().into(), prefix.to_owned()));
self
}

/// Reexport the dependency with the given link name.
/// This will make the dependency available to downstream dependencies.
///
/// Specifically it will reexport all include_prefixes of the given dependency
/// as well as any definitions made by that dependency.
///
/// Note that the link name may differ from the crate name.
/// Check your dependencies manifest file for the correct link name.
pub fn reexport_dependency(mut self, link_name: &str) -> Self {
self.reexport_links.insert(link_name.to_owned());
self
}

/// Export the manifest for this crate so that is can be used by downstream
/// crates or CMake
pub fn export(mut self) {
// Ensure that a link name has been set
if self.manifest.link_name.is_empty() {
panic!("The links key must be set when exporting with CXX-Qt-build");
}

self.write_exported_include_directories();

// We automatically reexport all qt_modules and downstream dependencies
// as they will always need to be enabled in the final binary.
// However, we only reexport the headers of libraries that
// are marked as re-export.
let dependencies = reexported_dependencies(&self, &self.dependencies);

self.manifest.exported_include_prefixes = all_include_prefixes(&self, &dependencies);

let manifest_path = dir::crate_target().join("manifest.json");
let manifest_json = serde_json::to_string_pretty(&self.manifest)
.expect("Failed to convert Manifest to JSON!");
std::fs::write(&manifest_path, manifest_json).expect("Failed to write manifest.json!");
println!(
"cargo::metadata=CXX_QT_MANIFEST_PATH={}",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes we have a links key set, right?
We should probably panic if it isn't the case here.
However, then we'd have to check whether we're currently exporting to CMake, as then it's not needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few lines further up (at the start of this export() function) we panic if the link name is empty?

manifest_path.to_string_lossy()
);
}

fn write_exported_include_directories(&self) {
// Add any export directories as a child of the include interface directory
let header_root_interface = dir::header_root_interface();
std::fs::create_dir_all(&header_root_interface)
.expect("Failed to create header root for interface");
for (header_dir, dest) in &self.exported_include_directories {
let dest_dir = header_root_interface.join(dest);
// Relative paths are take from under the header root
//
// TODO: is this correct?
let header_dir = if header_dir.is_relative() {
dir::header_root().join(header_dir)
} else {
header_dir.clone()
};
match dir::symlink_or_copy_directory(&header_dir, &dest_dir) {
Ok(true) => {},
Ok(false) => panic!("Failed to create symlink folder for `{dest}`"),
Err(e) => panic!(
"Failed to {INCLUDE_VERB} `{dest}` for export_include_directory `{dir_name}`: {e:?}",
dir_name = header_dir.to_string_lossy()
)
};
}

// Add any reexport links as a child of the include interface directory
let header_root = dir::header_root();
for reexport in &self.reexport_links {
let source_dir = header_root.join(reexport);
let dest_dir = header_root_interface.join(reexport);
match dir::symlink_or_copy_directory(&source_dir, &dest_dir) {
Ok(true) => {},
Ok(false) => panic!("Failed to create symlink folder for `{reexport}`"),
Err(e) => panic!(
"Failed to {INCLUDE_VERB} `{reexport}` for export_include_directory `{dir_name}`: {e:?}",
dir_name = source_dir.to_string_lossy()
)
};
}
}
}

fn all_include_prefixes(interface: &Interface, dependencies: &[Dependency]) -> Vec<String> {
interface
.exported_include_prefixes
.iter()
.cloned()
.chain(
interface
.exported_include_directories
.iter()
.map(|(_path, prefix)| prefix.clone()),
)
.chain(
dependencies
.iter()
.flat_map(|dep| &dep.manifest.exported_include_prefixes)
.cloned(),
)
.collect()
}

fn reexported_dependencies(interface: &Interface, dependencies: &[Dependency]) -> Vec<Dependency> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in impl Interface, no?
Same with all_include_prefix.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be, this has just moved the existing code :-)

let mut exported_dependencies = Vec::new();
for link_name in &interface.reexport_links {
if let Some(dependency) = dependencies
.iter()
.find(|dep| &dep.manifest.link_name == link_name)
{
exported_dependencies.push(dependency.clone());
} else {
panic!("Could not find dependency with link name `{link_name}` to reexport!");
}
}
exported_dependencies
}
Loading
Loading