Skip to main content

ferroui_static_view_locator_build/
lib.rs

1//! ferroui-static-view-locator-build
2//!
3//! The generator of `ferroui-static-view-locator`, run by the build script of a crate that
4//! declares view locators: a port of the source generator of StaticViewLocator
5//! (<https://github.com/wieslawsoltes/StaticViewLocator>, `StaticViewLocatorGenerator`). The plan,
6//! the mapping of every feature and the deviations are in `docs/porting/static-view-locator.md`.
7//!
8//! ```ignore
9//! // build.rs
10//! fn main() {
11//!     ferroui_static_view_locator_build::StaticViewLocatorBuild::from_env()
12//!         .view_model_namespace_prefixes(["crate::view_models"])
13//!         .run();
14//! }
15//! ```
16//!
17//! ```toml
18//! [build-dependencies]
19//! ferroui-static-view-locator-build = "0.4"
20//! ```
21//!
22//! # What a run does
23//!
24//! 1. Reads the `.xamlmeta` models of the dependencies (`DEP_<LINKS>_XAMLMETA`): the base classes
25//!    of the views (`UserControl`, `Window` and the classes derived from them in other crates) and,
26//!    with [`include_referenced_crates`](StaticViewLocatorBuild::include_referenced_crates), their
27//!    view models.
28//! 2. Scans the sources of the crate with the scanner of the framework (`ferroui-build-scan`): the
29//!    classes, their base classes and constructors, the paths of the modules.
30//! 3. Reads the items of the sources: the types, the `impl` blocks, the visibility of the modules
31//!    and the structs with `#[static_view_locator]`.
32//! 4. For each locator, maps its view models to their views and writes
33//!    `$OUT_DIR/static_view_locator/<name>.rs`, which `#[static_view_locator]` includes.
34//! 5. Prints the diagnostics (an error fails the build) and `cargo::rerun-if-changed` for every
35//!    file it read.
36//!
37//! The options of the reference's MSBuild properties are the setters of
38//! [`StaticViewLocatorBuild`]; the options of its attribute are the options of
39//! `#[static_view_locator(..)]`.
40
41#![forbid(unsafe_code)]
42
43pub mod diagnostics;
44mod emit;
45mod generator;
46pub mod options;
47mod source;
48#[cfg(test)]
49mod tests;
50mod universe;
51
52pub use diagnostics::{Diagnostic, Severity};
53pub use generator::ResolutionMode;
54pub use options::{GeneratorOptions, ReplacementRule};
55
56use ferroui_build_scan::export::{dependencies_from_env, source_root, write_if_changed};
57use ferroui_build_scan::model::AssemblyModel;
58use ferroui_build_scan::model_set::ModelSet;
59use ferroui_build_scan::scanner::{scan_crate, ScanOptions};
60use source::SourceCrate;
61use std::collections::{BTreeMap, BTreeSet};
62use std::env;
63use std::fs;
64use std::path::{Path, PathBuf};
65use universe::Universe;
66
67/// The directory below `OUT_DIR` the generated files are written to; `#[static_view_locator]`
68/// includes them from there.
69pub const OUTPUT_DIRECTORY: &str = "static_view_locator";
70
71/// A generated file.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct GeneratedFile {
74    /// The name of the file below [`OUTPUT_DIRECTORY`] (`ViewLocator.rs`).
75    pub name: String,
76    pub text: String,
77}
78
79/// What a run did.
80#[derive(Clone, Debug, Default)]
81pub struct Outcome {
82    /// The `cargo::` lines, in order (the diagnostics are not among them).
83    pub lines: Vec<String>,
84    /// The diagnostics, the errors first.
85    pub diagnostics: Vec<Diagnostic>,
86    /// The generated files, in the order of the locators.
87    pub files: Vec<GeneratedFile>,
88}
89
90impl Outcome {
91    /// The errors.
92    pub fn errors(&self) -> impl Iterator<Item = &Diagnostic> {
93        self.diagnostics.iter().filter(|diagnostic| diagnostic.severity == Severity::Error)
94    }
95
96    /// The generated file `name`.
97    pub fn file(&self, name: &str) -> Option<&str> {
98        self.files.iter().find(|file| file.name == name).map(|file| file.text.as_str())
99    }
100}
101
102/// The generator of the locators of a crate, run by its build script.
103pub struct StaticViewLocatorBuild {
104    manifest_dir: PathBuf,
105    out_dir: PathBuf,
106    crate_name: String,
107    dependencies: Vec<PathBuf>,
108    source_root: Option<PathBuf>,
109    options: GeneratorOptions,
110    reactiveui_available: bool,
111    write_files: bool,
112}
113
114impl StaticViewLocatorBuild {
115    /// The generator of the crate whose build script is running: `CARGO_MANIFEST_DIR`, `OUT_DIR`,
116    /// `CARGO_PKG_NAME` (the name of the library when the crate states another one cannot be
117    /// read; state it with [`crate_name`](Self::crate_name)), the `.xamlmeta` of every direct
118    /// dependency, and whether ferroui-static-view-locator has the feature `reactiveui`
119    /// (`DEP_FERROUI_STATIC_VIEW_LOCATOR_REACTIVEUI`).
120    ///
121    /// # Panics
122    /// Panics outside a build script (a variable Cargo sets is missing).
123    pub fn from_env() -> Self {
124        let variable = |name: &str| env::var_os(name).unwrap_or_else(|| panic!("{name} is not set: not a build script"));
125        let mut build = Self::new(
126            PathBuf::from(variable("CARGO_MANIFEST_DIR")),
127            PathBuf::from(variable("OUT_DIR")),
128            &variable("CARGO_PKG_NAME").to_string_lossy(),
129            dependencies_from_env(),
130        );
131        build.reactiveui_available = env::var_os("DEP_FERROUI_STATIC_VIEW_LOCATOR_REACTIVEUI").is_some();
132        build
133    }
134
135    /// The generator of the package `package_name` in `manifest_dir`, writing below `out_dir`,
136    /// with the `.xamlmeta` files of its direct dependencies.
137    pub fn new(manifest_dir: PathBuf, out_dir: PathBuf, package_name: &str, dependencies: Vec<PathBuf>) -> Self {
138        Self {
139            manifest_dir,
140            out_dir,
141            crate_name: package_name.replace('-', "_"),
142            dependencies,
143            source_root: None,
144            options: GeneratorOptions::default(),
145            reactiveui_available: false,
146            write_files: true,
147        }
148    }
149
150    /// The name of the crate as Rust paths spell it, when the library has another name than the
151    /// package.
152    pub fn crate_name(mut self, name: &str) -> Self {
153        self.crate_name = name.replace('-', "_");
154        self
155    }
156
157    /// The root file of the sources, relative to the crate directory. Without the call:
158    /// `src/lib.rs`, `lib.rs`, else `src/main.rs`.
159    pub fn source_root(mut self, file: &str) -> Self {
160        self.source_root = Some(PathBuf::from(file));
161        self
162    }
163
164    /// The module paths view models are taken from (`StaticViewLocatorViewModelNamespacePrefixes`).
165    pub fn view_model_namespace_prefixes<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, prefixes: I) -> Self {
166        self.options.namespace_prefixes = prefixes.into_iter().map(|prefix| prefix.as_ref().to_string()).collect();
167        self
168    }
169
170    /// `StaticViewLocatorIncludeInternalViewModels`; without an effect (see
171    /// [`GeneratorOptions::include_internal_view_models`]).
172    pub fn include_internal_view_models(mut self, include: bool) -> Self {
173        self.options.include_internal_view_models = include;
174        self
175    }
176
177    /// Whether the view models of the crates the crate depends on are taken
178    /// (`StaticViewLocatorIncludeReferencedAssemblies`).
179    pub fn include_referenced_crates(mut self, include: bool) -> Self {
180        self.options.include_referenced_crates = include;
181        self
182    }
183
184    /// More view base types (`StaticViewLocatorAdditionalViewBaseTypes`).
185    pub fn additional_view_base_types<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, types: I) -> Self {
186        self.options.additional_view_base_types = types.into_iter().map(|type_| type_.as_ref().to_string()).collect();
187        self
188    }
189
190    /// The namespace rules applied before the defaults (`StaticViewLocatorNamespaceReplacementRules`).
191    pub fn namespace_replacement_rules<I: IntoIterator<Item = (F, T)>, F: AsRef<str>, T: AsRef<str>>(mut self, rules: I) -> Self {
192        self.options.namespace_replacement_rules = rules.into_iter().map(|(from, to)| ReplacementRule::new(from.as_ref(), to.as_ref())).collect();
193        self
194    }
195
196    /// The type name rules applied before the default (`StaticViewLocatorTypeNameReplacementRules`).
197    pub fn type_name_replacement_rules<I: IntoIterator<Item = (F, T)>, F: AsRef<str>, T: AsRef<str>>(mut self, rules: I) -> Self {
198        self.options.type_name_replacement_rules = rules.into_iter().map(|(from, to)| ReplacementRule::new(from.as_ref(), to.as_ref())).collect();
199        self
200    }
201
202    /// `StaticViewLocatorStripGenericArityFromViewName` (default `true`).
203    pub fn strip_generic_arity_from_view_name(mut self, strip: bool) -> Self {
204        self.options.strip_generic_arity_from_view_name = strip;
205        self
206    }
207
208    /// The prefixes removed from the names of trait view models (`StaticViewLocatorInterfacePrefixesToStrip`).
209    pub fn interface_prefixes_to_strip<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, prefixes: I) -> Self {
210        let prefixes: Vec<String> = prefixes.into_iter().map(|prefix| prefix.as_ref().to_string()).filter(|prefix| !prefix.is_empty()).collect();
211        self.options.interface_prefixes_to_strip = if prefixes.is_empty() { vec!["I".to_string()] } else { prefixes };
212        self
213    }
214
215    /// Sets an option from its text, as an MSBuild property is read
216    /// ([`GeneratorOptions::set`]): `property("type_name_replacement_rules", "ViewModel=View,Vm=Page")`.
217    pub fn property(mut self, name: &str, value: &str) -> Self {
218        if let Err(error) = self.options.set(name, value) {
219            panic!("{error}");
220        }
221        self
222    }
223
224    /// All the options at once.
225    pub fn options(mut self, options: GeneratorOptions) -> Self {
226        self.options = options;
227        self
228    }
229
230    /// Whether ferroui-static-view-locator has the feature `reactiveui`; [`from_env`](Self::from_env)
231    /// reads it from Cargo.
232    pub fn reactiveui_available(mut self, available: bool) -> Self {
233        self.reactiveui_available = available;
234        self
235    }
236
237    /// Whether [`execute`](Self::execute) writes the generated files (default `true`); without, the
238    /// files are only in the [`Outcome`].
239    pub fn write_files(mut self, write: bool) -> Self {
240        self.write_files = write;
241        self
242    }
243
244    /// Runs the generator and prints its lines and diagnostics for Cargo. On an error the process
245    /// exits with a failure.
246    pub fn run(self) {
247        let outcome = self.execute();
248        for line in &outcome.lines {
249            println!("{line}");
250        }
251        for diagnostic in &outcome.diagnostics {
252            if diagnostic.severity == Severity::Error {
253                eprintln!("{diagnostic}");
254            }
255            println!("{}", diagnostic.cargo_line());
256        }
257        if outcome.errors().next().is_some() {
258            std::process::exit(1);
259        }
260    }
261
262    /// Runs the generator and returns what [`run`](Self::run) prints, with the generated files.
263    pub fn execute(self) -> Outcome {
264        let mut outcome = Outcome::default();
265        let rerun = |lines: &mut Vec<String>, path: &Path| lines.push(format!("cargo::rerun-if-changed={}", path.display()));
266        rerun(&mut outcome.lines, &self.manifest_dir.join("build.rs"));
267        rerun(&mut outcome.lines, &self.manifest_dir.join("Cargo.toml"));
268        outcome.lines.push("cargo::rerun-if-env-changed=DEP_FERROUI_STATIC_VIEW_LOCATOR_REACTIVEUI".to_string());
269
270        let error = |message: String| Diagnostic {
271            code: diagnostics::INVALID_OPTION,
272            severity: Severity::Error,
273            message,
274            file: None,
275            line: 0,
276        };
277        let (models, files) = match ModelSet::read(&self.dependencies) {
278            Ok(read) => read,
279            Err(message) => {
280                outcome.diagnostics.push(error(format!("the .xamlmeta of a dependency cannot be read: {message}")));
281                return outcome;
282            }
283        };
284        for file in &files {
285            rerun(&mut outcome.lines, file);
286        }
287        // The crates generated source can name: the direct dependencies.
288        let mut direct_crates: BTreeSet<String> = BTreeSet::new();
289        for path in &self.dependencies {
290            if let Ok(model) = fs::read_to_string(path).map_err(|_| ()).and_then(|text| AssemblyModel::parse(&text).map_err(|_| ())) {
291                direct_crates.insert(model.crate_name);
292            }
293        }
294        let root = match source_root(&self.manifest_dir, self.source_root.as_deref()) {
295            Ok(root) => root,
296            Err(message) => {
297                outcome.diagnostics.push(error(message));
298                return outcome;
299            }
300        };
301        let models = ModelSet::new(models);
302        let scan = scan_crate(&ScanOptions::new(&self.crate_name, &root).with_dependencies(models.models().to_vec()));
303        for file in &scan.files {
304            rerun(&mut outcome.lines, &file.path);
305        }
306        let files: Vec<(PathBuf, String)> = scan.files.iter().map(|file| (file.path.clone(), file.module.clone())).collect();
307        let source = SourceCrate::read(&self.crate_name, &files);
308        outcome.diagnostics.extend(source.diagnostics.iter().cloned());
309        let universe = Universe::new(&self.crate_name, &scan, &models, &source, direct_crates);
310
311        // One file per locator: its name, else its id (`GetGeneratedHintName`).
312        let mut names: BTreeMap<String, usize> = BTreeMap::new();
313        for locator in &source.locators {
314            *names.entry(locator.attribute.id.clone().unwrap_or_else(|| locator.name.clone())).or_default() += 1;
315        }
316        for locator in &source.locators {
317            let name = locator.attribute.id.clone().unwrap_or_else(|| locator.name.clone());
318            if names[&name] > 1 {
319                outcome.diagnostics.push(Diagnostic::error(
320                    diagnostics::DUPLICATE_LOCATOR_FILE,
321                    format!(
322                        "Locator '{}' generates the file '{name}.rs' as another locator of the crate does; give each a distinct id: #[static_view_locator(id = \"..\")]",
323                        locator.name
324                    ),
325                    &locator.location(),
326                ));
327                continue;
328            }
329            let mut locator_diagnostics = Vec::new();
330            let text = generator::generate_locator(&universe, &self.options, locator, self.reactiveui_available, &mut locator_diagnostics);
331            outcome.diagnostics.extend(locator_diagnostics);
332            outcome.files.push(GeneratedFile { name: format!("{name}.rs"), text });
333        }
334        outcome.diagnostics.sort_by_key(|diagnostic| diagnostic.severity);
335
336        if self.write_files {
337            let directory = self.out_dir.join(OUTPUT_DIRECTORY);
338            for file in &outcome.files {
339                if let Err(message) = write_if_changed(&directory.join(&file.name), &file.text) {
340                    outcome.diagnostics.push(error(message));
341                }
342            }
343        }
344        outcome
345    }
346}