1#![forbid(unsafe_code)]
157
158pub mod deterministic_id_generator;
159mod diagnostics;
160pub use ferroui_build_scan::{call_forms, export, model, model_set, scanner};
164pub mod type_system;
165pub mod xaml_compiler_diagnostics_filter;
166
167use std::env;
168use std::fs;
169use std::path::{Path, PathBuf};
170use std::rc::Rc;
171use std::time::Instant;
172
173use ferroui_base::metadata::MarkupAssembly;
174use xamlx::type_system::IXamlTypeSystem;
175
176use crate::export::ScanReport;
177use crate::model::AssemblyModel;
178use crate::model_set::ModelSet;
179use crate::scanner::{scan_crate, Scan, ScanOptions, Severity};
180use crate::type_system::{ModelEmitTypes, ModelTypeSystem};
181pub use ferroui_markup_xaml_loader::rust_emitter::ClassConstructor;
182use crate::xaml_compiler_diagnostics_filter::XamlCompilerDiagnosticsFilter;
183use ferroui_markup_xaml_loader::compiler_extensions::FerroXamlDiagnosticCodes;
184use ferroui_markup_xaml_loader::rust_emitter::{
185 class_document_group, class_of_document, generate_class_file_with, generate_file_with, rust_string_literal, ClassGroup, CompiledMarkupTypeSystem,
186 EmitterHost, TransformOptions, XamlMetadata, CLASS_GROUP_NOT_TRANSFORMED, GROUP_NOT_TRANSFORMED,
187};
188
189pub const DEFAULT_MODULE: &str = "compiled_xaml";
191
192pub const XAMLMETA_VARIABLE: &str = "FERROUI_XAMLMETA";
195
196pub struct XamlGroup {
200 module: String,
201 documents: Option<Vec<(String, String)>>,
202 create_source_info: Option<bool>,
203 exported: bool,
204 class_document: Option<String>,
205 constructor: Option<ClassConstructor>,
206}
207
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
210pub enum TypeSystem {
211 #[default]
218 Runtime,
219 Model,
224}
225
226impl XamlGroup {
227 pub fn new(module: &str) -> Self {
231 Self { module: module.to_string(), documents: None, create_source_info: None, exported: true, class_document: None, constructor: None }
232 }
233
234 pub fn class_document(mut self, name: &str) -> Self {
245 self.class_document = Some(name.to_string());
246 self
247 }
248
249 pub fn constructor(mut self, constructor: ClassConstructor) -> Self {
253 self.constructor = Some(constructor);
254 self
255 }
256
257 pub fn documents(mut self, documents: &[(&str, &str)]) -> Self {
261 self.documents = Some(documents.iter().map(|(name, xaml)| (name.to_string(), xaml.to_string())).collect());
262 self
263 }
264
265 pub fn create_source_info(mut self, value: bool) -> Self {
268 self.create_source_info = Some(value);
269 self
270 }
271
272 pub fn internal(mut self) -> Self {
276 self.exported = false;
277 self
278 }
279}
280
281pub use ferroui_build_scan::export::Outcome;
282
283pub struct Build {
285 manifest_dir: PathBuf,
286 out_dir: PathBuf,
287 crate_name: String,
288 assembly: Option<&'static MarkupAssembly>,
289 xaml_root: PathBuf,
290 groups: Vec<XamlGroup>,
291 asset_directories: Vec<String>,
292 loaders: Vec<String>,
293 checked_in_metadata: Vec<PathBuf>,
294 inputs: Vec<PathBuf>,
295 dependencies: Vec<PathBuf>,
296 export_metadata: bool,
297 source_root: Option<PathBuf>,
298 type_system: TypeSystem,
299 default_compile_bindings: bool,
300 analyzer_config_files: Vec<PathBuf>,
301 warnings_as_errors: bool,
302}
303
304impl Build {
305 pub fn from_env() -> Self {
312 let variable = |name: &str| env::var_os(name).unwrap_or_else(|| panic!("{name} is not set: not a build script"));
313 Self::new(
314 PathBuf::from(variable("CARGO_MANIFEST_DIR")),
315 PathBuf::from(variable("OUT_DIR")),
316 &variable("CARGO_PKG_NAME").to_string_lossy(),
317 ferroui_build_scan::export::dependencies_from_env(),
318 )
319 }
320
321 pub fn new(manifest_dir: PathBuf, out_dir: PathBuf, package_name: &str, dependencies: Vec<PathBuf>) -> Self {
324 Self {
325 manifest_dir,
326 out_dir,
327 crate_name: package_name.replace('-', "_"),
328 assembly: None,
329 xaml_root: PathBuf::new(),
330 groups: Vec::new(),
331 asset_directories: Vec::new(),
332 loaders: Vec::new(),
333 checked_in_metadata: Vec::new(),
334 inputs: Vec::new(),
335 dependencies,
336 export_metadata: false,
337 source_root: None,
338 type_system: TypeSystem::Runtime,
339 default_compile_bindings: false,
340 analyzer_config_files: Vec::new(),
341 warnings_as_errors: false,
342 }
343 }
344
345 pub fn type_system(mut self, type_system: TypeSystem) -> Self {
357 self.type_system = type_system;
358 self
359 }
360
361 pub fn default_compile_bindings(mut self, value: bool) -> Self {
365 self.default_compile_bindings = value;
366 self
367 }
368
369 pub fn analyzer_config_files(mut self, files: &[&str]) -> Self {
376 self.analyzer_config_files.extend(files.iter().map(|file| self.manifest_dir.join(file)));
377 self
378 }
379
380 pub fn warnings_as_errors(mut self, value: bool) -> Self {
385 self.warnings_as_errors = value;
386 self
387 }
388
389 pub fn export_metadata(mut self) -> Self {
401 self.export_metadata = true;
402 self
403 }
404
405 pub fn source_root(mut self, file: &str) -> Self {
409 self.source_root = Some(PathBuf::from(file));
410 self
411 }
412
413 fn scan(&self, dependencies: Vec<AssemblyModel>) -> Result<Scan, String> {
415 let root = ferroui_build_scan::export::source_root(&self.manifest_dir, self.source_root.as_deref())?;
416 let mut options = ScanOptions::new(&self.crate_name, root).with_dependencies(dependencies);
417 options.assembly_name = self.assembly.map(|assembly| assembly.name.to_string());
418 Ok(scan_crate(&options))
419 }
420
421 pub fn assembly(mut self, assembly: &'static MarkupAssembly) -> Self {
425 self.assembly = Some(assembly);
426 self
427 }
428
429 pub fn xaml_root(mut self, directory: &str) -> Self {
432 self.xaml_root = PathBuf::from(directory);
433 self
434 }
435
436 pub fn input(mut self, path: &str) -> Self {
439 self.inputs.push(self.manifest_dir.join(path));
440 self
441 }
442
443 pub fn embed_assets(mut self, directories: &[&str]) -> Self {
447 self.asset_directories.extend(directories.iter().map(|directory| directory.to_string()));
448 self
449 }
450
451 pub fn compile_xaml(self) -> Self {
454 self.compile_group(XamlGroup::new(DEFAULT_MODULE))
455 }
456
457 pub fn compile_group(mut self, group: XamlGroup) -> Self {
459 self.groups.push(group);
460 self
461 }
462
463 pub fn loader(mut self, path: &str) -> Self {
467 self.loaders.push(path.to_string());
468 self
469 }
470
471 pub fn checked_in_metadata(mut self, path: &str) -> Self {
474 self.checked_in_metadata.push(self.manifest_dir.join(path));
475 self
476 }
477
478 pub fn run(self) {
481 let outcome = self.execute();
482 for line in &outcome.lines {
483 println!("{line}");
484 }
485 if !outcome.errors.is_empty() {
486 for error in &outcome.errors {
487 eprintln!("error: {error}");
488 println!("cargo::error={error}");
489 }
490 std::process::exit(1);
491 }
492 }
493
494 pub fn execute(self) -> Outcome {
496 let mut lines = Vec::new();
497 let mut errors = Vec::new();
498 let rerun = |lines: &mut Vec<String>, path: &Path| lines.push(format!("cargo::rerun-if-changed={}", path.display()));
499 rerun(&mut lines, &self.manifest_dir.join("build.rs"));
500 rerun(&mut lines, &self.manifest_dir.join("Cargo.toml"));
501 for input in &self.inputs {
502 rerun(&mut lines, input);
503 }
504 let on_models = self.type_system == TypeSystem::Model;
505 if self.assembly.is_none() && !on_models && (!self.export_metadata || !self.groups.is_empty()) {
508 errors.push("the assembly of the crate is not stated (Build::assembly)".to_string());
509 return Outcome { lines, errors };
510 }
511 if !on_models && !self.groups.is_empty() && !cfg!(feature = "runtime-host") {
513 errors.push(
514 "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"
515 .to_string(),
516 );
517 return Outcome { lines, errors };
518 }
519
520 let mut report = ScanReport { crate_name: self.crate_name.clone(), ..ScanReport::default() };
522 let started = Instant::now();
523 let (models, dependency_files) = match ModelSet::read(&self.dependencies) {
524 Ok(read) => read,
525 Err(error) => {
526 errors.push(format!("the .xamlmeta of a dependency cannot be read: {error}"));
527 return Outcome { lines, errors };
528 }
529 };
530 report.read_dependencies = started.elapsed();
531 report.dependency_files = dependency_files.len();
532 report.dependency_models = models.len();
533 for file in &dependency_files {
534 rerun(&mut lines, file);
535 }
536 let dependencies: Vec<XamlMetadata> = models.iter().map(AssemblyModel::metadata).collect();
537
538 let scanned = self.export_metadata || on_models;
540 let mut model: Option<AssemblyModel> = None;
541 let mut compiled_against: Vec<AssemblyModel> = Vec::new();
544 if scanned {
545 if on_models {
546 compiled_against = models.clone();
547 }
548 let started = Instant::now();
549 match self.scan(models) {
550 Ok(scan) => {
551 report.scanned(&scan, started.elapsed());
552 for file in &scan.files {
553 rerun(&mut lines, &file.path);
554 }
555 for diagnostic in scan.diagnostics_of(Severity::Error) {
556 lines.push(format!("cargo::warning={diagnostic}"));
557 }
558 model = Some(scan.model);
559 }
560 Err(error) => {
561 errors.push(error);
562 return Outcome { lines, errors };
563 }
564 }
565 }
566 let assembly_name: String = match (self.assembly, &model) {
567 (Some(assembly), _) => assembly.name.to_string(),
568 (None, Some(model)) if !model.name.is_empty() => model.name.clone(),
569 (None, _) => self.crate_name.clone(),
572 };
573 let assembly_source_info = model.as_ref().is_some_and(|model| {
576 model.metadata.iter().find(|(key, _)| key == ferroui_build_scan::CREATE_SOURCE_INFO).is_some_and(|(_, value)| value.trim().eq_ignore_ascii_case("true"))
577 });
578
579 let mut model_host: Option<(Rc<dyn IXamlTypeSystem>, Rc<dyn IXamlTypeSystem>, ModelEmitTypes)> = None;
581 if on_models {
582 if let Some(model) = &model {
583 let mut own = model.clone();
584 own.name = assembly_name.clone();
585 compiled_against.push(own);
586 }
587 let system = ModelTypeSystem::new(compiled_against);
588 let types = ModelEmitTypes::new(system.clone(), Vec::new());
592 let type_system = match dependencies.is_empty() {
595 true => Ok(system.as_type_system()),
596 false => CompiledMarkupTypeSystem::new(system.as_type_system(), &dependencies).map(|wrapped| wrapped as Rc<dyn IXamlTypeSystem>),
597 };
598 match type_system {
599 Ok(type_system) => model_host = Some((type_system, system.as_type_system(), types)),
600 Err(error) => {
601 errors.push(format!("the compiled markup of a dependency cannot be read against the type models: {}", error.message()));
602 return Outcome { lines, errors };
603 }
604 }
605 } else if let Some(assembly) = self.assembly {
606 #[cfg(feature = "runtime-host")]
607 runtime_host::register(assembly);
608 #[cfg(not(feature = "runtime-host"))]
609 let _ = assembly;
610 }
611
612 for file in &self.analyzer_config_files {
614 rerun(&mut lines, file);
615 }
616 let diagnostics = diagnostics::Diagnostics::new(
617 XamlCompilerDiagnosticsFilter::new(Some(self.analyzer_config_files.clone())),
618 self.warnings_as_errors,
619 );
620 let xaml_directory = self.out_dir.join("xaml");
621 let root_uri = format!("ferres://{assembly_name}/");
622 let mut metadata = XamlMetadata {
623 name: assembly_name.clone(),
624 crate_name: self.crate_name.clone(),
625 documents: Vec::new(),
626 dependencies: self.dependencies.iter().map(|path| path.display().to_string()).collect(),
627 };
628 let mut modules: Vec<Module> = Vec::new();
629 let mut compiled_documents: Vec<String> = Vec::new();
630 for group in &self.groups {
631 if self.assembly.is_none() && !on_models {
632 break;
633 }
634 if modules.iter().any(|module| module.name == group.module) {
635 errors.push(format!("two groups have the module `{}`", group.module));
636 continue;
637 }
638 let documents = match &group.documents {
639 Some(documents) => documents.clone(),
640 None => {
641 let root = self.manifest_dir.join(&self.xaml_root);
642 rerun(&mut lines, &root);
643 let mut files = Vec::new();
644 if let Err(error) = collect(&self.manifest_dir, &root, &mut files) {
645 errors.push(error);
646 }
647 let mut documents = Vec::with_capacity(files.len());
648 for (rooted, path) in files {
649 if !path.extension().is_some_and(|extension| extension == "xaml") {
650 continue;
651 }
652 match fs::read_to_string(&path) {
653 Ok(text) => documents.push((rooted.trim_start_matches('/').to_string(), text)),
654 Err(error) => errors.push(format!("{}: {error}", path.display())),
655 }
656 }
657 documents
658 }
659 };
660 let borrowed: Vec<(&str, &str)> = documents.iter().map(|(name, xaml)| (name.as_str(), xaml.as_str())).collect();
661 let path = xaml_directory.join(format!("{}.rs", group.module));
662 let module_path = format!("::{}::{}", self.crate_name, group.module);
663
664 if let Some(class_document) = &group.class_document {
666 let Some((type_system, models, types)) = &model_host else {
667 errors.push(diagnostics::error(
668 FerroXamlDiagnosticCodes::TRANSFORM_ERROR,
669 class_document,
670 "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",
671 ));
672 continue;
673 };
674 let host = EmitterHost { type_system: type_system.clone(), types, parsers: Vec::new() };
675 let options = TransformOptions {
676 local_assembly: Some(assembly_name.clone()),
677 create_source_info: group.create_source_info.unwrap_or(assembly_source_info),
678 use_compiled_bindings_by_default: self.default_compile_bindings,
679 diagnostic_handler: Some(diagnostics.handler()),
680 design_mode: false,
681 };
682 let compiled = class_group(&host, &**models, &root_uri, &borrowed, class_document, group.constructor, &module_path, &options);
683 let reported_errors = diagnostics.report(&mut lines, &mut errors);
684 match compiled {
685 Ok(file) => {
686 for warning in &file.warnings {
688 diagnostics.warning(FerroXamlDiagnosticCodes::XAML_LOADER_UNREACHABLE, class_document, warning, &mut lines, &mut errors);
689 }
690 write_if_changed(&path, &file.source, &mut errors);
691 let class_metadata = file.metadata(&[]);
692 if group.exported {
693 compiled_documents.extend(class_metadata.documents.iter().filter_map(|document| document_path(&document.uri)));
694 metadata.documents.extend(class_metadata.documents);
695 }
696 }
697 Err(ClassGroupError::Transform(reason)) => {
701 if !reported_errors {
702 errors.push(diagnostics::error(FerroXamlDiagnosticCodes::TRANSFORM_ERROR, class_document, &reason));
703 }
704 }
705 Err(ClassGroupError::Emit(reasons)) => errors.extend(reasons.lines().map(|reason| {
706 let (document, reason) = borrowed
708 .iter()
709 .find_map(|(name, _)| reason.strip_prefix(*name).and_then(|rest| rest.strip_prefix(": ")).map(|rest| (*name, rest)))
710 .unwrap_or((class_document.as_str(), reason));
711 diagnostics::error(FerroXamlDiagnosticCodes::EMIT_ERROR, document, reason)
712 })),
713 }
714 modules.push(Module { name: group.module.clone(), path, exported: group.exported, file: true });
715 continue;
716 }
717
718 let file = match &model_host {
719 Some((type_system, _, types)) => {
720 let host = EmitterHost { type_system: type_system.clone(), types, parsers: Vec::new() };
721 let options = TransformOptions {
722 local_assembly: Some(assembly_name.clone()),
723 create_source_info: group.create_source_info.unwrap_or(assembly_source_info),
724 use_compiled_bindings_by_default: self.default_compile_bindings,
725 diagnostic_handler: Some(diagnostics.handler()),
726 design_mode: false,
727 };
728 generate_file_with(&host, &assembly_name, &root_uri, &borrowed, &options)
729 }
730 #[cfg(feature = "runtime-host")]
731 None => {
732 let Some(assembly) = self.assembly else { break };
733 runtime_host::generate_file(assembly, &root_uri, &borrowed, group.create_source_info, self.default_compile_bindings, &dependencies)
734 }
735 #[cfg(not(feature = "runtime-host"))]
738 None => break,
739 };
740 let reported_errors = diagnostics.report(&mut lines, &mut errors);
741 let mut group_reported = reported_errors;
745 for (name, reason) in &file.documents {
746 let Some(reason) = reason else { continue };
747 let reason = reason.replace(['\r', '\n'], " ");
748 match reason.starts_with(GROUP_NOT_TRANSFORMED) {
749 true if group_reported => {}
750 true => {
751 group_reported = true;
752 errors.push(diagnostics::error(FerroXamlDiagnosticCodes::TRANSFORM_ERROR, &format!("the group `{}`", group.module), &reason));
753 }
754 false => errors.push(diagnostics::error(FerroXamlDiagnosticCodes::EMIT_ERROR, name, &reason)),
755 }
756 }
757 write_if_changed(&path, &file.source, &mut errors);
758 write_if_changed(&xaml_directory.join(format!("{}.map.json", group.module)), &file.position_map, &mut errors);
759 if group.exported {
760 metadata.documents.extend(file.metadata(&self.crate_name, &module_path, &[]).documents);
761 compiled_documents.extend(documents.iter().map(|(name, _)| format!("/{name}")));
762 }
763 modules.push(Module { name: group.module.clone(), path, exported: group.exported, file: false });
764 }
765 for path in &self.checked_in_metadata {
766 rerun(&mut lines, path);
767 let read = fs::read_to_string(path).map_err(|error| error.to_string()).and_then(|text| XamlMetadata::parse(&text));
768 match read {
769 Ok(read) if read.name == assembly_name => metadata.documents.extend(read.documents),
770 Ok(read) => errors.push(format!(
771 "{}: the file describes the assembly `{}`, the crate is `{}`",
772 path.display(),
773 read.name,
774 assembly_name
775 )),
776 Err(error) => errors.push(format!("{}: {error}", path.display())),
777 }
778 }
779
780 let mut assets: Vec<(String, PathBuf)> = Vec::new();
782 for directory in &self.asset_directories {
783 let directory = self.manifest_dir.join(directory);
784 rerun(&mut lines, &directory);
785 if let Err(error) = collect(&self.manifest_dir, &directory, &mut assets) {
786 errors.push(error);
787 }
788 }
789 assets.retain(|(rooted, _)| !compiled_documents.contains(rooted));
790 assets.sort();
791 assets.dedup();
792
793 if !errors.is_empty() {
794 return Outcome { lines, errors };
795 }
796
797 let metadata_path = self.out_dir.join(format!("{}.xamlmeta", self.crate_name));
800 let metadata_text = match model {
801 Some(mut model) => {
802 model.name = metadata.name;
803 model.crate_name = metadata.crate_name;
804 model.documents = metadata.documents;
805 model.dependencies = metadata.dependencies;
806 model.to_json()
807 }
808 None => metadata.to_json(),
809 };
810 let started = Instant::now();
811 report.model_bytes = metadata_text.len();
812 match ferroui_build_scan::export::write_if_changed(&metadata_path, &metadata_text) {
813 Ok(written) => report.model_written = written,
814 Err(error) => errors.push(error),
815 }
816 report.write = started.elapsed();
817 if scanned {
819 if let Err(error) = report.write(&self.out_dir) {
820 errors.push(error);
821 }
822 }
823 if self.assembly.is_some() || !self.groups.is_empty() || !self.asset_directories.is_empty() {
825 let text = crate_file(&assembly_name, &modules, &self.loaders, &assets);
826 write_if_changed(&xaml_directory.join("mod.rs"), &text, &mut errors);
827 }
828 if !errors.is_empty() {
829 return Outcome { lines, errors };
830 }
831
832 lines.push(format!("cargo::metadata=xamlmeta={}", metadata_path.display()));
834 lines.push(format!("cargo::rustc-env={XAMLMETA_VARIABLE}={}", metadata_path.display()));
835 Outcome { lines, errors }
836 }
837}
838
839#[cfg(feature = "runtime-host")]
843mod runtime_host {
844 use ferroui_base::metadata::MarkupAssembly;
845 use ferroui_markup_xaml::RuntimeXamlLoaderConfiguration;
846 use ferroui_markup_xaml_loader::rust_emitter::{GeneratedFile, XamlMetadata};
847 use ferroui_markup_xaml_loader::FerroRuntimeXamlLoader;
848
849 pub(super) fn register(assembly: &'static MarkupAssembly) {
851 ferroui_markup_xaml::register_types();
854 MarkupAssembly::register(assembly);
855 ferroui_base::platform::register_assets(assembly.name, &[]);
858 FerroRuntimeXamlLoader::register();
859 }
860
861 pub(super) fn generate_file(
863 assembly: &'static MarkupAssembly,
864 root_uri: &str,
865 documents: &[(&str, &str)],
866 create_source_info: Option<bool>,
867 default_compile_bindings: bool,
868 dependencies: &[XamlMetadata],
869 ) -> GeneratedFile {
870 let mut configuration = RuntimeXamlLoaderConfiguration::new();
871 configuration.local_assembly = Some(assembly);
872 configuration.use_compiled_bindings_by_default = default_compile_bindings;
873 if let Some(create_source_info) = create_source_info {
874 configuration.set_create_source_info(create_source_info);
875 }
876 ferroui_markup_xaml_loader::rust_emitter::generate_file(assembly.name, root_uri, documents, &configuration, dependencies)
877 }
878}
879
880struct Module {
882 name: String,
883 path: PathBuf,
885 exported: bool,
887 file: bool,
890}
891
892fn document_path(uri: &str) -> Option<String> {
894 let rest = uri.split_once("://")?.1;
895 rest.find('/').map(|separator| rest[separator..].to_string())
896}
897
898#[allow(clippy::too_many_arguments)]
902fn class_group(
903 host: &EmitterHost<'_>,
904 models: &dyn IXamlTypeSystem,
905 root_uri: &str,
906 documents: &[(&str, &str)],
907 class_document: &str,
908 constructor: Option<ClassConstructor>,
909 module_path: &str,
910 options: &TransformOptions,
911) -> Result<ferroui_markup_xaml_loader::rust_emitter::ClassFile, ClassGroupError> {
912 let group = class_document_group(root_uri, documents, class_document).map_err(ClassGroupError::Transform)?;
913 let class_name = class_of_document(&group[0].1)
914 .map_err(ClassGroupError::Transform)?
915 .ok_or_else(|| ClassGroupError::Transform("the document names no class (`x:Class`)".to_string()))?;
916 let class = models.find_type(&class_name).ok_or_else(|| {
917 ClassGroupError::Transform(format!(
918 "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"
919 ))
920 })?;
921 generate_class_file_with(host, &ClassGroup { class, documents: &group, constructor, module_path }, options).map_err(|reasons| {
922 match reasons.strip_prefix(CLASS_GROUP_NOT_TRANSFORMED) {
923 Some(_) => ClassGroupError::Transform(reasons),
924 None => ClassGroupError::Emit(reasons),
925 }
926 })
927}
928
929enum ClassGroupError {
931 Transform(String),
934 Emit(String),
936}
937
938pub fn compile_xaml(assembly: &'static MarkupAssembly) {
941 Build::from_env().assembly(assembly).compile_xaml().run();
942}
943
944pub fn embed_assets(assembly: &'static MarkupAssembly, directories: &[&str]) {
947 Build::from_env().assembly(assembly).embed_assets(directories).run();
948}
949
950pub fn export_metadata() {
953 Build::from_env().export_metadata().run();
954}
955
956fn collect(root: &Path, directory: &Path, found: &mut Vec<(String, PathBuf)>) -> Result<(), String> {
960 let entries = fs::read_dir(directory).map_err(|error| format!("{}: {error}", directory.display()))?;
961 let mut paths = Vec::new();
962 for entry in entries {
963 paths.push(entry.map_err(|error| format!("{}: {error}", directory.display()))?.path());
964 }
965 paths.sort();
966 for path in paths {
967 let name = path.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
968 if path.is_dir() {
969 if !name.starts_with('.') && !(directory == root && name == "target") {
970 collect(root, &path, found)?;
971 }
972 continue;
973 }
974 let relative = path.strip_prefix(root).map_err(|_| format!("{} is not below {}", path.display(), root.display()))?;
975 let parts: Vec<String> = relative.components().map(|part| part.as_os_str().to_string_lossy().into_owned()).collect();
976 found.push((format!("/{}", parts.join("/")), path));
977 }
978 Ok(())
979}
980
981fn write_if_changed(path: &Path, text: &str, errors: &mut Vec<String>) {
984 if let Err(error) = ferroui_build_scan::export::write_if_changed(path, text) {
985 errors.push(error);
986 }
987}
988
989fn crate_file(assembly_name: &str, modules: &[Module], loaders: &[String], assets: &[(String, PathBuf)]) -> String {
994 let mut text = String::new();
995 text.push_str("// @generated by ferroui-build (Build::run). Do not edit.\n");
996 for Module { name: module, path, file, .. } in modules {
997 text.push('\n');
998 text.push_str(&format!("/// The compiled documents of the group `{module}`.\n"));
999 text.push_str("#[doc(hidden)]\n");
1000 text.push_str("#[rustfmt::skip]\n");
1001 if *file {
1002 text.push_str(&format!("#[path = {}]\n", rust_string_literal(&path.display().to_string())));
1004 text.push_str(&format!("pub mod {module};\n"));
1005 continue;
1006 }
1007 text.push_str("#[allow(dead_code)]\n");
1010 text.push_str(&format!("pub mod {module} {{\n"));
1011 text.push_str(&format!(" include!({});\n", rust_string_literal(&path.display().to_string())));
1012 text.push_str("}\n");
1013 }
1014 text.push('\n');
1015 text.push_str("/// The compiled markup and the embedded assets of the crate.\n");
1016 text.push_str("#[doc(hidden)]\n");
1017 text.push_str("#[rustfmt::skip]\n");
1018 text.push_str("pub mod compiled_markup {\n");
1019 text.push_str(" /// The assembly of the crate.\n");
1020 text.push_str(&format!(" pub const ASSEMBLY_NAME: &str = {};\n", rust_string_literal(assembly_name)));
1021 text.push('\n');
1022 text.push_str(" /// The embedded assets of the crate, by rooted path.\n");
1023 text.push_str(" pub static ASSETS: &[(&str, &[u8])] = &[\n");
1024 for (rooted, path) in assets {
1025 text.push_str(&format!(
1026 " ({}, include_bytes!({})),\n",
1027 rust_string_literal(rooted),
1028 rust_string_literal(&path.display().to_string())
1029 ));
1030 }
1031 text.push_str(" ];\n");
1032 text.push('\n');
1033 text.push_str(" /// The loader of the compiled markup of the crate: the object of the document with the URI\n");
1034 text.push_str(" /// `uri`, from the first table that has it; `Ok(None)` if none has.\n");
1035 text.push_str(" #[allow(unused_variables)]\n");
1036 text.push_str(" pub fn try_load(\n");
1037 text.push_str(" service_provider: ::core::option::Option<&::std::rc::Rc<dyn ::ferroui_base::metadata::IServiceProvider>>,\n");
1038 text.push_str(" uri: &str,\n");
1039 text.push_str(" ) -> ::core::result::Result<::core::option::Option<::ferroui_base::BoxedValue>, ::ferroui_markup_xaml::XamlLoadException> {\n");
1040 let tables = loaders
1041 .iter()
1042 .cloned()
1043 .chain(modules.iter().filter(|module| module.exported).map(|module| format!("super::{}::try_load", module.name)));
1044 for table in tables {
1045 text.push_str(&format!(" if let ::core::option::Option::Some(loaded) = {table}(service_provider, uri)? {{\n"));
1046 text.push_str(" return ::core::result::Result::Ok(::core::option::Option::Some(loaded));\n");
1047 text.push_str(" }\n");
1048 }
1049 text.push_str(" ::core::result::Result::Ok(::core::option::Option::None)\n");
1050 text.push_str(" }\n");
1051 text.push('\n');
1052 text.push_str(" /// Registers the assets and the compiled markup of the crate. The assembly is made known\n");
1053 text.push_str(" /// to the asset loader even without assets: a load by URI asks it for the assembly of the\n");
1054 text.push_str(" /// document.\n");
1055 text.push_str(" pub fn register() {\n");
1056 text.push_str(" ::ferroui_base::platform::register_assets(ASSEMBLY_NAME, ASSETS);\n");
1057 text.push_str(" ::ferroui_markup_xaml::FerroXamlLoader::register_compiled_xaml(ASSEMBLY_NAME, try_load);\n");
1058 text.push_str(" }\n");
1059 text.push_str("}\n");
1060 text
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065 use super::*;
1066
1067 #[test]
1071 fn crate_file_declares_the_modules_and_the_loader_table() {
1072 let module = |name: &str, exported: bool, file: bool| Module { name: name.to_string(), path: PathBuf::from(format!("/out/xaml/{name}.rs")), exported, file };
1073 let modules = vec![module("compiled_xaml", true, false), module("compiled_xaml_source_info", false, false), module("compiled_main_window", true, true)];
1074 let assets = vec![("/Assets/a.png".to_string(), PathBuf::from("/crate/Assets/a.png"))];
1075 let text = crate_file("Tests", &modules, &["crate::compiled_main::try_load".to_string()], &assets);
1076 assert!(text.contains("pub mod compiled_xaml {\n include!(\"/out/xaml/compiled_xaml.rs\");\n}\n"), "{text}");
1077 assert!(text.contains("pub mod compiled_xaml_source_info {\n"), "{text}");
1078 assert!(text.contains(" pub const ASSEMBLY_NAME: &str = \"Tests\";\n"), "{text}");
1079 assert!(text.contains(" (\"/Assets/a.png\", include_bytes!(\"/crate/Assets/a.png\")),\n"), "{text}");
1080 let checked_in = text.find("= crate::compiled_main::try_load(service_provider, uri)?").expect("the checked-in table");
1081 let group = text.find("= super::compiled_xaml::try_load(service_provider, uri)?").expect("the table of the group");
1082 assert!(checked_in < group, "{text}");
1083 assert!(!text.contains("super::compiled_xaml_source_info::try_load"), "{text}");
1084 assert!(text.contains("#[path = \"/out/xaml/compiled_main_window.rs\"]\npub mod compiled_main_window;\n"), "{text}");
1086 assert!(text.contains("= super::compiled_main_window::try_load(service_provider, uri)?"), "{text}");
1087 }
1088
1089 #[test]
1093 fn export_metadata_writes_the_model_and_reads_the_models_of_the_dependencies() {
1094 let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).with_file_name("FerroUI.Build.Scan").join("tests").join("fixtures");
1095 let out = env::temp_dir().join(format!("ferroui-build-export-{}", std::process::id()));
1096 let first = Build::new(fixtures.join("scanner"), out.join("fixture"), "fixture", Vec::new()).source_root("lib.rs").export_metadata().execute();
1097 assert_eq!(first.errors, Vec::<String>::new());
1098 let fixture_file = out.join("fixture").join("fixture.xamlmeta");
1099 assert!(first.lines.contains(&format!("cargo::metadata=xamlmeta={}", fixture_file.display())), "{:?}", first.lines);
1100 let border = fixtures.join("scanner").join("controls").join("border.rs");
1101 assert!(first.lines.contains(&format!("cargo::rerun-if-changed={}", border.display())), "{:?}", first.lines);
1102 assert!(first.lines.iter().any(|line| line.starts_with("cargo::warning=") && line.contains("error FRN9010")), "{:?}", first.lines);
1104 let text = fs::read_to_string(&fixture_file).expect("the .xamlmeta of the fixture");
1105 let model = AssemblyModel::parse(&text).expect("a file of format 2");
1106 assert_eq!((model.name.as_str(), model.crate_name.as_str()), ("Fixture", "fixture"));
1107 assert!(model.find_type("Fixture.Controls.Border").is_some() && !model.exports.is_empty());
1108 assert!(model.documents.is_empty() && model.dependencies.is_empty());
1109 assert_eq!(XamlMetadata::parse(&text).map(|metadata| metadata.name), Ok("Fixture".to_string()));
1110 assert!(!out.join("fixture").join("xaml").join("mod.rs").exists(), "a crate that only exports its model includes nothing");
1111
1112 let second = Build::new(fixtures.join("dependent"), out.join("dependent"), "dependent", vec![fixture_file.clone()]).export_metadata().execute();
1113 assert_eq!(second.errors, Vec::<String>::new());
1114 assert!(second.lines.contains(&format!("cargo::rerun-if-changed={}", fixture_file.display())), "{:?}", second.lines);
1115 let text = fs::read_to_string(out.join("dependent").join("dependent.xamlmeta")).expect("the .xamlmeta of the dependent crate");
1116 let model = AssemblyModel::parse(&text).expect("a file of format 2");
1117 assert_eq!((model.name.as_str(), model.crate_name.as_str()), ("dependent", "dependent"));
1118 assert_eq!(model.dependencies, [fixture_file.display().to_string()]);
1119 let card = model.find_type("Card").expect("the class of the dependent crate");
1120 assert_eq!(card.registered[0].name.as_deref(), Some("Background"));
1121 assert_eq!(card.base.as_ref().map(|base| base.text.as_str()), Some("::fixture::controls::border::Border"));
1122 let (models, files) = ModelSet::read(&[out.join("dependent").join("dependent.xamlmeta")]).expect("the two files");
1124 assert_eq!(models.iter().map(|model| model.name.as_str()).collect::<Vec<_>>(), ["dependent", "Fixture"]);
1125 assert_eq!(files.len(), 2);
1126
1127 let missing = Build::new(fixtures.join("dependent"), out.join("missing"), "dependent", Vec::new()).source_root("main.rs").export_metadata().execute();
1128 assert_eq!(missing.errors.len(), 1, "{:?}", missing.errors);
1129 assert!(missing.errors[0].ends_with("main.rs, is not a file"), "{:?}", missing.errors);
1130 let _ = fs::remove_dir_all(&out);
1131 }
1132
1133 #[test]
1136 fn texts_the_scanner_states_are_the_ones_of_the_base_crate() {
1137 assert_eq!(ferroui_build_scan::FERRO_XML_NAMESPACE, ferroui_base::metadata::FERRO_XML_NAMESPACE);
1138 assert_eq!(ferroui_build_scan::CREATE_SOURCE_INFO, MarkupAssembly::CREATE_SOURCE_INFO);
1139 assert_eq!(ferroui_build_scan::USE_COMPILED_BINDINGS_BY_DEFAULT, MarkupAssembly::USE_COMPILED_BINDINGS_BY_DEFAULT);
1140 }
1141
1142 #[test]
1148 fn class_document_is_compiled_against_the_models_only() {
1149 let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).with_file_name("FerroUI.Build.Scan").join("tests").join("fixtures");
1150 let out = env::temp_dir().join(format!("ferroui-build-class-{}", std::process::id()));
1151 let document = "<Border xmlns='https://github.com/ferroui' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' x:Class='Fixture.Missing'/>";
1152 let group = || XamlGroup::new("compiled_border").documents(&[("Border.xaml", document)]).class_document("Border.xaml");
1153 let build = |name: &str| Build::new(fixtures.join("scanner"), out.join(name), "fixture", Vec::new()).source_root("lib.rs");
1154
1155 let on_models = build("models").type_system(TypeSystem::Model).compile_group(group()).execute();
1156 assert_eq!(on_models.errors.len(), 1, "{:?}", on_models.errors);
1157 assert!(
1158 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`"),
1159 "{:?}",
1160 on_models.errors
1161 );
1162 let border = fixtures.join("scanner").join("controls").join("border.rs");
1164 assert!(on_models.lines.contains(&format!("cargo::rerun-if-changed={}", border.display())), "{:?}", on_models.lines);
1165
1166 static ASSEMBLY: MarkupAssembly =
1169 MarkupAssembly { name: "Fixture", crate_name: "fixture", xmlns_definitions: &[], xmlns_prefixes: &[], metadata: &[] };
1170 let at_run_time = build("runtime").assembly(&ASSEMBLY).compile_group(group()).execute();
1171 assert_eq!(at_run_time.errors.len(), 1, "{:?}", at_run_time.errors);
1172 let expected = match cfg!(feature = "runtime-host") {
1173 true => "Border.xaml: error FRN2000: the document of a class of the crate is compiled against the type models only",
1174 false => "the documents are compiled against the type models (Build::type_system(TypeSystem::Model)): the run-time type system is the feature `runtime-host`",
1175 };
1176 assert!(at_run_time.errors[0].starts_with(expected), "{:?}", at_run_time.errors);
1177 let _ = fs::remove_dir_all(&out);
1178 }
1179
1180 #[test]
1183 fn build_without_an_assembly_is_an_error() {
1184 let outcome = Build::new(PathBuf::from("/crate"), PathBuf::from("/out"), "my-crate", Vec::new()).compile_xaml().execute();
1185 assert_eq!(outcome.errors, ["the assembly of the crate is not stated (Build::assembly)"]);
1186 assert_eq!(outcome.lines, ["cargo::rerun-if-changed=/crate/build.rs", "cargo::rerun-if-changed=/crate/Cargo.toml"]);
1187 }
1188}