Skip to main content

ferroui_build/
lib.rs

1//! ferroui-build
2//!
3//! Build-script support: compiles the markup documents of a crate to Rust
4//! source and embeds its assets (docs/porting/xaml.md, section 9.6). The
5//! counterpart of the build tasks of the upstream project
6//! (`CompileXamlTask` with `XamlCompilerTaskExecutor`, and
7//! `GenerateResourcesTask`), which run after the compiler and rewrite the
8//! assembly; a Rust crate has no such step, so the build script of the crate
9//! runs before the compiler and writes source the crate includes.
10//!
11//! ```ignore
12//! // build.rs
13//! fn main() {
14//!     ferroui_controls::register_types();           // the types the documents name
15//!     ferroui_build::Build::from_env()
16//!         .assembly(&ASSEMBLY)                      // the assembly of the crate
17//!         .embed_assets(&["Assets"])
18//!         .compile_xaml()                           // every *.xaml of the crate
19//!         .run();
20//! }
21//!
22//! // lib.rs
23//! ferroui_markup_xaml::include_compiled_xaml!();    // pub mod compiled_xaml, pub mod compiled_markup
24//! pub fn register_types() { /* .. */ compiled_markup::register(); }
25//! ```
26//!
27//! # What a run does, in order
28//!
29//! 1. Reads the `.xamlmeta` of every crate the crate depends on directly
30//!    (`DEP_<CRATE>_XAML_XAMLMETA`, which Cargo sets from the `links` key and
31//!    the `cargo::metadata=xamlmeta=<path>` line of that crate) and,
32//!    transitively, the files they name: the compiled documents an include of
33//!    a document of another crate is linked to.
34//! 2. Compiles each group of documents as upstream compiles the documents of
35//!    an assembly (parse, transform, group transformers, emit) and writes
36//!    `$OUT_DIR/xaml/<module>.rs` with its position map.
37//! 3. Lists the assets, leaving out the documents it compiled (upstream
38//!    removes a compiled resource from the assembly).
39//! 4. Writes `$OUT_DIR/<crate>.xamlmeta` (the compiled documents of the
40//!    crate, and the paths of the files of step 1) and
41//!    `$OUT_DIR/xaml/mod.rs` (the modules of the groups, the asset table, the
42//!    loader table of the crate and `register()`).
43//! 5. Prints the `cargo::` lines: `rerun-if-changed` for every input,
44//!    `metadata=xamlmeta=<path>` for the crates that depend on this one, and
45//!    `rustc-env=FERROUI_XAMLMETA=<path>` for the crate itself.
46//!
47//! Every document is compiled before the run fails: each error is printed as
48//! a `cargo::error=` line, and the script exits with a failure once.
49//!
50//! # Diagnostics
51//!
52//! What the compiler reports has the code of upstream's build task
53//! (`FerroXamlDiagnosticCodes`) and, where it is known, the document and the
54//! position: `Views/Main.xaml(12,8): warning FRN2208: ..`. A diagnostic of the
55//! transform goes through the filter of upstream's task
56//! ([`xaml_compiler_diagnostics_filter`]): the EditorConfig files of the build
57//! ([`Build::analyzer_config_files`]) state its severity by its code, and
58//! [`Build::warnings_as_errors`] makes every warning that is left an error. A
59//! warning is a `cargo::warning=` line; an error fails the build: a group that
60//! does not transform (`FRN2000`, once for the group, or the diagnostics of the
61//! transform that say why), a document the emitter refuses (`FRN3000`, with the
62//! document and the node), a class no load by URI can create (`FRN3001`, a
63//! warning).
64//!
65//! # Where the type metadata comes from
66//!
67//! A build compiles against the type models of the crates
68//! ([`TypeSystem::Model`], the next section): this crate links the compiler
69//! (the transform and the emitter of Rust source) and the base crate, and
70//! neither the XAML runtime library nor the controls, so a build script that
71//! uses it builds none of them for the host.
72//!
73//! The other host is the feature `runtime-host` ([`TypeSystem::Runtime`]): the
74//! compiler transforms a document against the run-time type system, the
75//! types, properties and constructors the crates registered in the process
76//! (`register_types()` of each). A build script that enables it takes the crates
77//! whose types its documents name as build dependencies and registers them
78//! before it calls [`Build::run`], and it starts whatever application
79//! services the transform of its documents needs. Two consequences:
80//!
81//! - those crates are built for the host as well as for the target (once
82//!   more when the two differ or their features do);
83//! - a build script cannot link the crate it builds, so it cannot compile a
84//!   document that names a type of that crate: neither the document of a
85//!   class of the crate (`x:Class`) nor a document that uses one of its
86//!   types. Such a document is generated by a test of the crate and checked
87//!   in (`rust_emitter::generate_class_file`); its loader table and its
88//!   `.xamlmeta` join the ones of the build through [`Build::loader`] and
89//!   [`Build::checked_in_metadata`].
90//!
91//! The feature links the XAML runtime library and, through it, the controls. No
92//! crate of the repository enables it.
93//!
94//! # The build-time type model
95//!
96//! The way out of both consequences is xaml.md 9.5: the types read from the
97//! sources instead of the process. With [`Build::type_system`] and
98//! [`TypeSystem::Model`] a build scans the crate, reads the type models of
99//! the crates it depends on from their `.xamlmeta` (the framework crates
100//! export theirs from their own build scripts) and compiles every group
101//! against the type system over those models: the build script registers
102//! nothing, needs no crate for its types, and compiles the documents of the
103//! classes of the crate it builds ([`XamlGroup::class_document`]). The
104//! run-time type system stays the value of a build that does not state one, and
105//! is an error of the build without the feature `runtime-host`.
106//!
107//! ```ignore
108//! // build.rs
109//! fn main() {
110//!     ferroui_build::Build::from_env()
111//!         .type_system(ferroui_build::TypeSystem::Model)   // the assembly is the one the sources state
112//!         .compile_group(XamlGroup::new("compiled_xaml").documents(DOCUMENTS))
113//!         .compile_group(XamlGroup::new("compiled_main_window").documents(VIEWS).class_document("MainWindow.xaml"))
114//!         .run();
115//! }
116//! ```
117//!
118//! What is here:
119//!
120//! - [`model`], [`scanner`], [`model_set`], [`call_forms`], [`export`]: the
121//!   type model of a crate and its file (the `.xamlmeta` of format 2), the
122//!   source scanner that fills it, the models of several crates read together,
123//!   the call form of every declared member, and the export of a model from a
124//!   build script. They are the crate `ferroui-build-scan`, which links
125//!   nothing of the framework so that the framework crates can scan
126//!   themselves with it, exported here under the names they had in this crate;
127//! - [`Build::export_metadata`]: the scan of the crate, with the models of
128//!   its dependencies, written into its `.xamlmeta`;
129//! - [`type_system`]: the type system of the compiler over the models
130//!   (`ModelTypeSystem`, xaml.md 9.5.5), with the closed table of runtime
131//!   library types it shares with the run-time type system, and what the
132//!   models state for the emitter (`ModelEmitTypes`, xaml.md 9.5.12).
133//!
134//! What a build against the models does not have yet: the compile-time value
135//! parser of the run-time host (text a type refuses is found when the document
136//! is built, not when it is compiled), and a position map for the file of a
137//! class.
138//!
139//! ```ignore
140//! // build.rs of a crate that declares types and has no documents
141//! fn main() {
142//!     ferroui_build::Build::from_env().export_metadata().run();
143//! }
144//! ```
145//!
146//! # A crate with checked-in compiled markup
147//!
148//! A crate that keeps all of its compiled markup checked in (the two themes)
149//! does not need this crate: its build script prints the line of step 5 for
150//! its checked-in file, and its manifest has the `links` key.
151//!
152//! ```ignore
153//! println!("cargo::metadata=xamlmeta={}/compiled_xaml.xamlmeta", env!("CARGO_MANIFEST_DIR"));
154//! ```
155
156#![forbid(unsafe_code)]
157
158pub mod deterministic_id_generator;
159mod diagnostics;
160// The model, the scanner and the export are the crate the framework crates scan themselves
161// with (`ferroui-build-scan`, which links nothing of the framework); they are exported
162// here under the names they had in this crate.
163pub 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
189/// The name of the module of the default group ([`Build::compile_xaml`]).
190pub const DEFAULT_MODULE: &str = "compiled_xaml";
191
192/// The environment variable the build sets for the crate: the path of its `.xamlmeta`
193/// (`env!("FERROUI_XAMLMETA")`).
194pub const XAMLMETA_VARIABLE: &str = "FERROUI_XAMLMETA";
195
196/// A group of documents that are compiled together into one module: the group
197/// transformers see every document of it, so includes between them are linked, as
198/// upstream compiles the documents of an assembly.
199pub 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/// The type system the documents of a build are compiled against ([`Build::type_system`]).
209#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
210pub enum TypeSystem {
211    /// The run-time type system: the types the build script registered in its process. The
212    /// script links the crates whose types its documents name, and cannot compile a
213    /// document that names a type of the crate it builds. Only with the feature
214    /// `runtime-host` of this crate, which links the XAML runtime library and the
215    /// controls; without it a build that compiles documents against this type system
216    /// fails with an error that says so.
217    #[default]
218    Runtime,
219    /// The build-time type system over the type models (docs/porting/xaml.md, 9.5): the
220    /// model of the crate, scanned from its sources, and the models of the crates it
221    /// depends on, read from their `.xamlmeta`. The script links no crate for its types
222    /// and compiles the documents of the classes of the crate ([`XamlGroup::class_document`]).
223    Model,
224}
225
226impl XamlGroup {
227    /// A group whose generated file is the body of the module `module` of the crate
228    /// root. Without [`documents`](Self::documents) the group is every `.xaml` file
229    /// below the XAML root of the build.
230    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    /// Makes the group the group of a class of the crate: `name` (its path below the root
235    /// URI of the assembly) is the document of the class its root names with `x:Class`,
236    /// and the group is that document with every document of the group it includes,
237    /// directly or not. The generated file is the file of the class
238    /// (`rust_emitter::generate_class_file`): `populate`, which the constructor of the
239    /// class calls, the build functions of the included documents and the loader table.
240    /// Its module is declared at the crate root like the module of any group
241    /// (`crate::<module>::populate`).
242    ///
243    /// Only with [`TypeSystem::Model`]: the class is a type of the crate being built.
244    pub fn class_document(mut self, name: &str) -> Self {
245        self.class_document = Some(name.to_string());
246        self
247    }
248
249    /// The constructor the loader table creates the class of a class group with. Without
250    /// the call the compiler picks it as upstream's does: a public parameterless
251    /// constructor, else a public constructor that takes the service provider.
252    pub fn constructor(mut self, constructor: ClassConstructor) -> Self {
253        self.constructor = Some(constructor);
254        self
255    }
256
257    /// The documents of the group, by their path below the root URI of the assembly
258    /// (`Views/Main.xaml`), in the order they are compiled. The build script states the
259    /// files they come from with [`Build::input`].
260    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    /// Whether the documents are compiled with source information (`CreateSourceInfo`);
266    /// without the call, as the assembly states it.
267    pub fn create_source_info(mut self, value: bool) -> Self {
268        self.create_source_info = Some(value);
269        self
270    }
271
272    /// Keeps the group out of the loader table and of the `.xamlmeta` of the crate: its
273    /// documents are reached by their build functions only (a second compilation of
274    /// documents another group registers, as a test compiles them).
275    pub fn internal(mut self) -> Self {
276        self.exported = false;
277        self
278    }
279}
280
281pub use ferroui_build_scan::export::Outcome;
282
283/// The build of the markup of a crate; see the crate documentation.
284pub 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    /// The build of the crate whose build script is running: `CARGO_MANIFEST_DIR`,
306    /// `OUT_DIR`, `CARGO_PKG_NAME`, and the `.xamlmeta` of every direct dependency
307    /// (`DEP_<CRATE>_XAML_XAMLMETA`), in the order of the names of the variables.
308    ///
309    /// # Panics
310    /// Panics outside a build script (a variable Cargo sets is missing).
311    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    /// The build of the package `package_name` in `manifest_dir`, writing below `out_dir`,
322    /// with the `.xamlmeta` files of its direct dependencies.
323    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    /// The type system the documents are compiled against; [`TypeSystem::Runtime`] without
346    /// the call.
347    ///
348    /// With [`TypeSystem::Model`] the build scans the sources of the crate (as
349    /// [`export_metadata`](Self::export_metadata) does, and writes the model the same
350    /// way), reads the models of the crates it depends on from their `.xamlmeta`, and
351    /// compiles every group against the type system over those models. The build script
352    /// registers nothing and needs no [`assembly`](Self::assembly): the assembly is the
353    /// `MarkupAssembly` the sources state. A document the models cannot answer a question
354    /// of the emitter for is an error of the build that names the document and the
355    /// question; it is never compiled from a guess.
356    pub fn type_system(mut self, type_system: TypeSystem) -> Self {
357        self.type_system = type_system;
358        self
359    }
360
361    /// Whether `{Binding}` is a compiled binding in a document that does not say otherwise
362    /// (`x:CompileBindings`): the `DefaultCompileBindings` of upstream's build task. Not
363    /// without the call.
364    pub fn default_compile_bindings(mut self, value: bool) -> Self {
365        self.default_compile_bindings = value;
366        self
367    }
368
369    /// The EditorConfig files of the build, relative to the crate directory (the
370    /// `AnalyzerConfigFiles` of upstream's build task): an entry
371    /// `ferro_xaml_diagnostic.<code>.severity = <severity>` states the severity a diagnostic
372    /// of the markup compiler with that code is reported with (`error`, `warning`,
373    /// `default`; anything else silences it). A file that does not exist is skipped; the
374    /// build runs again when one changes.
375    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    /// Reports every warning of the markup compiler as an error of the build (the
381    /// `TreatWarningsAsErrors` upstream leaves to the build system), after the severities
382    /// of the EditorConfig files: a warning a file silences stays silent. Every document is
383    /// still compiled before the build fails.
384    pub fn warnings_as_errors(mut self, value: bool) -> Self {
385        self.warnings_as_errors = value;
386        self
387    }
388
389    /// Scans the sources of the crate and writes its type model into its `.xamlmeta`
390    /// (format 2: the model next to the compiled documents), which the crates that depend
391    /// on this one read as they read its documents (xaml.md 9.6.3). The scan reads the
392    /// models of the dependencies of the crate first, so that what the crate names of
393    /// theirs is resolved ([`ScanOptions::dependencies`]).
394    ///
395    /// The sources are read as files: nothing of the crate is linked, and a crate that
396    /// only exports its model needs no [`assembly`](Self::assembly) (the scan reads the
397    /// `MarkupAssembly` the sources state; a crate without one is the assembly named after
398    /// the crate). What the scanner cannot read of a declaration is a `cargo::warning=`
399    /// line and does not fail the build.
400    pub fn export_metadata(mut self) -> Self {
401        self.export_metadata = true;
402        self
403    }
404
405    /// The root file of the sources of the crate, relative to the crate directory, for
406    /// [`export_metadata`](Self::export_metadata). Without the call: `src/lib.rs`, else
407    /// `lib.rs`, else `src/main.rs`.
408    pub fn source_root(mut self, file: &str) -> Self {
409        self.source_root = Some(PathBuf::from(file));
410        self
411    }
412
413    /// The scan of the sources of the crate, with the models of its dependencies.
414    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    /// The assembly of the crate: its name is the host of the URIs of its documents and
422    /// assets, and its metadata the default of `CreateSourceInfo`. The same value the
423    /// crate registers in its `register_types()`.
424    pub fn assembly(mut self, assembly: &'static MarkupAssembly) -> Self {
425        self.assembly = Some(assembly);
426        self
427    }
428
429    /// The directory, relative to the crate directory, below which the documents of a
430    /// group without stated documents are found. The crate directory by default.
431    pub fn xaml_root(mut self, directory: &str) -> Self {
432        self.xaml_root = PathBuf::from(directory);
433        self
434    }
435
436    /// A file, relative to the crate directory, the build reads through the build
437    /// script (a file the script includes): the build runs again when it changes.
438    pub fn input(mut self, path: &str) -> Self {
439        self.inputs.push(self.manifest_dir.join(path));
440        self
441    }
442
443    /// Embeds every file below the directories (relative to the crate directory) as an
444    /// asset of the crate, by its path below the crate directory, except the documents
445    /// the build compiles.
446    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    /// Compiles every `.xaml` file below the XAML root as the group
452    /// [`DEFAULT_MODULE`].
453    pub fn compile_xaml(self) -> Self {
454        self.compile_group(XamlGroup::new(DEFAULT_MODULE))
455    }
456
457    /// Compiles a group of documents.
458    pub fn compile_group(mut self, group: XamlGroup) -> Self {
459        self.groups.push(group);
460        self
461    }
462
463    /// The loader table of a checked-in generated file of the crate, by the path of its
464    /// `try_load` (`crate::compiled_main_window::try_load`): the table of the crate asks
465    /// it first, in the order of the calls.
466    pub fn loader(mut self, path: &str) -> Self {
467        self.loaders.push(path.to_string());
468        self
469    }
470
471    /// The `.xamlmeta` of a checked-in generated file of the crate, relative to the crate
472    /// directory: its documents are added to the `.xamlmeta` of the crate.
473    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    /// Runs the build and prints its lines for Cargo. On an error every error is printed
479    /// and the process exits with a failure.
480    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    /// Runs the build and returns what [`run`](Self::run) prints.
495    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        // The documents are compiled against the assembly the crate registers; a build
506        // that reads the type model of the crate reads the assembly from the sources.
507        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        // The run-time type system is in the build only with the feature that links it.
512        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        // 1. The compiled markup and the type models of the dependencies.
521        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        // The type model of the crate, from its sources.
539        let scanned = self.export_metadata || on_models;
540        let mut model: Option<AssemblyModel> = None;
541        // The models the documents are compiled against: the ones of the dependencies, then
542        // the one of the crate.
543        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            // A crate that states no assembly is the assembly named after the crate, as
570            // the type systems name it.
571            (None, _) => self.crate_name.clone(),
572        };
573        // Whether source information is on for the assembly, as the configuration of the
574        // run-time loader reads it from the metadata of the assembly.
575        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        // The type system of the build.
580        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            // No class is one "with markup of its own" for a build: that states which
589            // classes the run-time loader populates, and a class whose document is compiled
590            // populates itself from its compiled markup.
591            let types = ModelEmitTypes::new(system.clone(), Vec::new());
592            // The compiled documents of the dependencies are part of the type system, as
593            // they are for the run-time host (`EmitterHost::runtime`).
594            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        // 2. The groups.
613        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            // The group of a class of the crate.
665            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                        // Upstream's `XamlLoaderUnreachable`, a warning the filter decides on.
687                        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                    // The class is not found, or its group does not transform: an error of the
698                    // transform, which the diagnostics of the transform reported if they
699                    // have it. Anything else is a document the emitter refuses.
700                    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                        // A reason names its document when it is not the document of the class.
707                        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                // Reported before the groups: without the feature no group is compiled
736                // against the run-time type system.
737                #[cfg(not(feature = "runtime-host"))]
738                None => break,
739            };
740            let reported_errors = diagnostics.report(&mut lines, &mut errors);
741            // A group that does not transform is one error of the transform, which the
742            // diagnostics of the transform reported if they have it; a document the emitter
743            // refuses is an error of the emit, each with its document.
744            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        // 3. The assets.
781        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        // 4. The `.xamlmeta` (with the type model of the crate, when it was scanned) and
798        // the file the crate includes.
799        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        // What the scan cost, for whoever judges it (`export::ScanReport`).
818        if scanned {
819            if let Err(error) = report.write(&self.out_dir) {
820                errors.push(error);
821            }
822        }
823        // A crate that only exports its model includes nothing.
824        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        // 5. For the dependents, and for the crate itself.
833        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/// The run-time host of the compiler: the documents are compiled against the types the
840/// build script registered in its process. With the feature `runtime-host`, which links
841/// the XAML runtime library and, through it, the controls.
842#[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    /// Makes the assembly of the crate and the runtime library known to the compiler.
850    pub(super) fn register(assembly: &'static MarkupAssembly) {
851        // The compiler resolves the assembly of the crate and the types of the runtime
852        // library; every other type is the build script's to register.
853        ferroui_markup_xaml::register_types();
854        MarkupAssembly::register(assembly);
855        // Makes the assembly known to the asset loader, as the `register_types()` of the
856        // crate does: the host of the emitter in the tests of a crate runs with it.
857        ferroui_base::platform::register_assets(assembly.name, &[]);
858        FerroRuntimeXamlLoader::register();
859    }
860
861    /// The generated file of a group, with the configuration of the run-time loader.
862    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
880/// The module of a group in the file the crate includes.
881struct Module {
882    name: String,
883    /// The generated file.
884    path: PathBuf,
885    /// Whether the loader table of the crate asks the table of the module.
886    exported: bool,
887    /// Whether the generated file is a module file of its own (the file of a class, which
888    /// starts with attributes of the module) and not the body of a module.
889    file: bool,
890}
891
892/// The rooted path (`/Folder/Style.xaml`) of the document with the URI `uri`.
893fn 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/// The file of the class whose document is `class_document`, among the documents of its
899/// crate: the class is the type the models have for the `x:Class` of the document, and
900/// the group is the document with the documents it includes.
901#[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
929/// Why the file of a class is not generated.
930enum ClassGroupError {
931    /// The group of the class is not found or does not transform (upstream's
932    /// `TransformError`).
933    Transform(String),
934    /// Documents the emitter refuses, one reason per line (upstream's `EmitError`).
935    Emit(String),
936}
937
938/// Compiles every `.xaml` file of the crate whose build script is running
939/// (`Build::from_env().assembly(assembly).compile_xaml().run()`).
940pub fn compile_xaml(assembly: &'static MarkupAssembly) {
941    Build::from_env().assembly(assembly).compile_xaml().run();
942}
943
944/// Embeds the files below `directories` as the assets of the crate whose build script
945/// is running (`Build::from_env().assembly(assembly).embed_assets(directories).run()`).
946pub fn embed_assets(assembly: &'static MarkupAssembly, directories: &[&str]) {
947    Build::from_env().assembly(assembly).embed_assets(directories).run();
948}
949
950/// Scans the sources of the crate whose build script is running and exports its type
951/// model (`Build::from_env().export_metadata().run()`).
952pub fn export_metadata() {
953    Build::from_env().export_metadata().run();
954}
955
956/// Every file below `directory`, with its rooted path below `root` (`/Assets/a.png`),
957/// in the order of the names. Directories whose name starts with a dot and the
958/// directory `target` of `root` are skipped.
959fn 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
981/// Writes `text` to `path` unless the file holds it already, so that a build that
982/// changed nothing does not make the compiler run again.
983fn 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
989/// The text of `$OUT_DIR/xaml/mod.rs`, which `include_compiled_xaml!()` includes at the
990/// root of the crate: the module of every group, and the module `compiled_markup`
991/// with the asset table, the loader table of the crate (the checked-in tables, then
992/// the groups, in order) and `register()`.
993fn 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            // The file of a class is a module file: it starts with attributes of the module.
1003            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        // A document that is not public (`x:ClassModifier`) and that the documents of its
1008        // group only merge has a build function nothing calls: its content is in theirs.
1009        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    /// Not from upstream: the file a crate includes names the generated files, chains the
1068    /// loader tables (the checked-in ones first, a group that is not exported not at all)
1069    /// and registers the assets with the table.
1070    #[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        // The file of a class is a module file of its own.
1085        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    /// Not from upstream: `export_metadata()` scans the sources of the crate, writes its
1090    /// type model into its `.xamlmeta` and names every file it read; the build of a crate
1091    /// that depends on it reads that file and resolves against the model in it.
1092    #[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        // What the scanner does not read is a warning of the build.
1103        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        // The models are read back as a set, the dependency through the file that names it.
1123        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    /// Not from upstream: the two texts of the base crate the scanner states itself, because
1134    /// the crate it is in cannot link the base crate, are the ones of the base crate.
1135    #[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    /// Not from upstream: against the type models a build reads the assembly from the
1143    /// sources and compiles the document of a class of the crate; what the models do not
1144    /// have is an error that names the document. The fixture declares no framework type,
1145    /// so the group does not get as far as a transform here: the include fixture
1146    /// (`tests/XamlIncludeFixture`) is the build that compiles.
1147    #[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        // The sources were scanned: the files are inputs of the build.
1163        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        // Against the run-time type system the document of a class is not compiled; without
1167        // the feature that links that type system no document is.
1168        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    /// Not from upstream: a build without an assembly reports it, with the lines that make
1181    /// Cargo run the script again.
1182    #[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}