1#![cfg_attr(
47 feature = "document-features",
48 doc = concat!("## Feature flags\n\n", document_features::document_features!())
49)]
50#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
51#![warn(missing_docs)]
52
53#[cfg(not(feature = "compat-1-18"))]
54compile_error!(
55 "The feature `compat-1-18` must be enabled to ensure \
56 forward compatibility with future version of this crate"
57);
58
59use std::collections::HashMap;
60use std::env;
61use std::io::{BufWriter, Write};
62use std::path::Path;
63
64use i_slint_compiler::diagnostics::BuildDiagnostics;
65
66pub use i_slint_compiler::DefaultTranslationContext;
69
70#[derive(Clone)]
72pub struct CompilerConfiguration {
73 config: i_slint_compiler::CompilerConfiguration,
74}
75
76#[derive(Clone, PartialEq)]
80pub enum EmbedResourcesKind {
81 AsAbsolutePath,
86 EmbedFiles,
89 #[cfg(feature = "renderer-software")]
90 EmbedForSoftwareRenderer,
96}
97
98impl Default for CompilerConfiguration {
99 fn default() -> Self {
100 Self {
101 config: i_slint_compiler::CompilerConfiguration::new(
102 i_slint_compiler::generator::OutputFormat::Rust,
103 ),
104 }
105 }
106}
107
108impl CompilerConfiguration {
109 pub fn new() -> Self {
111 Self::default()
112 }
113
114 #[must_use]
117 pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
118 let mut config = self.config;
119 config.include_paths = include_paths;
120 Self { config }
121 }
122
123 #[must_use]
150 pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
151 let mut config = self.config;
152 config.library_paths = library_paths;
153 Self { config }
154 }
155
156 #[must_use]
158 pub fn with_style(self, style: String) -> Self {
159 let mut config = self.config;
160 config.style = Some(style);
161 Self { config }
162 }
163
164 #[must_use]
168 pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
169 let mut config = self.config;
170 config.embed_resources = match kind {
171 EmbedResourcesKind::AsAbsolutePath => {
172 i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
173 }
174 EmbedResourcesKind::EmbedFiles => {
175 i_slint_compiler::EmbedResourcesKind::EmbedAllResources
176 }
177 #[cfg(feature = "renderer-software")]
178 EmbedResourcesKind::EmbedForSoftwareRenderer => {
179 i_slint_compiler::EmbedResourcesKind::EmbedTextures
180 }
181 };
182 Self { config }
183 }
184
185 #[must_use]
192 pub fn with_scale_factor(mut self, factor: f32) -> Self {
193 self.config.const_scale_factor = Some(factor);
194 self
195 }
196
197 #[must_use]
206 pub fn with_bundled_translations(
207 self,
208 path: impl Into<std::path::PathBuf>,
209 ) -> CompilerConfiguration {
210 let mut config = self.config;
211 config.translation_path_bundle = Some(path.into());
212 Self { config }
213 }
214
215 #[must_use]
221 pub fn with_default_translation_context(
222 mut self,
223 default_translation_context: DefaultTranslationContext,
224 ) -> Self {
225 self.config.default_translation_context = default_translation_context;
226 self
227 }
228
229 #[doc(hidden)]
234 #[must_use]
235 pub fn with_debug_info(self, enable: bool) -> Self {
236 let mut config = self.config;
237 config.debug_info = enable;
238 Self { config }
239 }
240
241 #[cfg(feature = "experimental-module-builds")]
248 #[must_use]
249 pub fn as_library(self, library_name: &str) -> Self {
250 let mut config = self.config;
251 config.library_name = Some(library_name.to_string());
252 Self { config }
253 }
254
255 #[cfg(feature = "experimental-module-builds")]
259 #[must_use]
260 pub fn rust_module(self, rust_module: &str) -> Self {
261 let mut config = self.config;
262 config.rust_module = Some(rust_module.to_string());
263 Self { config }
264 }
265 #[cfg(feature = "sdf-fonts")]
276 #[must_use]
277 pub fn with_sdf_fonts(self, enable: bool) -> Self {
278 let mut config = self.config;
279 config.use_sdf_fonts = enable;
280 Self { config }
281 }
282
283 #[must_use]
285 fn with_absolute_paths(self, manifest_dir: &std::path::Path) -> Self {
286 let mut config = self.config;
287
288 let to_absolute_path = |path: &mut std::path::PathBuf| {
289 if path.is_relative() {
290 *path = manifest_dir.join(&path);
291 }
292 };
293
294 for path in config.library_paths.values_mut() {
295 to_absolute_path(path);
296 }
297
298 for path in config.include_paths.iter_mut() {
299 to_absolute_path(path);
300 }
301
302 if let Some(path) = config.translation_path_bundle.as_mut() {
303 to_absolute_path(path);
304 }
305
306 Self { config }
307 }
308}
309
310#[derive(derive_more::Error, derive_more::Display, Debug)]
312#[non_exhaustive]
313pub enum CompileError {
314 #[display(
316 "Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo."
317 )]
318 NotRunViaCargo,
319 #[display("{_0:?}")]
321 CompileError(#[error(not(source))] Vec<String>),
322 #[display("Cannot write the generated file: {_0}")]
324 SaveError(std::io::Error),
325}
326
327struct CodeFormatter<Sink> {
328 indentation: usize,
329 in_string: bool,
331 in_char: usize,
333 escaped: bool,
335 sink: Sink,
336}
337
338impl<Sink> CodeFormatter<Sink> {
339 pub fn new(sink: Sink) -> Self {
340 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
341 }
342}
343
344impl<Sink: Write> Write for CodeFormatter<Sink> {
345 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
346 let len = s.len();
347 while let Some(idx) = s.iter().position(|c| match c {
348 b'{' if !self.in_string && self.in_char == 0 => {
349 self.indentation += 1;
350 true
351 }
352 b'}' if !self.in_string && self.in_char == 0 => {
353 self.indentation -= 1;
354 true
355 }
356 b';' if !self.in_string && self.in_char == 0 => true,
357 b'"' if !self.in_string && self.in_char == 0 => {
358 self.in_string = true;
359 self.escaped = false;
360 false
361 }
362 b'"' if self.in_string && !self.escaped => {
363 self.in_string = false;
364 false
365 }
366 b'\'' if !self.in_string && self.in_char == 0 => {
367 self.in_char = 1;
368 self.escaped = false;
369 false
370 }
371 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
372 self.in_char = 0;
373 false
374 }
375 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
376 self.in_char = 0;
378 false
379 }
380 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
381 self.escaped = true;
382 false
384 }
385 _ if self.in_char > 0 => {
386 self.in_char += 1;
387 self.escaped = false;
388 false
389 }
390 _ => {
391 self.escaped = false;
392 false
393 }
394 }) {
395 let idx = idx + 1;
396 self.sink.write_all(&s[..idx])?;
397 self.sink.write_all(b"\n")?;
398 for _ in 0..self.indentation {
399 self.sink.write_all(b" ")?;
400 }
401 s = &s[idx..];
402 }
403 self.sink.write_all(s)?;
404 Ok(len)
405 }
406 fn flush(&mut self) -> std::io::Result<()> {
407 self.sink.flush()
408 }
409}
410
411#[test]
412fn formatter_test() {
413 fn format_code(code: &str) -> String {
414 let mut res = Vec::new();
415 let mut formatter = CodeFormatter::new(&mut res);
416 formatter.write_all(code.as_bytes()).unwrap();
417 String::from_utf8(res).unwrap()
418 }
419
420 assert_eq!(
421 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
422 r#"fn main() {
423 if ';' == '}' {
424 return ";";
425 }
426 else {
427 panic!() }
428 }
429"#
430 );
431
432 assert_eq!(
433 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
434 r#"fn xx<'lt>(foo: &'lt str) {
435 println!("{}", '\u{f700}');
436 return Ok(());
437 }
438"#
439 );
440
441 assert_eq!(
442 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
443 r#"fn main() {
444 "";
445 "'";
446 "\"";
447 "{}";
448 "\\";
449 "\\\"";
450 }
451"#
452 );
453
454 assert_eq!(
455 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
456 r#"fn main() {
457 '"';
458 '\'';
459 '{';
460 '}';
461 '\\';
462 }
463"#
464 );
465}
466
467pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
492 compile_with_config(path, CompilerConfiguration::default())
493}
494
495pub fn compile_with_config(
505 relative_slint_file_path: impl AsRef<std::path::Path>,
506 config: CompilerConfiguration,
507) -> Result<(), CompileError> {
508 let manifest_path = std::path::PathBuf::from(
509 env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?,
510 );
511 let config = config.with_absolute_paths(&manifest_path);
512
513 let path = manifest_path.join(relative_slint_file_path.as_ref());
514
515 let absolute_rust_output_file_path =
516 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
517 path.file_stem()
518 .map(Path::new)
519 .unwrap_or_else(|| Path::new("slint_out"))
520 .with_extension("rs"),
521 );
522
523 #[cfg(feature = "experimental-module-builds")]
524 if let Some(library_name) = config.config.library_name.clone() {
525 println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
526 println!(
527 "cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
528 std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
529 );
530 println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
531 if let Some(rust_module) = &config.config.rust_module {
532 println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
533 }
534 }
535 if let Some(bundle_path) = &config.config.translation_path_bundle {
537 println!("cargo:rerun-if-changed={}", bundle_path.display());
538 }
539
540 let paths_dependencies =
541 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
542
543 for path_dependency in paths_dependencies {
544 println!("cargo:rerun-if-changed={}", path_dependency.display());
545 }
546
547 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
548 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
549 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
550 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
551 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
552 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
553 println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
554 println!("cargo:rerun-if-env-changed=SLINT_BUNDLE_TRANSLATIONS");
555
556 println!(
557 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
558 absolute_rust_output_file_path.display()
559 );
560
561 Ok(())
562}
563
564pub fn compile_with_output_path(
574 input_slint_file_path: impl AsRef<std::path::Path>,
575 output_rust_file_path: impl AsRef<std::path::Path>,
576 config: CompilerConfiguration,
577) -> Result<Vec<std::path::PathBuf>, CompileError> {
578 let mut diag = BuildDiagnostics::default();
579 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
580
581 if diag.has_errors() {
582 let vec = diag.to_string_vec();
583 diag.print();
584 return Err(CompileError::CompileError(vec));
585 }
586
587 let mut compiler_config = config.config;
588 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
589
590 let syntax_node = syntax_node.expect("diags contained no compilation errors");
591
592 let (doc, diag, loader) =
594 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
595
596 if diag.has_errors()
597 || (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
598 {
599 let vec = diag.to_string_vec();
600 diag.print();
601 return Err(CompileError::CompileError(vec));
602 }
603
604 let output_file =
605 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
606 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
607 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
608 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
609
610 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
611
612 for x in &diag.all_loaded_files {
613 if x.is_absolute() {
614 dependencies.push(x.clone());
615 }
616 }
617
618 diag.diagnostics_as_string().lines().for_each(|w| {
620 if !w.is_empty() {
621 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
622 }
623 });
624
625 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
626 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
627
628 for er in doc.embedded_file_resources.borrow().iter() {
629 if let Some(resource) = er.path.as_deref()
630 && !resource.starts_with("builtin:")
631 {
632 dependencies.push(Path::new(resource).to_path_buf());
633 }
634 }
635
636 code_formatter.sink.flush().map_err(CompileError::SaveError)?;
637
638 Ok(dependencies)
639}
640
641pub fn print_rustc_flags() -> std::io::Result<()> {
644 if let Some(board_config_path) =
645 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
646 {
647 let config = std::fs::read_to_string(board_config_path.as_path())?;
648 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
649
650 for link_arg in
651 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
652 {
653 if let Some(option) = link_arg.as_str() {
654 println!("cargo:rustc-link-arg={option}");
655 }
656 }
657
658 for link_search_path in
659 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
660 {
661 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
662 if path.is_relative() {
663 path = board_config_path.parent().unwrap().join(path);
664 }
665 println!("cargo:rustc-link-search={}", path.to_string_lossy());
666 }
667 }
668 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
669 println!("cargo:rerun-if-changed={}", board_config_path.display());
670 }
671
672 Ok(())
673}
674
675#[cfg(test)]
676fn root_path_prefix() -> std::path::PathBuf {
677 #[cfg(windows)]
678 return std::path::PathBuf::from("C:/");
679 #[cfg(not(windows))]
680 return std::path::PathBuf::from("/");
681}
682
683#[test]
684fn with_absolute_library_paths_test() {
685 use std::path::PathBuf;
686
687 let library_paths = std::collections::HashMap::from([
688 ("relative".to_string(), PathBuf::from("some/relative/path")),
689 ("absolute".to_string(), root_path_prefix().join("some/absolute/path")),
690 ]);
691 let config = CompilerConfiguration::new().with_library_paths(library_paths);
692
693 let manifest_path = root_path_prefix().join("path/to/manifest");
694 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
695 let relative = &absolute_config.config.library_paths["relative"];
696 assert!(relative.is_absolute());
697 assert!(relative.starts_with(&manifest_path));
698
699 assert!(!absolute_config.config.library_paths["absolute"].starts_with(&manifest_path));
700}
701
702#[test]
703fn with_absolute_include_paths_test() {
704 use std::path::PathBuf;
705
706 let config = CompilerConfiguration::new().with_include_paths(Vec::from([
707 root_path_prefix().join("some/absolute/path"),
708 PathBuf::from("some/relative/path"),
709 ]));
710
711 let manifest_path = root_path_prefix().join("path/to/manifest");
712 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
713 assert_eq!(
714 absolute_config.config.include_paths,
715 Vec::from([
716 root_path_prefix().join("some/absolute/path"),
717 manifest_path.join("some/relative/path"),
718 ])
719 )
720}