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