ferroui_static_view_locator_build/
lib.rs1#![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
67pub const OUTPUT_DIRECTORY: &str = "static_view_locator";
70
71#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct GeneratedFile {
74 pub name: String,
76 pub text: String,
77}
78
79#[derive(Clone, Debug, Default)]
81pub struct Outcome {
82 pub lines: Vec<String>,
84 pub diagnostics: Vec<Diagnostic>,
86 pub files: Vec<GeneratedFile>,
88}
89
90impl Outcome {
91 pub fn errors(&self) -> impl Iterator<Item = &Diagnostic> {
93 self.diagnostics.iter().filter(|diagnostic| diagnostic.severity == Severity::Error)
94 }
95
96 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
102pub 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 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 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 pub fn crate_name(mut self, name: &str) -> Self {
153 self.crate_name = name.replace('-', "_");
154 self
155 }
156
157 pub fn source_root(mut self, file: &str) -> Self {
160 self.source_root = Some(PathBuf::from(file));
161 self
162 }
163
164 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 pub fn include_internal_view_models(mut self, include: bool) -> Self {
173 self.options.include_internal_view_models = include;
174 self
175 }
176
177 pub fn include_referenced_crates(mut self, include: bool) -> Self {
180 self.options.include_referenced_crates = include;
181 self
182 }
183
184 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 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 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 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 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 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 pub fn options(mut self, options: GeneratorOptions) -> Self {
226 self.options = options;
227 self
228 }
229
230 pub fn reactiveui_available(mut self, available: bool) -> Self {
233 self.reactiveui_available = available;
234 self
235 }
236
237 pub fn write_files(mut self, write: bool) -> Self {
240 self.write_files = write;
241 self
242 }
243
244 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 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 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 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}