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,
82 EmbedFiles,
85 EmbedForSoftwareRenderer,
91}
92
93impl Default for CompilerConfiguration {
94 fn default() -> Self {
95 Self {
96 config: i_slint_compiler::CompilerConfiguration::new(
97 i_slint_compiler::generator::OutputFormat::Rust,
98 ),
99 }
100 }
101}
102
103impl CompilerConfiguration {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 #[must_use]
112 pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
113 let mut config = self.config;
114 config.include_paths = include_paths;
115 Self { config }
116 }
117
118 #[must_use]
145 pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
146 let mut config = self.config;
147 config.library_paths = library_paths;
148 Self { config }
149 }
150
151 #[must_use]
153 pub fn with_style(self, style: String) -> Self {
154 let mut config = self.config;
155 config.style = Some(style);
156 Self { config }
157 }
158
159 #[must_use]
163 pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
164 let mut config = self.config;
165 config.embed_resources = match kind {
166 EmbedResourcesKind::AsAbsolutePath => {
167 i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
168 }
169 EmbedResourcesKind::EmbedFiles => {
170 i_slint_compiler::EmbedResourcesKind::EmbedAllResources
171 }
172 EmbedResourcesKind::EmbedForSoftwareRenderer => {
173 i_slint_compiler::EmbedResourcesKind::EmbedTextures
174 }
175 };
176 Self { config }
177 }
178
179 #[must_use]
186 pub fn with_scale_factor(mut self, factor: f32) -> Self {
187 self.config.const_scale_factor = Some(factor);
188 self
189 }
190
191 #[must_use]
200 pub fn with_bundled_translations(
201 self,
202 path: impl Into<std::path::PathBuf>,
203 ) -> CompilerConfiguration {
204 let mut config = self.config;
205 config.translation_path_bundle = Some(path.into());
206 Self { config }
207 }
208
209 #[must_use]
215 pub fn with_default_translation_context(
216 mut self,
217 default_translation_context: DefaultTranslationContext,
218 ) -> Self {
219 self.config.default_translation_context = default_translation_context;
220 self
221 }
222
223 #[doc(hidden)]
228 #[must_use]
229 pub fn with_debug_info(self, enable: bool) -> Self {
230 let mut config = self.config;
231 config.debug_info = enable;
232 Self { config }
233 }
234
235 #[cfg(feature = "experimental-module-builds")]
242 #[must_use]
243 pub fn as_library(self, library_name: &str) -> Self {
244 let mut config = self.config;
245 config.library_name = Some(library_name.to_string());
246 Self { config }
247 }
248
249 #[cfg(feature = "experimental-module-builds")]
253 #[must_use]
254 pub fn rust_module(self, rust_module: &str) -> Self {
255 let mut config = self.config;
256 config.rust_module = Some(rust_module.to_string());
257 Self { config }
258 }
259 #[cfg(feature = "sdf-fonts")]
270 #[must_use]
271 pub fn with_sdf_fonts(self, enable: bool) -> Self {
272 let mut config = self.config;
273 config.use_sdf_fonts = enable;
274 Self { config }
275 }
276
277 #[must_use]
279 fn with_absolute_paths(self, manifest_dir: &std::path::Path) -> Self {
280 let mut config = self.config;
281
282 let to_absolute_path = |path: &mut std::path::PathBuf| {
283 if path.is_relative() {
284 *path = manifest_dir.join(&path);
285 }
286 };
287
288 for path in config.library_paths.values_mut() {
289 to_absolute_path(path);
290 }
291
292 for path in config.include_paths.iter_mut() {
293 to_absolute_path(path);
294 }
295
296 Self { config }
297 }
298}
299
300#[derive(derive_more::Error, derive_more::Display, Debug)]
302#[non_exhaustive]
303pub enum CompileError {
304 #[display(
306 "Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo."
307 )]
308 NotRunViaCargo,
309 #[display("{_0:?}")]
311 CompileError(#[error(not(source))] Vec<String>),
312 #[display("Cannot write the generated file: {_0}")]
314 SaveError(std::io::Error),
315}
316
317struct CodeFormatter<Sink> {
318 indentation: usize,
319 in_string: bool,
321 in_char: usize,
323 escaped: bool,
325 sink: Sink,
326}
327
328impl<Sink> CodeFormatter<Sink> {
329 pub fn new(sink: Sink) -> Self {
330 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
331 }
332}
333
334impl<Sink: Write> Write for CodeFormatter<Sink> {
335 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
336 let len = s.len();
337 while let Some(idx) = s.iter().position(|c| match c {
338 b'{' if !self.in_string && self.in_char == 0 => {
339 self.indentation += 1;
340 true
341 }
342 b'}' if !self.in_string && self.in_char == 0 => {
343 self.indentation -= 1;
344 true
345 }
346 b';' if !self.in_string && self.in_char == 0 => true,
347 b'"' if !self.in_string && self.in_char == 0 => {
348 self.in_string = true;
349 self.escaped = false;
350 false
351 }
352 b'"' if self.in_string && !self.escaped => {
353 self.in_string = false;
354 false
355 }
356 b'\'' if !self.in_string && self.in_char == 0 => {
357 self.in_char = 1;
358 self.escaped = false;
359 false
360 }
361 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
362 self.in_char = 0;
363 false
364 }
365 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
366 self.in_char = 0;
368 false
369 }
370 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
371 self.escaped = true;
372 false
374 }
375 _ if self.in_char > 0 => {
376 self.in_char += 1;
377 self.escaped = false;
378 false
379 }
380 _ => {
381 self.escaped = false;
382 false
383 }
384 }) {
385 let idx = idx + 1;
386 self.sink.write_all(&s[..idx])?;
387 self.sink.write_all(b"\n")?;
388 for _ in 0..self.indentation {
389 self.sink.write_all(b" ")?;
390 }
391 s = &s[idx..];
392 }
393 self.sink.write_all(s)?;
394 Ok(len)
395 }
396 fn flush(&mut self) -> std::io::Result<()> {
397 self.sink.flush()
398 }
399}
400
401#[test]
402fn formatter_test() {
403 fn format_code(code: &str) -> String {
404 let mut res = Vec::new();
405 let mut formatter = CodeFormatter::new(&mut res);
406 formatter.write_all(code.as_bytes()).unwrap();
407 String::from_utf8(res).unwrap()
408 }
409
410 assert_eq!(
411 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
412 r#"fn main() {
413 if ';' == '}' {
414 return ";";
415 }
416 else {
417 panic!() }
418 }
419"#
420 );
421
422 assert_eq!(
423 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
424 r#"fn xx<'lt>(foo: &'lt str) {
425 println!("{}", '\u{f700}');
426 return Ok(());
427 }
428"#
429 );
430
431 assert_eq!(
432 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
433 r#"fn main() {
434 "";
435 "'";
436 "\"";
437 "{}";
438 "\\";
439 "\\\"";
440 }
441"#
442 );
443
444 assert_eq!(
445 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
446 r#"fn main() {
447 '"';
448 '\'';
449 '{';
450 '}';
451 '\\';
452 }
453"#
454 );
455}
456
457pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
482 compile_with_config(path, CompilerConfiguration::default())
483}
484
485pub fn compile_with_config(
495 relative_slint_file_path: impl AsRef<std::path::Path>,
496 config: CompilerConfiguration,
497) -> Result<(), CompileError> {
498 let manifest_path = std::path::PathBuf::from(
499 env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?,
500 );
501 let config = config.with_absolute_paths(&manifest_path);
502
503 let path = manifest_path.join(relative_slint_file_path.as_ref());
504
505 let absolute_rust_output_file_path =
506 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
507 path.file_stem()
508 .map(Path::new)
509 .unwrap_or_else(|| Path::new("slint_out"))
510 .with_extension("rs"),
511 );
512
513 #[cfg(feature = "experimental-module-builds")]
514 if let Some(library_name) = config.config.library_name.clone() {
515 println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
516 println!(
517 "cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
518 std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
519 );
520 println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
521 if let Some(rust_module) = &config.config.rust_module {
522 println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
523 }
524 }
525 let paths_dependencies =
526 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
527
528 for path_dependency in paths_dependencies {
529 println!("cargo:rerun-if-changed={}", path_dependency.display());
530 }
531
532 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
533 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
534 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
535 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
536 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
537 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
538 println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
539
540 println!(
541 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
542 absolute_rust_output_file_path.display()
543 );
544
545 Ok(())
546}
547
548pub fn compile_with_output_path(
558 input_slint_file_path: impl AsRef<std::path::Path>,
559 output_rust_file_path: impl AsRef<std::path::Path>,
560 config: CompilerConfiguration,
561) -> Result<Vec<std::path::PathBuf>, CompileError> {
562 let mut diag = BuildDiagnostics::default();
563 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
564
565 if diag.has_errors() {
566 let vec = diag.to_string_vec();
567 diag.print();
568 return Err(CompileError::CompileError(vec));
569 }
570
571 let mut compiler_config = config.config;
572 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
573
574 let syntax_node = syntax_node.expect("diags contained no compilation errors");
575
576 let (doc, diag, loader) =
578 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
579
580 if diag.has_errors()
581 || (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
582 {
583 let vec = diag.to_string_vec();
584 diag.print();
585 return Err(CompileError::CompileError(vec));
586 }
587
588 let output_file =
589 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
590 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
591 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
592 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
593
594 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
595
596 for x in &diag.all_loaded_files {
597 if x.is_absolute() {
598 dependencies.push(x.clone());
599 }
600 }
601
602 diag.diagnostics_as_string().lines().for_each(|w| {
604 if !w.is_empty() {
605 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
606 }
607 });
608
609 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
610 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
611
612 for er in doc.embedded_file_resources.borrow().iter() {
613 if let Some(resource) = er.path.as_deref()
614 && !resource.starts_with("builtin:")
615 {
616 dependencies.push(Path::new(resource).to_path_buf());
617 }
618 }
619
620 code_formatter.sink.flush().map_err(CompileError::SaveError)?;
621
622 Ok(dependencies)
623}
624
625pub fn print_rustc_flags() -> std::io::Result<()> {
628 if let Some(board_config_path) =
629 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
630 {
631 let config = std::fs::read_to_string(board_config_path.as_path())?;
632 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
633
634 for link_arg in
635 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
636 {
637 if let Some(option) = link_arg.as_str() {
638 println!("cargo:rustc-link-arg={option}");
639 }
640 }
641
642 for link_search_path in
643 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
644 {
645 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
646 if path.is_relative() {
647 path = board_config_path.parent().unwrap().join(path);
648 }
649 println!("cargo:rustc-link-search={}", path.to_string_lossy());
650 }
651 }
652 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
653 println!("cargo:rerun-if-changed={}", board_config_path.display());
654 }
655
656 Ok(())
657}
658
659#[cfg(test)]
660fn root_path_prefix() -> std::path::PathBuf {
661 #[cfg(windows)]
662 return std::path::PathBuf::from("C:/");
663 #[cfg(not(windows))]
664 return std::path::PathBuf::from("/");
665}
666
667#[test]
668fn with_absolute_library_paths_test() {
669 use std::path::PathBuf;
670
671 let library_paths = std::collections::HashMap::from([
672 ("relative".to_string(), PathBuf::from("some/relative/path")),
673 ("absolute".to_string(), root_path_prefix().join("some/absolute/path")),
674 ]);
675 let config = CompilerConfiguration::new().with_library_paths(library_paths);
676
677 let manifest_path = root_path_prefix().join("path/to/manifest");
678 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
679 let relative = &absolute_config.config.library_paths["relative"];
680 assert!(relative.is_absolute());
681 assert!(relative.starts_with(&manifest_path));
682
683 assert!(!absolute_config.config.library_paths["absolute"].starts_with(&manifest_path));
684}
685
686#[test]
687fn with_absolute_include_paths_test() {
688 use std::path::PathBuf;
689
690 let config = CompilerConfiguration::new().with_include_paths(Vec::from([
691 root_path_prefix().join("some/absolute/path"),
692 PathBuf::from("some/relative/path"),
693 ]));
694
695 let manifest_path = root_path_prefix().join("path/to/manifest");
696 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
697 assert_eq!(
698 absolute_config.config.include_paths,
699 Vec::from([
700 root_path_prefix().join("some/absolute/path"),
701 manifest_path.join("some/relative/path"),
702 ])
703 )
704}