// file: poc-qt/poc001/build.rs use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; use cxx_qt_build::{CxxQtBuilder, QmlModule}; use serde::Deserialize; use serde_json::Value; const DEFAULT_CONFIG_STR: &str = include_str!("build_config.default.json"); const CONFIG_SCHEMA_STR: &str = include_str!("../build_config.schema.json"); #[derive(Debug, Deserialize)] struct BuildConfig { active_profile: String, profiles: BTreeMap, } #[derive(Debug, Deserialize)] struct BuildProfile { qt: QtConfig, link: LinkConfig, } #[derive(Debug, Deserialize)] struct QtConfig { qmake: String, lib_dir: String, qml_dir: String, plugins_dir: String, version_major: u8, } #[derive(Debug, Deserialize)] struct LinkConfig { rpaths: Vec, } fn ensure_user_config_exists(config_path: &Path) { if config_path.exists() { return; } match fs::write(config_path, DEFAULT_CONFIG_STR) { Ok(()) => { println!( "cargo:warning=build_config.json was missing and has been created from build_config.default.json" ); } Err(err) => { panic!( "failed to create {} from embedded defaults: {}", config_path.display(), err ); } } } fn validate_embedded_schema() { let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json"); match jsonschema::meta::validate(&schema_json) { Ok(()) => {} Err(err) => { panic!("embedded build_config.schema.json is not a valid JSON Schema: {err}"); } } } fn validate_default_config() { validate_config_str_against_schema(DEFAULT_CONFIG_STR, "embedded build_config.default.json"); } fn validate_user_config(config_path: &Path) { let raw = match fs::read_to_string(config_path) { Ok(v) => v, Err(err) => { panic!("failed to read {}: {}", config_path.display(), err); } }; validate_config_str_against_schema(&raw, &format!("{}", config_path.display())); } fn validate_config_str_against_schema(config_raw: &str, config_name: &str) { let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json"); let config_json = parse_json_str(config_raw, config_name); let validator = match jsonschema::validator_for(&schema_json) { Ok(v) => v, Err(err) => { panic!( "failed to build JSON Schema validator for {}: {}", config_name, err ); } }; if validator.is_valid(&config_json) { return; } let mut errors = Vec::new(); for err in validator.iter_errors(&config_json) { errors.push(format!( "- {} at instance path {}", err, err.instance_path() )); } let joined = errors.join("\n"); panic!("{} failed schema validation:\n{}", config_name, joined); } fn load_build_config(config_path: &Path) -> BuildConfig { let raw = match fs::read_to_string(config_path) { Ok(v) => v, Err(err) => { panic!("failed to read {}: {}", config_path.display(), err); } }; match serde_json::from_str::(&raw) { Ok(v) => v, Err(err) => { panic!( "failed to deserialize {} into BuildConfig: {}", config_path.display(), err ); } } } fn resolve_profile_name(build_config: &BuildConfig) -> String { let env_profile = std::env::var("BUILD_CONFIG_PROFILE"); match env_profile { Ok(v) => { if v.trim().is_empty() { build_config.active_profile.clone() } else { v } } Err(_) => build_config.active_profile.clone(), } } fn resolve_profile<'a>(build_config: &'a BuildConfig, profile_name: &str) -> &'a BuildProfile { match build_config.profiles.get(profile_name) { Some(v) => v, None => { let mut available = Vec::new(); for key in build_config.profiles.keys() { available.push(key.clone()); } panic!( "active profile '{}' not found in build_config.json. available profiles: {}", profile_name, available.join(", ") ); } } } fn validate_profile_paths(profile: &BuildProfile, profile_name: &str) { ensure_existing_file(&profile.qt.qmake, profile_name, "qt.qmake"); ensure_existing_dir(&profile.qt.lib_dir, profile_name, "qt.lib_dir"); ensure_existing_dir(&profile.qt.qml_dir, profile_name, "qt.qml_dir"); ensure_existing_dir(&profile.qt.plugins_dir, profile_name, "qt.plugins_dir"); let mut index = 0usize; while index < profile.link.rpaths.len() { ensure_existing_dir( &profile.link.rpaths[index], profile_name, &format!("link.rpaths[{index}]"), ); index += 1; } } fn ensure_existing_file(path_str: &str, profile_name: &str, field_name: &str) { let path = Path::new(path_str); if !path.exists() { panic!( "profile '{}' has invalid {}: path does not exist: {}", profile_name, field_name, path.display() ); } if !path.is_file() { panic!( "profile '{}' has invalid {}: path is not a file: {}", profile_name, field_name, path.display() ); } } fn ensure_existing_dir(path_str: &str, profile_name: &str, field_name: &str) { let path = Path::new(path_str); if !path.exists() { panic!( "profile '{}' has invalid {}: path does not exist: {}", profile_name, field_name, path.display() ); } if !path.is_dir() { panic!( "profile '{}' has invalid {}: path is not a directory: {}", profile_name, field_name, path.display() ); } } fn apply_build_environment(profile: &BuildProfile) { unsafe { std::env::set_var("QMAKE", &profile.qt.qmake); std::env::set_var("QT_VERSION_MAJOR", profile.qt.version_major.to_string()); } println!("cargo:rerun-if-env-changed=BUILD_CONFIG_PROFILE"); println!("cargo:rerun-if-env-changed=QMAKE"); println!("cargo:rerun-if-env-changed=QT_VERSION_MAJOR"); } fn apply_linker_settings(profile: &BuildProfile) { let mut index = 0usize; while index < profile.link.rpaths.len() { println!( "cargo:rustc-link-arg=-Wl,-rpath,{}", profile.link.rpaths[index] ); index += 1; } } fn emit_runtime_hints(profile: &BuildProfile, profile_name: &str) { println!( "cargo:warning=using build profile '{}' with qmake '{}'", profile_name, profile.qt.qmake ); println!( "cargo:warning=qt lib_dir='{}', qml_dir='{}', plugins_dir='{}'", profile.qt.lib_dir, profile.qt.qml_dir, profile.qt.plugins_dir ); } fn parse_json_str(raw: &str, label: &str) -> Value { match serde_json::from_str::(raw) { Ok(v) => v, Err(err) => { panic!("failed to parse {} as JSON: {}", label, err); } } } fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=build_config.default.json"); println!("cargo:rerun-if-changed=build_config.schema.json"); println!("cargo:rerun-if-changed=src/bridge.rs"); println!("cargo:rerun-if-changed=qml/main.qml"); let config_path = PathBuf::from("build_config.json"); ensure_user_config_exists(&config_path); validate_embedded_schema(); validate_default_config(); validate_user_config(&config_path); let build_config = load_build_config(&config_path); let profile_name = resolve_profile_name(&build_config); let profile = resolve_profile(&build_config, &profile_name); validate_profile_paths(profile, &profile_name); apply_build_environment(profile); apply_linker_settings(profile); CxxQtBuilder::new_qml_module(QmlModule::new("com.sasedev.hello").qml_files(["qml/main.qml"])) .qt_module("Qml") .qt_module("Network") .files(["src/bridge.rs"]) .build(); emit_runtime_hints(profile, &profile_name); }