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 Self { config }
303 }
304}
305
306#[derive(derive_more::Error, derive_more::Display, Debug)]
308#[non_exhaustive]
309pub enum CompileError {
310 #[display(
312 "Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo."
313 )]
314 NotRunViaCargo,
315 #[display("{_0:?}")]
317 CompileError(#[error(not(source))] Vec<String>),
318 #[display("Cannot write the generated file: {_0}")]
320 SaveError(std::io::Error),
321}
322
323struct CodeFormatter<Sink> {
324 indentation: usize,
325 in_string: bool,
327 in_char: usize,
329 escaped: bool,
331 sink: Sink,
332}
333
334impl<Sink> CodeFormatter<Sink> {
335 pub fn new(sink: Sink) -> Self {
336 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
337 }
338}
339
340impl<Sink: Write> Write for CodeFormatter<Sink> {
341 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
342 let len = s.len();
343 while let Some(idx) = s.iter().position(|c| match c {
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 => {
349 self.indentation -= 1;
350 true
351 }
352 b';' if !self.in_string && self.in_char == 0 => true,
353 b'"' if !self.in_string && self.in_char == 0 => {
354 self.in_string = true;
355 self.escaped = false;
356 false
357 }
358 b'"' if self.in_string && !self.escaped => {
359 self.in_string = false;
360 false
361 }
362 b'\'' if !self.in_string && self.in_char == 0 => {
363 self.in_char = 1;
364 self.escaped = false;
365 false
366 }
367 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
368 self.in_char = 0;
369 false
370 }
371 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
372 self.in_char = 0;
374 false
375 }
376 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
377 self.escaped = true;
378 false
380 }
381 _ if self.in_char > 0 => {
382 self.in_char += 1;
383 self.escaped = false;
384 false
385 }
386 _ => {
387 self.escaped = false;
388 false
389 }
390 }) {
391 let idx = idx + 1;
392 self.sink.write_all(&s[..idx])?;
393 self.sink.write_all(b"\n")?;
394 for _ in 0..self.indentation {
395 self.sink.write_all(b" ")?;
396 }
397 s = &s[idx..];
398 }
399 self.sink.write_all(s)?;
400 Ok(len)
401 }
402 fn flush(&mut self) -> std::io::Result<()> {
403 self.sink.flush()
404 }
405}
406
407#[test]
408fn formatter_test() {
409 fn format_code(code: &str) -> String {
410 let mut res = Vec::new();
411 let mut formatter = CodeFormatter::new(&mut res);
412 formatter.write_all(code.as_bytes()).unwrap();
413 String::from_utf8(res).unwrap()
414 }
415
416 assert_eq!(
417 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
418 r#"fn main() {
419 if ';' == '}' {
420 return ";";
421 }
422 else {
423 panic!() }
424 }
425"#
426 );
427
428 assert_eq!(
429 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
430 r#"fn xx<'lt>(foo: &'lt str) {
431 println!("{}", '\u{f700}');
432 return Ok(());
433 }
434"#
435 );
436
437 assert_eq!(
438 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
439 r#"fn main() {
440 "";
441 "'";
442 "\"";
443 "{}";
444 "\\";
445 "\\\"";
446 }
447"#
448 );
449
450 assert_eq!(
451 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
452 r#"fn main() {
453 '"';
454 '\'';
455 '{';
456 '}';
457 '\\';
458 }
459"#
460 );
461}
462
463pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
488 compile_with_config(path, CompilerConfiguration::default())
489}
490
491pub fn compile_with_config(
501 relative_slint_file_path: impl AsRef<std::path::Path>,
502 config: CompilerConfiguration,
503) -> Result<(), CompileError> {
504 let manifest_path = std::path::PathBuf::from(
505 env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?,
506 );
507 let config = config.with_absolute_paths(&manifest_path);
508
509 let path = manifest_path.join(relative_slint_file_path.as_ref());
510
511 let absolute_rust_output_file_path =
512 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
513 path.file_stem()
514 .map(Path::new)
515 .unwrap_or_else(|| Path::new("slint_out"))
516 .with_extension("rs"),
517 );
518
519 #[cfg(feature = "experimental-module-builds")]
520 if let Some(library_name) = config.config.library_name.clone() {
521 println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
522 println!(
523 "cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
524 std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
525 );
526 println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
527 if let Some(rust_module) = &config.config.rust_module {
528 println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
529 }
530 }
531 let paths_dependencies =
532 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
533
534 for path_dependency in paths_dependencies {
535 println!("cargo:rerun-if-changed={}", path_dependency.display());
536 }
537
538 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
539 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
540 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
541 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
542 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
543 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
544 println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
545
546 println!(
547 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
548 absolute_rust_output_file_path.display()
549 );
550
551 Ok(())
552}
553
554pub fn compile_with_output_path(
564 input_slint_file_path: impl AsRef<std::path::Path>,
565 output_rust_file_path: impl AsRef<std::path::Path>,
566 config: CompilerConfiguration,
567) -> Result<Vec<std::path::PathBuf>, CompileError> {
568 let mut diag = BuildDiagnostics::default();
569 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
570
571 if diag.has_errors() {
572 let vec = diag.to_string_vec();
573 diag.print();
574 return Err(CompileError::CompileError(vec));
575 }
576
577 let mut compiler_config = config.config;
578 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
579
580 let syntax_node = syntax_node.expect("diags contained no compilation errors");
581
582 let (doc, diag, loader) =
584 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
585
586 if diag.has_errors()
587 || (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
588 {
589 let vec = diag.to_string_vec();
590 diag.print();
591 return Err(CompileError::CompileError(vec));
592 }
593
594 let output_file =
595 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
596 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
597 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
598 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
599
600 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
601
602 for x in &diag.all_loaded_files {
603 if x.is_absolute() {
604 dependencies.push(x.clone());
605 }
606 }
607
608 diag.diagnostics_as_string().lines().for_each(|w| {
610 if !w.is_empty() {
611 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
612 }
613 });
614
615 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
616 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
617
618 for er in doc.embedded_file_resources.borrow().iter() {
619 if let Some(resource) = er.path.as_deref()
620 && !resource.starts_with("builtin:")
621 {
622 dependencies.push(Path::new(resource).to_path_buf());
623 }
624 }
625
626 code_formatter.sink.flush().map_err(CompileError::SaveError)?;
627
628 Ok(dependencies)
629}
630
631pub fn print_rustc_flags() -> std::io::Result<()> {
634 if let Some(board_config_path) =
635 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
636 {
637 let config = std::fs::read_to_string(board_config_path.as_path())?;
638 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
639
640 for link_arg in
641 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
642 {
643 if let Some(option) = link_arg.as_str() {
644 println!("cargo:rustc-link-arg={option}");
645 }
646 }
647
648 for link_search_path in
649 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
650 {
651 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
652 if path.is_relative() {
653 path = board_config_path.parent().unwrap().join(path);
654 }
655 println!("cargo:rustc-link-search={}", path.to_string_lossy());
656 }
657 }
658 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
659 println!("cargo:rerun-if-changed={}", board_config_path.display());
660 }
661
662 Ok(())
663}
664
665#[cfg(test)]
666fn root_path_prefix() -> std::path::PathBuf {
667 #[cfg(windows)]
668 return std::path::PathBuf::from("C:/");
669 #[cfg(not(windows))]
670 return std::path::PathBuf::from("/");
671}
672
673#[test]
674fn with_absolute_library_paths_test() {
675 use std::path::PathBuf;
676
677 let library_paths = std::collections::HashMap::from([
678 ("relative".to_string(), PathBuf::from("some/relative/path")),
679 ("absolute".to_string(), root_path_prefix().join("some/absolute/path")),
680 ]);
681 let config = CompilerConfiguration::new().with_library_paths(library_paths);
682
683 let manifest_path = root_path_prefix().join("path/to/manifest");
684 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
685 let relative = &absolute_config.config.library_paths["relative"];
686 assert!(relative.is_absolute());
687 assert!(relative.starts_with(&manifest_path));
688
689 assert!(!absolute_config.config.library_paths["absolute"].starts_with(&manifest_path));
690}
691
692#[test]
693fn with_absolute_include_paths_test() {
694 use std::path::PathBuf;
695
696 let config = CompilerConfiguration::new().with_include_paths(Vec::from([
697 root_path_prefix().join("some/absolute/path"),
698 PathBuf::from("some/relative/path"),
699 ]));
700
701 let manifest_path = root_path_prefix().join("path/to/manifest");
702 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
703 assert_eq!(
704 absolute_config.config.include_paths,
705 Vec::from([
706 root_path_prefix().join("some/absolute/path"),
707 manifest_path.join("some/relative/path"),
708 ])
709 )
710}