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
62#[derive(Clone)]
64pub struct CompilerConfiguration {
65 config: i_slint_compiler::CompilerConfiguration,
66}
67
68#[derive(Clone, PartialEq)]
72pub enum EmbedResourcesKind {
73 AsAbsolutePath,
76 EmbedFiles,
78 EmbedForSoftwareRenderer,
81}
82
83impl Default for CompilerConfiguration {
84 fn default() -> Self {
85 Self {
86 config: i_slint_compiler::CompilerConfiguration::new(
87 i_slint_compiler::generator::OutputFormat::Rust,
88 ),
89 }
90 }
91}
92
93impl CompilerConfiguration {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 #[must_use]
102 pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
103 let mut config = self.config;
104 config.include_paths = include_paths;
105 Self { config }
106 }
107
108 #[must_use]
135 pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
136 let mut config = self.config;
137 config.library_paths = library_paths;
138 Self { config }
139 }
140
141 #[must_use]
143 pub fn with_style(self, style: String) -> Self {
144 let mut config = self.config;
145 config.style = Some(style);
146 Self { config }
147 }
148
149 #[must_use]
153 pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
154 let mut config = self.config;
155 config.embed_resources = match kind {
156 EmbedResourcesKind::AsAbsolutePath => {
157 i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
158 }
159 EmbedResourcesKind::EmbedFiles => {
160 i_slint_compiler::EmbedResourcesKind::EmbedAllResources
161 }
162 EmbedResourcesKind::EmbedForSoftwareRenderer => {
163 i_slint_compiler::EmbedResourcesKind::EmbedTextures
164 }
165 };
166 Self { config }
167 }
168
169 #[must_use]
174 pub fn with_scale_factor(self, factor: f32) -> Self {
175 let mut config = self.config;
176 config.const_scale_factor = factor as f64;
177 Self { config }
178 }
179
180 #[must_use]
189 pub fn with_bundled_translations(
190 self,
191 path: impl Into<std::path::PathBuf>,
192 ) -> CompilerConfiguration {
193 let mut config = self.config;
194 config.translation_path_bundle = Some(path.into());
195 Self { config }
196 }
197
198 #[doc(hidden)]
203 #[must_use]
204 pub fn with_debug_info(self, enable: bool) -> Self {
205 let mut config = self.config;
206 config.debug_info = enable;
207 Self { config }
208 }
209
210 #[cfg(feature = "experimental-module-builds")]
217 #[must_use]
218 pub fn as_library(self, library_name: &str) -> Self {
219 let mut config = self.config;
220 config.library_name = Some(library_name.to_string());
221 Self { config }
222 }
223
224 #[cfg(feature = "experimental-module-builds")]
228 #[must_use]
229 pub fn rust_module(self, rust_module: &str) -> Self {
230 let mut config = self.config;
231 config.rust_module = Some(rust_module.to_string());
232 Self { config }
233 }
234 #[cfg(feature = "sdf-fonts")]
245 #[must_use]
246 pub fn with_sdf_fonts(self, enable: bool) -> Self {
247 let mut config = self.config;
248 config.use_sdf_fonts = enable;
249 Self { config }
250 }
251
252 #[must_use]
254 fn with_absolute_paths(self, manifest_dir: &std::path::Path) -> Self {
255 let mut config = self.config;
256
257 let to_absolute_path = |path: &mut std::path::PathBuf| {
258 if path.is_relative() {
259 *path = manifest_dir.join(&path);
260 }
261 };
262
263 for path in config.library_paths.values_mut() {
264 to_absolute_path(path);
265 }
266
267 for path in config.include_paths.iter_mut() {
268 to_absolute_path(path);
269 }
270
271 Self { config }
272 }
273}
274
275#[derive(derive_more::Error, derive_more::Display, Debug)]
277#[non_exhaustive]
278pub enum CompileError {
279 #[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
281 NotRunViaCargo,
282 #[display("{_0:?}")]
284 CompileError(#[error(not(source))] Vec<String>),
285 #[display("Cannot write the generated file: {_0}")]
287 SaveError(std::io::Error),
288}
289
290struct CodeFormatter<Sink> {
291 indentation: usize,
292 in_string: bool,
294 in_char: usize,
296 escaped: bool,
298 sink: Sink,
299}
300
301impl<Sink> CodeFormatter<Sink> {
302 pub fn new(sink: Sink) -> Self {
303 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
304 }
305}
306
307impl<Sink: Write> Write for CodeFormatter<Sink> {
308 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
309 let len = s.len();
310 while let Some(idx) = s.iter().position(|c| match c {
311 b'{' if !self.in_string && self.in_char == 0 => {
312 self.indentation += 1;
313 true
314 }
315 b'}' if !self.in_string && self.in_char == 0 => {
316 self.indentation -= 1;
317 true
318 }
319 b';' if !self.in_string && self.in_char == 0 => true,
320 b'"' if !self.in_string && self.in_char == 0 => {
321 self.in_string = true;
322 self.escaped = false;
323 false
324 }
325 b'"' if self.in_string && !self.escaped => {
326 self.in_string = false;
327 false
328 }
329 b'\'' if !self.in_string && self.in_char == 0 => {
330 self.in_char = 1;
331 self.escaped = false;
332 false
333 }
334 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
335 self.in_char = 0;
336 false
337 }
338 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
339 self.in_char = 0;
341 false
342 }
343 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
344 self.escaped = true;
345 false
347 }
348 _ if self.in_char > 0 => {
349 self.in_char += 1;
350 self.escaped = false;
351 false
352 }
353 _ => {
354 self.escaped = false;
355 false
356 }
357 }) {
358 let idx = idx + 1;
359 self.sink.write_all(&s[..idx])?;
360 self.sink.write_all(b"\n")?;
361 for _ in 0..self.indentation {
362 self.sink.write_all(b" ")?;
363 }
364 s = &s[idx..];
365 }
366 self.sink.write_all(s)?;
367 Ok(len)
368 }
369 fn flush(&mut self) -> std::io::Result<()> {
370 self.sink.flush()
371 }
372}
373
374#[test]
375fn formatter_test() {
376 fn format_code(code: &str) -> String {
377 let mut res = Vec::new();
378 let mut formatter = CodeFormatter::new(&mut res);
379 formatter.write_all(code.as_bytes()).unwrap();
380 String::from_utf8(res).unwrap()
381 }
382
383 assert_eq!(
384 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
385 r#"fn main() {
386 if ';' == '}' {
387 return ";";
388 }
389 else {
390 panic!() }
391 }
392"#
393 );
394
395 assert_eq!(
396 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
397 r#"fn xx<'lt>(foo: &'lt str) {
398 println!("{}", '\u{f700}');
399 return Ok(());
400 }
401"#
402 );
403
404 assert_eq!(
405 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
406 r#"fn main() {
407 "";
408 "'";
409 "\"";
410 "{}";
411 "\\";
412 "\\\"";
413 }
414"#
415 );
416
417 assert_eq!(
418 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
419 r#"fn main() {
420 '"';
421 '\'';
422 '{';
423 '}';
424 '\\';
425 }
426"#
427 );
428}
429
430pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
455 compile_with_config(path, CompilerConfiguration::default())
456}
457
458pub fn compile_with_config(
468 relative_slint_file_path: impl AsRef<std::path::Path>,
469 config: CompilerConfiguration,
470) -> Result<(), CompileError> {
471 let manifest_path = std::path::PathBuf::from(
472 env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?,
473 );
474 let config = config.with_absolute_paths(&manifest_path);
475
476 let path = manifest_path.join(relative_slint_file_path.as_ref());
477
478 let absolute_rust_output_file_path =
479 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
480 path.file_stem()
481 .map(Path::new)
482 .unwrap_or_else(|| Path::new("slint_out"))
483 .with_extension("rs"),
484 );
485
486 #[cfg(feature = "experimental-module-builds")]
487 if let Some(library_name) = config.config.library_name.clone() {
488 println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
489 println!(
490 "cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
491 std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
492 );
493 println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
494 if let Some(rust_module) = &config.config.rust_module {
495 println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
496 }
497 }
498 let paths_dependencies =
499 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
500
501 for path_dependency in paths_dependencies {
502 println!("cargo:rerun-if-changed={}", path_dependency.display());
503 }
504
505 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
506 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
507 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
508 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
509 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
510 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
511 println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
512
513 println!(
514 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
515 absolute_rust_output_file_path.display()
516 );
517
518 Ok(())
519}
520
521pub fn compile_with_output_path(
531 input_slint_file_path: impl AsRef<std::path::Path>,
532 output_rust_file_path: impl AsRef<std::path::Path>,
533 config: CompilerConfiguration,
534) -> Result<Vec<std::path::PathBuf>, CompileError> {
535 let mut diag = BuildDiagnostics::default();
536 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
537
538 if diag.has_errors() {
539 let vec = diag.to_string_vec();
540 diag.print();
541 return Err(CompileError::CompileError(vec));
542 }
543
544 let mut compiler_config = config.config;
545 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
546
547 let syntax_node = syntax_node.expect("diags contained no compilation errors");
548
549 let (doc, diag, loader) =
551 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
552
553 if diag.has_errors()
554 || (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
555 {
556 let vec = diag.to_string_vec();
557 diag.print();
558 return Err(CompileError::CompileError(vec));
559 }
560
561 let output_file =
562 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
563 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
564 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
565 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
566
567 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
568
569 for x in &diag.all_loaded_files {
570 if x.is_absolute() {
571 dependencies.push(x.clone());
572 }
573 }
574
575 diag.diagnostics_as_string().lines().for_each(|w| {
577 if !w.is_empty() {
578 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
579 }
580 });
581
582 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
583 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
584
585 for resource in doc.embedded_file_resources.borrow().keys() {
586 if !resource.starts_with("builtin:") {
587 dependencies.push(Path::new(resource).to_path_buf());
588 }
589 }
590
591 code_formatter.sink.flush().map_err(CompileError::SaveError)?;
592
593 Ok(dependencies)
594}
595
596pub fn print_rustc_flags() -> std::io::Result<()> {
599 if let Some(board_config_path) =
600 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
601 {
602 let config = std::fs::read_to_string(board_config_path.as_path())?;
603 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
604
605 for link_arg in
606 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
607 {
608 if let Some(option) = link_arg.as_str() {
609 println!("cargo:rustc-link-arg={option}");
610 }
611 }
612
613 for link_search_path in
614 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
615 {
616 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
617 if path.is_relative() {
618 path = board_config_path.parent().unwrap().join(path);
619 }
620 println!("cargo:rustc-link-search={}", path.to_string_lossy());
621 }
622 }
623 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
624 println!("cargo:rerun-if-changed={}", board_config_path.display());
625 }
626
627 Ok(())
628}
629
630#[cfg(test)]
631fn root_path_prefix() -> std::path::PathBuf {
632 #[cfg(windows)]
633 return std::path::PathBuf::from("C:/");
634 #[cfg(not(windows))]
635 return std::path::PathBuf::from("/");
636}
637
638#[test]
639fn with_absolute_library_paths_test() {
640 use std::path::PathBuf;
641
642 let library_paths = std::collections::HashMap::from([
643 ("relative".to_string(), PathBuf::from("some/relative/path")),
644 ("absolute".to_string(), root_path_prefix().join("some/absolute/path")),
645 ]);
646 let config = CompilerConfiguration::new().with_library_paths(library_paths);
647
648 let manifest_path = root_path_prefix().join("path/to/manifest");
649 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
650 let relative = &absolute_config.config.library_paths["relative"];
651 assert!(relative.is_absolute());
652 assert!(relative.starts_with(&manifest_path));
653
654 assert!(!absolute_config.config.library_paths["absolute"].starts_with(&manifest_path));
655}
656
657#[test]
658fn with_absolute_include_paths_test() {
659 use std::path::PathBuf;
660
661 let config = CompilerConfiguration::new().with_include_paths(Vec::from([
662 root_path_prefix().join("some/absolute/path"),
663 PathBuf::from("some/relative/path"),
664 ]));
665
666 let manifest_path = root_path_prefix().join("path/to/manifest");
667 let absolute_config = config.clone().with_absolute_paths(&manifest_path);
668 assert_eq!(
669 absolute_config.config.include_paths,
670 Vec::from([
671 root_path_prefix().join("some/absolute/path"),
672 manifest_path.join("some/relative/path"),
673 ])
674 )
675}