#![forbid(unsafe_code)]
pub mod deterministic_id_generator;
mod diagnostics;
pub use ferroui_build_scan::{call_forms, export, model, model_set, scanner};
pub mod type_system;
pub mod xaml_compiler_diagnostics_filter;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::time::Instant;
use ferroui_base::metadata::MarkupAssembly;
use xamlx::type_system::IXamlTypeSystem;
use crate::export::ScanReport;
use crate::model::AssemblyModel;
use crate::model_set::ModelSet;
use crate::scanner::{scan_crate, Scan, ScanOptions, Severity};
use crate::type_system::{ModelEmitTypes, ModelTypeSystem};
pub use ferroui_markup_xaml_loader::rust_emitter::ClassConstructor;
use crate::xaml_compiler_diagnostics_filter::XamlCompilerDiagnosticsFilter;
use ferroui_markup_xaml_loader::compiler_extensions::FerroXamlDiagnosticCodes;
use ferroui_markup_xaml_loader::rust_emitter::{
class_document_group, class_of_document, generate_class_file_with, generate_file_with, rust_string_literal, ClassGroup, CompiledMarkupTypeSystem,
EmitterHost, TransformOptions, XamlMetadata, CLASS_GROUP_NOT_TRANSFORMED, GROUP_NOT_TRANSFORMED,
};
pub const DEFAULT_MODULE: &str = "compiled_xaml";
pub const XAMLMETA_VARIABLE: &str = "FERROUI_XAMLMETA";
pub struct XamlGroup {
module: String,
documents: Option<Vec<(String, String)>>,
create_source_info: Option<bool>,
exported: bool,
class_document: Option<String>,
constructor: Option<ClassConstructor>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TypeSystem {
#[default]
Runtime,
Model,
}
impl XamlGroup {
pub fn new(module: &str) -> Self {
Self { module: module.to_string(), documents: None, create_source_info: None, exported: true, class_document: None, constructor: None }
}
pub fn class_document(mut self, name: &str) -> Self {
self.class_document = Some(name.to_string());
self
}
pub fn constructor(mut self, constructor: ClassConstructor) -> Self {
self.constructor = Some(constructor);
self
}
pub fn documents(mut self, documents: &[(&str, &str)]) -> Self {
self.documents = Some(documents.iter().map(|(name, xaml)| (name.to_string(), xaml.to_string())).collect());
self
}
pub fn create_source_info(mut self, value: bool) -> Self {
self.create_source_info = Some(value);
self
}
pub fn internal(mut self) -> Self {
self.exported = false;
self
}
}
pub use ferroui_build_scan::export::Outcome;
pub struct Build {
manifest_dir: PathBuf,
out_dir: PathBuf,
crate_name: String,
assembly: Option<&'static MarkupAssembly>,
xaml_root: PathBuf,
groups: Vec<XamlGroup>,
asset_directories: Vec<String>,
loaders: Vec<String>,
checked_in_metadata: Vec<PathBuf>,
inputs: Vec<PathBuf>,
dependencies: Vec<PathBuf>,
export_metadata: bool,
source_root: Option<PathBuf>,
type_system: TypeSystem,
default_compile_bindings: bool,
analyzer_config_files: Vec<PathBuf>,
warnings_as_errors: bool,
}
impl Build {
pub fn from_env() -> Self {
let variable = |name: &str| env::var_os(name).unwrap_or_else(|| panic!("{name} is not set: not a build script"));
Self::new(
PathBuf::from(variable("CARGO_MANIFEST_DIR")),
PathBuf::from(variable("OUT_DIR")),
&variable("CARGO_PKG_NAME").to_string_lossy(),
ferroui_build_scan::export::dependencies_from_env(),
)
}
pub fn new(manifest_dir: PathBuf, out_dir: PathBuf, package_name: &str, dependencies: Vec<PathBuf>) -> Self {
Self {
manifest_dir,
out_dir,
crate_name: package_name.replace('-', "_"),
assembly: None,
xaml_root: PathBuf::new(),
groups: Vec::new(),
asset_directories: Vec::new(),
loaders: Vec::new(),
checked_in_metadata: Vec::new(),
inputs: Vec::new(),
dependencies,
export_metadata: false,
source_root: None,
type_system: TypeSystem::Runtime,
default_compile_bindings: false,
analyzer_config_files: Vec::new(),
warnings_as_errors: false,
}
}
pub fn type_system(mut self, type_system: TypeSystem) -> Self {
self.type_system = type_system;
self
}
pub fn default_compile_bindings(mut self, value: bool) -> Self {
self.default_compile_bindings = value;
self
}
pub fn analyzer_config_files(mut self, files: &[&str]) -> Self {
self.analyzer_config_files.extend(files.iter().map(|file| self.manifest_dir.join(file)));
self
}
pub fn warnings_as_errors(mut self, value: bool) -> Self {
self.warnings_as_errors = value;
self
}
pub fn export_metadata(mut self) -> Self {
self.export_metadata = true;
self
}
pub fn source_root(mut self, file: &str) -> Self {
self.source_root = Some(PathBuf::from(file));
self
}
fn scan(&self, dependencies: Vec<AssemblyModel>) -> Result<Scan, String> {
let root = ferroui_build_scan::export::source_root(&self.manifest_dir, self.source_root.as_deref())?;
let mut options = ScanOptions::new(&self.crate_name, root).with_dependencies(dependencies);
options.assembly_name = self.assembly.map(|assembly| assembly.name.to_string());
Ok(scan_crate(&options))
}
pub fn assembly(mut self, assembly: &'static MarkupAssembly) -> Self {
self.assembly = Some(assembly);
self
}
pub fn xaml_root(mut self, directory: &str) -> Self {
self.xaml_root = PathBuf::from(directory);
self
}
pub fn input(mut self, path: &str) -> Self {
self.inputs.push(self.manifest_dir.join(path));
self
}
pub fn embed_assets(mut self, directories: &[&str]) -> Self {
self.asset_directories.extend(directories.iter().map(|directory| directory.to_string()));
self
}
pub fn compile_xaml(self) -> Self {
self.compile_group(XamlGroup::new(DEFAULT_MODULE))
}
pub fn compile_group(mut self, group: XamlGroup) -> Self {
self.groups.push(group);
self
}
pub fn loader(mut self, path: &str) -> Self {
self.loaders.push(path.to_string());
self
}
pub fn checked_in_metadata(mut self, path: &str) -> Self {
self.checked_in_metadata.push(self.manifest_dir.join(path));
self
}
pub fn run(self) {
let outcome = self.execute();
for line in &outcome.lines {
println!("{line}");
}
if !outcome.errors.is_empty() {
for error in &outcome.errors {
eprintln!("error: {error}");
println!("cargo::error={error}");
}
std::process::exit(1);
}
}
pub fn execute(self) -> Outcome {
let mut lines = Vec::new();
let mut errors = Vec::new();
let rerun = |lines: &mut Vec<String>, path: &Path| lines.push(format!("cargo::rerun-if-changed={}", path.display()));
rerun(&mut lines, &self.manifest_dir.join("build.rs"));
rerun(&mut lines, &self.manifest_dir.join("Cargo.toml"));
for input in &self.inputs {
rerun(&mut lines, input);
}
let on_models = self.type_system == TypeSystem::Model;
if self.assembly.is_none() && !on_models && (!self.export_metadata || !self.groups.is_empty()) {
errors.push("the assembly of the crate is not stated (Build::assembly)".to_string());
return Outcome { lines, errors };
}
if !on_models && !self.groups.is_empty() && !cfg!(feature = "runtime-host") {
errors.push(
"the documents are compiled against the type models (Build::type_system(TypeSystem::Model)): the run-time type system is the feature `runtime-host` of ferroui-build, which links the XAML runtime library and the controls"
.to_string(),
);
return Outcome { lines, errors };
}
let mut report = ScanReport { crate_name: self.crate_name.clone(), ..ScanReport::default() };
let started = Instant::now();
let (models, dependency_files) = match ModelSet::read(&self.dependencies) {
Ok(read) => read,
Err(error) => {
errors.push(format!("the .xamlmeta of a dependency cannot be read: {error}"));
return Outcome { lines, errors };
}
};
report.read_dependencies = started.elapsed();
report.dependency_files = dependency_files.len();
report.dependency_models = models.len();
for file in &dependency_files {
rerun(&mut lines, file);
}
let dependencies: Vec<XamlMetadata> = models.iter().map(AssemblyModel::metadata).collect();
let scanned = self.export_metadata || on_models;
let mut model: Option<AssemblyModel> = None;
let mut compiled_against: Vec<AssemblyModel> = Vec::new();
if scanned {
if on_models {
compiled_against = models.clone();
}
let started = Instant::now();
match self.scan(models) {
Ok(scan) => {
report.scanned(&scan, started.elapsed());
for file in &scan.files {
rerun(&mut lines, &file.path);
}
for diagnostic in scan.diagnostics_of(Severity::Error) {
lines.push(format!("cargo::warning={diagnostic}"));
}
model = Some(scan.model);
}
Err(error) => {
errors.push(error);
return Outcome { lines, errors };
}
}
}
let assembly_name: String = match (self.assembly, &model) {
(Some(assembly), _) => assembly.name.to_string(),
(None, Some(model)) if !model.name.is_empty() => model.name.clone(),
(None, _) => self.crate_name.clone(),
};
let assembly_source_info = model.as_ref().is_some_and(|model| {
model.metadata.iter().find(|(key, _)| key == ferroui_build_scan::CREATE_SOURCE_INFO).is_some_and(|(_, value)| value.trim().eq_ignore_ascii_case("true"))
});
let mut model_host: Option<(Rc<dyn IXamlTypeSystem>, Rc<dyn IXamlTypeSystem>, ModelEmitTypes)> = None;
if on_models {
if let Some(model) = &model {
let mut own = model.clone();
own.name = assembly_name.clone();
compiled_against.push(own);
}
let system = ModelTypeSystem::new(compiled_against);
let types = ModelEmitTypes::new(system.clone(), Vec::new());
let type_system = match dependencies.is_empty() {
true => Ok(system.as_type_system()),
false => CompiledMarkupTypeSystem::new(system.as_type_system(), &dependencies).map(|wrapped| wrapped as Rc<dyn IXamlTypeSystem>),
};
match type_system {
Ok(type_system) => model_host = Some((type_system, system.as_type_system(), types)),
Err(error) => {
errors.push(format!("the compiled markup of a dependency cannot be read against the type models: {}", error.message()));
return Outcome { lines, errors };
}
}
} else if let Some(assembly) = self.assembly {
#[cfg(feature = "runtime-host")]
runtime_host::register(assembly);
#[cfg(not(feature = "runtime-host"))]
let _ = assembly;
}
for file in &self.analyzer_config_files {
rerun(&mut lines, file);
}
let diagnostics = diagnostics::Diagnostics::new(
XamlCompilerDiagnosticsFilter::new(Some(self.analyzer_config_files.clone())),
self.warnings_as_errors,
);
let xaml_directory = self.out_dir.join("xaml");
let root_uri = format!("ferres://{assembly_name}/");
let mut metadata = XamlMetadata {
name: assembly_name.clone(),
crate_name: self.crate_name.clone(),
documents: Vec::new(),
dependencies: self.dependencies.iter().map(|path| path.display().to_string()).collect(),
};
let mut modules: Vec<Module> = Vec::new();
let mut compiled_documents: Vec<String> = Vec::new();
for group in &self.groups {
if self.assembly.is_none() && !on_models {
break;
}
if modules.iter().any(|module| module.name == group.module) {
errors.push(format!("two groups have the module `{}`", group.module));
continue;
}
let documents = match &group.documents {
Some(documents) => documents.clone(),
None => {
let root = self.manifest_dir.join(&self.xaml_root);
rerun(&mut lines, &root);
let mut files = Vec::new();
if let Err(error) = collect(&self.manifest_dir, &root, &mut files) {
errors.push(error);
}
let mut documents = Vec::with_capacity(files.len());
for (rooted, path) in files {
if !path.extension().is_some_and(|extension| extension == "xaml") {
continue;
}
match fs::read_to_string(&path) {
Ok(text) => documents.push((rooted.trim_start_matches('/').to_string(), text)),
Err(error) => errors.push(format!("{}: {error}", path.display())),
}
}
documents
}
};
let borrowed: Vec<(&str, &str)> = documents.iter().map(|(name, xaml)| (name.as_str(), xaml.as_str())).collect();
let path = xaml_directory.join(format!("{}.rs", group.module));
let module_path = format!("::{}::{}", self.crate_name, group.module);
if let Some(class_document) = &group.class_document {
let Some((type_system, models, types)) = &model_host else {
errors.push(diagnostics::error(
FerroXamlDiagnosticCodes::TRANSFORM_ERROR,
class_document,
"the document of a class of the crate is compiled against the type models only (Build::type_system(TypeSystem::Model)): a build script cannot link the crate it builds",
));
continue;
};
let host = EmitterHost { type_system: type_system.clone(), types, parsers: Vec::new() };
let options = TransformOptions {
local_assembly: Some(assembly_name.clone()),
create_source_info: group.create_source_info.unwrap_or(assembly_source_info),
use_compiled_bindings_by_default: self.default_compile_bindings,
diagnostic_handler: Some(diagnostics.handler()),
design_mode: false,
};
let compiled = class_group(&host, &**models, &root_uri, &borrowed, class_document, group.constructor, &module_path, &options);
let reported_errors = diagnostics.report(&mut lines, &mut errors);
match compiled {
Ok(file) => {
for warning in &file.warnings {
diagnostics.warning(FerroXamlDiagnosticCodes::XAML_LOADER_UNREACHABLE, class_document, warning, &mut lines, &mut errors);
}
write_if_changed(&path, &file.source, &mut errors);
let class_metadata = file.metadata(&[]);
if group.exported {
compiled_documents.extend(class_metadata.documents.iter().filter_map(|document| document_path(&document.uri)));
metadata.documents.extend(class_metadata.documents);
}
}
Err(ClassGroupError::Transform(reason)) => {
if !reported_errors {
errors.push(diagnostics::error(FerroXamlDiagnosticCodes::TRANSFORM_ERROR, class_document, &reason));
}
}
Err(ClassGroupError::Emit(reasons)) => errors.extend(reasons.lines().map(|reason| {
let (document, reason) = borrowed
.iter()
.find_map(|(name, _)| reason.strip_prefix(*name).and_then(|rest| rest.strip_prefix(": ")).map(|rest| (*name, rest)))
.unwrap_or((class_document.as_str(), reason));
diagnostics::error(FerroXamlDiagnosticCodes::EMIT_ERROR, document, reason)
})),
}
modules.push(Module { name: group.module.clone(), path, exported: group.exported, file: true });
continue;
}
let file = match &model_host {
Some((type_system, _, types)) => {
let host = EmitterHost { type_system: type_system.clone(), types, parsers: Vec::new() };
let options = TransformOptions {
local_assembly: Some(assembly_name.clone()),
create_source_info: group.create_source_info.unwrap_or(assembly_source_info),
use_compiled_bindings_by_default: self.default_compile_bindings,
diagnostic_handler: Some(diagnostics.handler()),
design_mode: false,
};
generate_file_with(&host, &assembly_name, &root_uri, &borrowed, &options)
}
#[cfg(feature = "runtime-host")]
None => {
let Some(assembly) = self.assembly else { break };
runtime_host::generate_file(assembly, &root_uri, &borrowed, group.create_source_info, self.default_compile_bindings, &dependencies)
}
#[cfg(not(feature = "runtime-host"))]
None => break,
};
let reported_errors = diagnostics.report(&mut lines, &mut errors);
let mut group_reported = reported_errors;
for (name, reason) in &file.documents {
let Some(reason) = reason else { continue };
let reason = reason.replace(['\r', '\n'], " ");
match reason.starts_with(GROUP_NOT_TRANSFORMED) {
true if group_reported => {}
true => {
group_reported = true;
errors.push(diagnostics::error(FerroXamlDiagnosticCodes::TRANSFORM_ERROR, &format!("the group `{}`", group.module), &reason));
}
false => errors.push(diagnostics::error(FerroXamlDiagnosticCodes::EMIT_ERROR, name, &reason)),
}
}
write_if_changed(&path, &file.source, &mut errors);
write_if_changed(&xaml_directory.join(format!("{}.map.json", group.module)), &file.position_map, &mut errors);
if group.exported {
metadata.documents.extend(file.metadata(&self.crate_name, &module_path, &[]).documents);
compiled_documents.extend(documents.iter().map(|(name, _)| format!("/{name}")));
}
modules.push(Module { name: group.module.clone(), path, exported: group.exported, file: false });
}
for path in &self.checked_in_metadata {
rerun(&mut lines, path);
let read = fs::read_to_string(path).map_err(|error| error.to_string()).and_then(|text| XamlMetadata::parse(&text));
match read {
Ok(read) if read.name == assembly_name => metadata.documents.extend(read.documents),
Ok(read) => errors.push(format!(
"{}: the file describes the assembly `{}`, the crate is `{}`",
path.display(),
read.name,
assembly_name
)),
Err(error) => errors.push(format!("{}: {error}", path.display())),
}
}
let mut assets: Vec<(String, PathBuf)> = Vec::new();
for directory in &self.asset_directories {
let directory = self.manifest_dir.join(directory);
rerun(&mut lines, &directory);
if let Err(error) = collect(&self.manifest_dir, &directory, &mut assets) {
errors.push(error);
}
}
assets.retain(|(rooted, _)| !compiled_documents.contains(rooted));
assets.sort();
assets.dedup();
if !errors.is_empty() {
return Outcome { lines, errors };
}
let metadata_path = self.out_dir.join(format!("{}.xamlmeta", self.crate_name));
let metadata_text = match model {
Some(mut model) => {
model.name = metadata.name;
model.crate_name = metadata.crate_name;
model.documents = metadata.documents;
model.dependencies = metadata.dependencies;
model.to_json()
}
None => metadata.to_json(),
};
let started = Instant::now();
report.model_bytes = metadata_text.len();
match ferroui_build_scan::export::write_if_changed(&metadata_path, &metadata_text) {
Ok(written) => report.model_written = written,
Err(error) => errors.push(error),
}
report.write = started.elapsed();
if scanned {
if let Err(error) = report.write(&self.out_dir) {
errors.push(error);
}
}
if self.assembly.is_some() || !self.groups.is_empty() || !self.asset_directories.is_empty() {
let text = crate_file(&assembly_name, &modules, &self.loaders, &assets);
write_if_changed(&xaml_directory.join("mod.rs"), &text, &mut errors);
}
if !errors.is_empty() {
return Outcome { lines, errors };
}
lines.push(format!("cargo::metadata=xamlmeta={}", metadata_path.display()));
lines.push(format!("cargo::rustc-env={XAMLMETA_VARIABLE}={}", metadata_path.display()));
Outcome { lines, errors }
}
}
#[cfg(feature = "runtime-host")]
mod runtime_host {
use ferroui_base::metadata::MarkupAssembly;
use ferroui_markup_xaml::RuntimeXamlLoaderConfiguration;
use ferroui_markup_xaml_loader::rust_emitter::{GeneratedFile, XamlMetadata};
use ferroui_markup_xaml_loader::FerroRuntimeXamlLoader;
pub(super) fn register(assembly: &'static MarkupAssembly) {
ferroui_markup_xaml::register_types();
MarkupAssembly::register(assembly);
ferroui_base::platform::register_assets(assembly.name, &[]);
FerroRuntimeXamlLoader::register();
}
pub(super) fn generate_file(
assembly: &'static MarkupAssembly,
root_uri: &str,
documents: &[(&str, &str)],
create_source_info: Option<bool>,
default_compile_bindings: bool,
dependencies: &[XamlMetadata],
) -> GeneratedFile {
let mut configuration = RuntimeXamlLoaderConfiguration::new();
configuration.local_assembly = Some(assembly);
configuration.use_compiled_bindings_by_default = default_compile_bindings;
if let Some(create_source_info) = create_source_info {
configuration.set_create_source_info(create_source_info);
}
ferroui_markup_xaml_loader::rust_emitter::generate_file(assembly.name, root_uri, documents, &configuration, dependencies)
}
}
struct Module {
name: String,
path: PathBuf,
exported: bool,
file: bool,
}
fn document_path(uri: &str) -> Option<String> {
let rest = uri.split_once("://")?.1;
rest.find('/').map(|separator| rest[separator..].to_string())
}
#[allow(clippy::too_many_arguments)]
fn class_group(
host: &EmitterHost<'_>,
models: &dyn IXamlTypeSystem,
root_uri: &str,
documents: &[(&str, &str)],
class_document: &str,
constructor: Option<ClassConstructor>,
module_path: &str,
options: &TransformOptions,
) -> Result<ferroui_markup_xaml_loader::rust_emitter::ClassFile, ClassGroupError> {
let group = class_document_group(root_uri, documents, class_document).map_err(ClassGroupError::Transform)?;
let class_name = class_of_document(&group[0].1)
.map_err(ClassGroupError::Transform)?
.ok_or_else(|| ClassGroupError::Transform("the document names no class (`x:Class`)".to_string()))?;
let class = models.find_type(&class_name).ok_or_else(|| {
ClassGroupError::Transform(format!(
"the type models have no type `{class_name}`, which the document names with `x:Class`: the class is not declared in the sources of the crate, or its declaration is not read"
))
})?;
generate_class_file_with(host, &ClassGroup { class, documents: &group, constructor, module_path }, options).map_err(|reasons| {
match reasons.strip_prefix(CLASS_GROUP_NOT_TRANSFORMED) {
Some(_) => ClassGroupError::Transform(reasons),
None => ClassGroupError::Emit(reasons),
}
})
}
enum ClassGroupError {
Transform(String),
Emit(String),
}
pub fn compile_xaml(assembly: &'static MarkupAssembly) {
Build::from_env().assembly(assembly).compile_xaml().run();
}
pub fn embed_assets(assembly: &'static MarkupAssembly, directories: &[&str]) {
Build::from_env().assembly(assembly).embed_assets(directories).run();
}
pub fn export_metadata() {
Build::from_env().export_metadata().run();
}
fn collect(root: &Path, directory: &Path, found: &mut Vec<(String, PathBuf)>) -> Result<(), String> {
let entries = fs::read_dir(directory).map_err(|error| format!("{}: {error}", directory.display()))?;
let mut paths = Vec::new();
for entry in entries {
paths.push(entry.map_err(|error| format!("{}: {error}", directory.display()))?.path());
}
paths.sort();
for path in paths {
let name = path.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
if path.is_dir() {
if !name.starts_with('.') && !(directory == root && name == "target") {
collect(root, &path, found)?;
}
continue;
}
let relative = path.strip_prefix(root).map_err(|_| format!("{} is not below {}", path.display(), root.display()))?;
let parts: Vec<String> = relative.components().map(|part| part.as_os_str().to_string_lossy().into_owned()).collect();
found.push((format!("/{}", parts.join("/")), path));
}
Ok(())
}
fn write_if_changed(path: &Path, text: &str, errors: &mut Vec<String>) {
if let Err(error) = ferroui_build_scan::export::write_if_changed(path, text) {
errors.push(error);
}
}
fn crate_file(assembly_name: &str, modules: &[Module], loaders: &[String], assets: &[(String, PathBuf)]) -> String {
let mut text = String::new();
text.push_str("// @generated by ferroui-build (Build::run). Do not edit.\n");
for Module { name: module, path, file, .. } in modules {
text.push('\n');
text.push_str(&format!("/// The compiled documents of the group `{module}`.\n"));
text.push_str("#[doc(hidden)]\n");
text.push_str("#[rustfmt::skip]\n");
if *file {
text.push_str(&format!("#[path = {}]\n", rust_string_literal(&path.display().to_string())));
text.push_str(&format!("pub mod {module};\n"));
continue;
}
text.push_str("#[allow(dead_code)]\n");
text.push_str(&format!("pub mod {module} {{\n"));
text.push_str(&format!(" include!({});\n", rust_string_literal(&path.display().to_string())));
text.push_str("}\n");
}
text.push('\n');
text.push_str("/// The compiled markup and the embedded assets of the crate.\n");
text.push_str("#[doc(hidden)]\n");
text.push_str("#[rustfmt::skip]\n");
text.push_str("pub mod compiled_markup {\n");
text.push_str(" /// The assembly of the crate.\n");
text.push_str(&format!(" pub const ASSEMBLY_NAME: &str = {};\n", rust_string_literal(assembly_name)));
text.push('\n');
text.push_str(" /// The embedded assets of the crate, by rooted path.\n");
text.push_str(" pub static ASSETS: &[(&str, &[u8])] = &[\n");
for (rooted, path) in assets {
text.push_str(&format!(
" ({}, include_bytes!({})),\n",
rust_string_literal(rooted),
rust_string_literal(&path.display().to_string())
));
}
text.push_str(" ];\n");
text.push('\n');
text.push_str(" /// The loader of the compiled markup of the crate: the object of the document with the URI\n");
text.push_str(" /// `uri`, from the first table that has it; `Ok(None)` if none has.\n");
text.push_str(" #[allow(unused_variables)]\n");
text.push_str(" pub fn try_load(\n");
text.push_str(" service_provider: ::core::option::Option<&::std::rc::Rc<dyn ::ferroui_base::metadata::IServiceProvider>>,\n");
text.push_str(" uri: &str,\n");
text.push_str(" ) -> ::core::result::Result<::core::option::Option<::ferroui_base::BoxedValue>, ::ferroui_markup_xaml::XamlLoadException> {\n");
let tables = loaders
.iter()
.cloned()
.chain(modules.iter().filter(|module| module.exported).map(|module| format!("super::{}::try_load", module.name)));
for table in tables {
text.push_str(&format!(" if let ::core::option::Option::Some(loaded) = {table}(service_provider, uri)? {{\n"));
text.push_str(" return ::core::result::Result::Ok(::core::option::Option::Some(loaded));\n");
text.push_str(" }\n");
}
text.push_str(" ::core::result::Result::Ok(::core::option::Option::None)\n");
text.push_str(" }\n");
text.push('\n');
text.push_str(" /// Registers the assets and the compiled markup of the crate. The assembly is made known\n");
text.push_str(" /// to the asset loader even without assets: a load by URI asks it for the assembly of the\n");
text.push_str(" /// document.\n");
text.push_str(" pub fn register() {\n");
text.push_str(" ::ferroui_base::platform::register_assets(ASSEMBLY_NAME, ASSETS);\n");
text.push_str(" ::ferroui_markup_xaml::FerroXamlLoader::register_compiled_xaml(ASSEMBLY_NAME, try_load);\n");
text.push_str(" }\n");
text.push_str("}\n");
text
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crate_file_declares_the_modules_and_the_loader_table() {
let module = |name: &str, exported: bool, file: bool| Module { name: name.to_string(), path: PathBuf::from(format!("/out/xaml/{name}.rs")), exported, file };
let modules = vec![module("compiled_xaml", true, false), module("compiled_xaml_source_info", false, false), module("compiled_main_window", true, true)];
let assets = vec![("/Assets/a.png".to_string(), PathBuf::from("/crate/Assets/a.png"))];
let text = crate_file("Tests", &modules, &["crate::compiled_main::try_load".to_string()], &assets);
assert!(text.contains("pub mod compiled_xaml {\n include!(\"/out/xaml/compiled_xaml.rs\");\n}\n"), "{text}");
assert!(text.contains("pub mod compiled_xaml_source_info {\n"), "{text}");
assert!(text.contains(" pub const ASSEMBLY_NAME: &str = \"Tests\";\n"), "{text}");
assert!(text.contains(" (\"/Assets/a.png\", include_bytes!(\"/crate/Assets/a.png\")),\n"), "{text}");
let checked_in = text.find("= crate::compiled_main::try_load(service_provider, uri)?").expect("the checked-in table");
let group = text.find("= super::compiled_xaml::try_load(service_provider, uri)?").expect("the table of the group");
assert!(checked_in < group, "{text}");
assert!(!text.contains("super::compiled_xaml_source_info::try_load"), "{text}");
assert!(text.contains("#[path = \"/out/xaml/compiled_main_window.rs\"]\npub mod compiled_main_window;\n"), "{text}");
assert!(text.contains("= super::compiled_main_window::try_load(service_provider, uri)?"), "{text}");
}
#[test]
fn export_metadata_writes_the_model_and_reads_the_models_of_the_dependencies() {
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).with_file_name("FerroUI.Build.Scan").join("tests").join("fixtures");
let out = env::temp_dir().join(format!("ferroui-build-export-{}", std::process::id()));
let first = Build::new(fixtures.join("scanner"), out.join("fixture"), "fixture", Vec::new()).source_root("lib.rs").export_metadata().execute();
assert_eq!(first.errors, Vec::<String>::new());
let fixture_file = out.join("fixture").join("fixture.xamlmeta");
assert!(first.lines.contains(&format!("cargo::metadata=xamlmeta={}", fixture_file.display())), "{:?}", first.lines);
let border = fixtures.join("scanner").join("controls").join("border.rs");
assert!(first.lines.contains(&format!("cargo::rerun-if-changed={}", border.display())), "{:?}", first.lines);
assert!(first.lines.iter().any(|line| line.starts_with("cargo::warning=") && line.contains("error FRN9010")), "{:?}", first.lines);
let text = fs::read_to_string(&fixture_file).expect("the .xamlmeta of the fixture");
let model = AssemblyModel::parse(&text).expect("a file of format 2");
assert_eq!((model.name.as_str(), model.crate_name.as_str()), ("Fixture", "fixture"));
assert!(model.find_type("Fixture.Controls.Border").is_some() && !model.exports.is_empty());
assert!(model.documents.is_empty() && model.dependencies.is_empty());
assert_eq!(XamlMetadata::parse(&text).map(|metadata| metadata.name), Ok("Fixture".to_string()));
assert!(!out.join("fixture").join("xaml").join("mod.rs").exists(), "a crate that only exports its model includes nothing");
let second = Build::new(fixtures.join("dependent"), out.join("dependent"), "dependent", vec![fixture_file.clone()]).export_metadata().execute();
assert_eq!(second.errors, Vec::<String>::new());
assert!(second.lines.contains(&format!("cargo::rerun-if-changed={}", fixture_file.display())), "{:?}", second.lines);
let text = fs::read_to_string(out.join("dependent").join("dependent.xamlmeta")).expect("the .xamlmeta of the dependent crate");
let model = AssemblyModel::parse(&text).expect("a file of format 2");
assert_eq!((model.name.as_str(), model.crate_name.as_str()), ("dependent", "dependent"));
assert_eq!(model.dependencies, [fixture_file.display().to_string()]);
let card = model.find_type("Card").expect("the class of the dependent crate");
assert_eq!(card.registered[0].name.as_deref(), Some("Background"));
assert_eq!(card.base.as_ref().map(|base| base.text.as_str()), Some("::fixture::controls::border::Border"));
let (models, files) = ModelSet::read(&[out.join("dependent").join("dependent.xamlmeta")]).expect("the two files");
assert_eq!(models.iter().map(|model| model.name.as_str()).collect::<Vec<_>>(), ["dependent", "Fixture"]);
assert_eq!(files.len(), 2);
let missing = Build::new(fixtures.join("dependent"), out.join("missing"), "dependent", Vec::new()).source_root("main.rs").export_metadata().execute();
assert_eq!(missing.errors.len(), 1, "{:?}", missing.errors);
assert!(missing.errors[0].ends_with("main.rs, is not a file"), "{:?}", missing.errors);
let _ = fs::remove_dir_all(&out);
}
#[test]
fn texts_the_scanner_states_are_the_ones_of_the_base_crate() {
assert_eq!(ferroui_build_scan::FERRO_XML_NAMESPACE, ferroui_base::metadata::FERRO_XML_NAMESPACE);
assert_eq!(ferroui_build_scan::CREATE_SOURCE_INFO, MarkupAssembly::CREATE_SOURCE_INFO);
assert_eq!(ferroui_build_scan::USE_COMPILED_BINDINGS_BY_DEFAULT, MarkupAssembly::USE_COMPILED_BINDINGS_BY_DEFAULT);
}
#[test]
fn class_document_is_compiled_against_the_models_only() {
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).with_file_name("FerroUI.Build.Scan").join("tests").join("fixtures");
let out = env::temp_dir().join(format!("ferroui-build-class-{}", std::process::id()));
let document = "<Border xmlns='https://github.com/ferroui' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' x:Class='Fixture.Missing'/>";
let group = || XamlGroup::new("compiled_border").documents(&[("Border.xaml", document)]).class_document("Border.xaml");
let build = |name: &str| Build::new(fixtures.join("scanner"), out.join(name), "fixture", Vec::new()).source_root("lib.rs");
let on_models = build("models").type_system(TypeSystem::Model).compile_group(group()).execute();
assert_eq!(on_models.errors.len(), 1, "{:?}", on_models.errors);
assert!(
on_models.errors[0].starts_with("Border.xaml: error FRN2000: the type models have no type `Fixture.Missing`, which the document names with `x:Class`"),
"{:?}",
on_models.errors
);
let border = fixtures.join("scanner").join("controls").join("border.rs");
assert!(on_models.lines.contains(&format!("cargo::rerun-if-changed={}", border.display())), "{:?}", on_models.lines);
static ASSEMBLY: MarkupAssembly =
MarkupAssembly { name: "Fixture", crate_name: "fixture", xmlns_definitions: &[], xmlns_prefixes: &[], metadata: &[] };
let at_run_time = build("runtime").assembly(&ASSEMBLY).compile_group(group()).execute();
assert_eq!(at_run_time.errors.len(), 1, "{:?}", at_run_time.errors);
let expected = match cfg!(feature = "runtime-host") {
true => "Border.xaml: error FRN2000: the document of a class of the crate is compiled against the type models only",
false => "the documents are compiled against the type models (Build::type_system(TypeSystem::Model)): the run-time type system is the feature `runtime-host`",
};
assert!(at_run_time.errors[0].starts_with(expected), "{:?}", at_run_time.errors);
let _ = fs::remove_dir_all(&out);
}
#[test]
fn build_without_an_assembly_is_an_error() {
let outcome = Build::new(PathBuf::from("/crate"), PathBuf::from("/out"), "my-crate", Vec::new()).compile_xaml().execute();
assert_eq!(outcome.errors, ["the assembly of the crate is not stated (Build::assembly)"]);
assert_eq!(outcome.lines, ["cargo::rerun-if-changed=/crate/build.rs", "cargo::rerun-if-changed=/crate/Cargo.toml"]);
}
}