1#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
47#![warn(missing_docs)]
48
49#[cfg(not(feature = "default"))]
50compile_error!(
51 "The feature `default` must be enabled to ensure \
52 forward compatibility with future version of this crate"
53);
54
55use std::collections::HashMap;
56use std::env;
57use std::io::{BufWriter, Write};
58use std::path::Path;
59
60use i_slint_compiler::diagnostics::BuildDiagnostics;
61
62pub use i_slint_compiler::DefaultTranslationContext;
65
66#[derive(Clone)]
68pub struct CompilerConfiguration {
69 config: i_slint_compiler::CompilerConfiguration,
70}
71
72#[derive(Clone, PartialEq)]
76pub enum EmbedResourcesKind {
77 AsAbsolutePath,
80 EmbedFiles,
82 EmbedForSoftwareRenderer,
85}
86
87impl Default for CompilerConfiguration {
88 fn default() -> Self {
89 Self {
90 config: i_slint_compiler::CompilerConfiguration::new(
91 i_slint_compiler::generator::OutputFormat::Rust,
92 ),
93 }
94 }
95}
96
97impl CompilerConfiguration {
98 pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use]
106 pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
107 let mut config = self.config;
108 config.include_paths = include_paths;
109 Self { config }
110 }
111
112 #[must_use]
139 pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
140 let mut config = self.config;
141 config.library_paths = library_paths;
142 Self { config }
143 }
144
145 #[must_use]
147 pub fn with_style(self, style: String) -> Self {
148 let mut config = self.config;
149 config.style = Some(style);
150 Self { config }
151 }
152
153 #[must_use]
157 pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
158 let mut config = self.config;
159 config.embed_resources = match kind {
160 EmbedResourcesKind::AsAbsolutePath => {
161 i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
162 }
163 EmbedResourcesKind::EmbedFiles => {
164 i_slint_compiler::EmbedResourcesKind::EmbedAllResources
165 }
166 EmbedResourcesKind::EmbedForSoftwareRenderer => {
167 i_slint_compiler::EmbedResourcesKind::EmbedTextures
168 }
169 };
170 Self { config }
171 }
172
173 #[must_use]
180 pub fn with_scale_factor(mut self, factor: f32) -> Self {
181 self.config.const_scale_factor = Some(factor);
182 self
183 }
184
185 #[must_use]
194 pub fn with_bundled_translations(
195 self,
196 path: impl Into<std::path::PathBuf>,
197 ) -> CompilerConfiguration {
198 let mut config = self.config;
199 config.translation_path_bundle = Some(path.into());
200 Self { config }
201 }
202
203 #[must_use]
209 pub fn with_default_translation_context(
210 mut self,
211 default_translation_context: DefaultTranslationContext,
212 ) -> Self {
213 self.config.default_translation_context = default_translation_context;
214 self
215 }
216
217 #[doc(hidden)]
222 #[must_use]
223 pub fn with_debug_info(self, enable: bool) -> Self {
224 let mut config = self.config;
225 config.debug_info = enable;
226 Self { config }
227 }
228
229 #[cfg(feature = "experimental-module-builds")]
236 #[must_use]
237 pub fn as_library(self, library_name: &str) -> Self {
238 let mut config = self.config;
239 config.library_name = Some(library_name.to_string());
240 Self { config }
241 }
242
243 #[cfg(feature = "experimental-module-builds")]
247 #[must_use]
248 pub fn rust_module(self, rust_module: &str) -> Self {
249 let mut config = self.config;
250 config.rust_module = Some(rust_module.to_string());
251 Self { config }
252 }
253 #[cfg(feature = "sdf-fonts")]
264 #[must_use]
265 pub fn with_sdf_fonts(self, enable: bool) -> Self {
266 let mut config = self.config;
267 config.use_sdf_fonts = enable;
268 Self { config }
269 }
270
271 #[must_use]
273 fn with_absolute_paths(self, manifest_dir: &std::path::Path) -> Self {
274 let mut config = self.config;
275
276 let to_absolute_path = |path: &mut std::path::PathBuf| {
277 if path.is_relative() {
278 *path = manifest_dir.join(&path);
279 }
280 };
281
282 for path in config.library_paths.values_mut() {
283 to_absolute_path(path);
284 }
285
286 for path in config.include_paths.iter_mut() {
287 to_absolute_path(path);
288 }
289
290 Self { config }
291 }
292}
293
294#[derive(derive_more::Error, derive_more::Display, Debug)]
296#[non_exhaustive]
297pub enum CompileError {
298 #[display(
300 "Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo."
301 )]
302 NotRunViaCargo,
303 #[display("{_0:?}")]
305 CompileError(#[error(not(source))] Vec<String>),
306 #[display("Cannot write the generated file: {_0}")]
308 SaveError(std::io::Error),
309}
310
311struct CodeFormatter<Sink> {
312 indentation: usize,
313 in_string: bool,
315 in_char: usize,
317 escaped: bool,
319 sink: Sink,
320}
321
322impl<Sink> CodeFormatter<Sink> {
323 pub fn new(sink: Sink) -> Self {
324 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
325 }
326}
327
328impl<Sink: Write> Write for CodeFormatter<Sink> {
329 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
330 let len = s.len();
331 while let Some(idx) = s.iter().position(|c| match c {
332 b'{' if !self.in_string && self.in_char == 0 => {
333 self.indentation += 1;
334 true
335 }
336 b'}' if !self.in_string && self.in_char == 0 => {
337 self.indentation -= 1;
338 true
339 }
340 b';' if !self.in_string && self.in_char == 0 => true,
341 b'"' if !self.in_string && self.in_char == 0 => {
342 self.in_string = true;
343 self.escaped = false;
344 false
345 }
346 b'"' if self.in_string && !self.escaped => {
347 self.in_string = false;
348 false
349 }
350 b'\'' if !self.in_string && self.in_char == 0 => {
351 self.in_char = 1;
352 self.escaped = false;
353 false
354 }
355 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
356 self.in_char = 0;
357 false
358 }
359 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
360 self.in_char = 0;
362 false
363 }
364 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
365 self.escaped = true;
366 false
368 }
369 _ if self.in_char > 0 => {
370 self.in_char += 1;
371 self.escaped = false;
372 false
373 }
374 _ => {
375 self.escaped = false;
376 false
377 }
378 }) {
379 let idx = idx + 1;
380 self.sink.write_all(&s[..idx])?;
381 self.sink.write_all(b"\n")?;
382 for _ in 0..self.indentation {
383 self.sink.write_all(b" ")?;
384 }
385 s = &s[idx..];
386 }
387 self.sink.write_all(s)?;
388 Ok(len)
389 }
390 fn flush(&mut self) -> std::io::Result<()> {
391 self.sink.flush()
392 }
393}
394
395#[test]
396fn formatter_test() {
397 fn format_code(code: &str) -> String {
398 let mut res = Vec::new();
399 let mut formatter = CodeFormatter::new(&mut res);
400 formatter.write_all(code.as_bytes()).unwrap();
401 String::from_utf8(res).unwrap()
402 }
403
404 assert_eq!(
405 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
406 r#"fn main() {
407 if ';' == '}' {
408 return ";";
409 }
410 else {
411 panic!() }
412 }
413"#
414 );
415
416 assert_eq!(
417 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
418 r#"fn xx<'lt>(foo: &'lt str) {
419 println!("{}", '\u{f700}');
420 return Ok(());
421 }
422"#
423 );
424
425 assert_eq!(
426 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
427 r#"fn main() {
428 "";
429 "'";
430 "\"";
431 "{}";
432 "\\";
433 "\\\"";
434 }
435"#
436 );
437
438 assert_eq!(
439 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
440 r#"fn main() {
441 '"';
442 '\'';
443 '{';
444 '}';
445 '\\';
446 }
447"#
448 );
449}
450
451pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
476 compile_with_config(path, CompilerConfiguration::default())
477}
478
479pub fn compile_with_config(
489 relative_slint_file_path: impl AsRef<std::path::Path>,
490 config: CompilerConfiguration,
491) -> Result<(), CompileError> {
492 let manifest_path = std::path::PathBuf::from(
493 env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?,
494 );
495 let config = config.with_absolute_paths(&manifest_path);
496
497 let path = manifest_path.join(relative_slint_file_path.as_ref());
498
499 let absolute_rust_output_file_path =
500 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
501 path.file_stem()
502 .map(Path::new)
503 .unwrap_or_else(|| Path::new("slint_out"))
504 .with_extension("rs"),
505 );
506
507 #[cfg(feature = "experimental-module-builds")]
508 if let Some(library_name) = config.config.library_name.clone() {
509 println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
510 println!(
511 "cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
512 std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
513 );
514 println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
515 if let Some(rust_module) = &config.config.rust_module {
516 println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
517 }
518 }
519 let paths_dependencies =
520 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
521
522 for path_dependency in paths_dependencies {
523 println!("cargo:rerun-if-changed={}", path_dependency.display());
524 }
525
526 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
527 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
528 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
529 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
530 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
531 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
532 println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
533
534 println!(
535 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
536 absolute_rust_output_file_path.display()
537 );
538
539 Ok(())
540}
541
542pub fn compile_with_output_path(
552 input_slint_file_path: impl AsRef<std::path::Path>,
553 output_rust_file_path: impl AsRef<std::path::Path>,
554 config: CompilerConfiguration,
555) -> Result<Vec<std::path::PathBuf>, CompileError> {
556 let mut diag = BuildDiagnostics::default();
557 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
558
559 if diag.has_errors() {
560 let vec = diag.to_string_vec();
561 diag.print();
562 return Err(CompileError::CompileError(vec));
563 }
564
565 let mut compiler_config = config.config;
566 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
567
568 let syntax_node = syntax_node.expect("diags contained no compilation errors");
569
570 let (doc, diag, loader) =
572 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
573
574 if diag.has_errors()
575 || (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
576 {
577 let vec = diag.to_string_vec();
578 diag.print();
579 return Err(CompileError::CompileError(vec));
580 }
581
582 let output_file =
583 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
584 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
585 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
586 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
587
588 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
589
590 for x in &diag.all_loaded_files {
591 if x.is_absolute() {
592 dependencies.push(x.clone());
593 }
594 }
595
596 diag.diagnostics_as_string().lines().for_each(|w| {
598 if !w.is_empty() {
599 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
600 }
601 });
602
603 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
604 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
605
606 for resource in doc.embedded_file_resources.borrow().keys() {
607 if !resource.starts_with("builtin:") {
608 dependencies.push(Path::new(resource).to_path_buf());
609 }
610 }
611
612 code_formatter.sink.flush().map_err(CompileError::SaveError)?;
613
614 Ok(dependencies)
615}
616
617pub fn print_rustc_flags() -> std::io::Result<()> {
620 if let Some(board_config_path) =
621 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
622 {
623 let config = std::fs::read_to_string(board_config_path.as_path())?;
624 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
625
626 for link_arg in
627 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
628 {
629 if let Some(option) = link_arg.as_str() {
630 println!("cargo:rustc-link-arg={option}");
631 }
632 }
633
634 for link_search_path in
635 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
636 {
637 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
638 if path.is_relative() {
639 path = board_config_path.parent().unwrap().join(path);
640 }
641 println!("cargo:rustc-link-search={}", path.to_string_lossy());
642 }
643 }
644 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
645 println!("cargo:rerun-if-changed={}", board_config_path.display());
646 }
647
648 Ok(())
649}
650
651#[cfg(test)]
652fn root_path_prefix() -> std::path::PathBuf {
653 #[cfg(windows)]
654 return std::path::PathBuf::from("C:/");
655 #[cfg(not(windows))]
656 return std::path::PathBuf::from("/");
657}
658
659#[test]
660fn with_absolute_library_paths_test() {
661 use std::path::PathBuf;
662
663 let library_paths = std::collections::HashMap::from([
664 ("relative".to_string(), PathBuf::from("some/relative/path")),
665 ("absolute".to_string(), root_path_prefix().join("some/absolute/path")),
666 ]);
667 let config = CompilerConfiguration::new().with_library_paths(library_paths);
668
669 let manifest_path = root_path_prefix().join("path/to/manifest");
670 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
671 let relative = &absolute_config.config.library_paths["relative"];
672 assert!(relative.is_absolute());
673 assert!(relative.starts_with(&manifest_path));
674
675 assert!(!absolute_config.config.library_paths["absolute"].starts_with(&manifest_path));
676}
677
678#[test]
679fn with_absolute_include_paths_test() {
680 use std::path::PathBuf;
681
682 let config = CompilerConfiguration::new().with_include_paths(Vec::from([
683 root_path_prefix().join("some/absolute/path"),
684 PathBuf::from("some/relative/path"),
685 ]));
686
687 let manifest_path = root_path_prefix().join("path/to/manifest");
688 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
689 assert_eq!(
690 absolute_config.config.include_paths,
691 Vec::from([
692 root_path_prefix().join("some/absolute/path"),
693 manifest_path.join("some/relative/path"),
694 ])
695 )
696}