1#![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")]
45#![warn(missing_docs)]
46
47#[cfg(not(feature = "default"))]
48compile_error!(
49 "The feature `default` must be enabled to ensure \
50 forward compatibility with future version of this crate"
51);
52
53use std::collections::HashMap;
54use std::env;
55use std::io::{BufWriter, Write};
56use std::path::Path;
57
58use i_slint_compiler::diagnostics::BuildDiagnostics;
59
60#[derive(Clone)]
62pub struct CompilerConfiguration {
63 config: i_slint_compiler::CompilerConfiguration,
64}
65
66#[derive(Clone, PartialEq)]
70pub enum EmbedResourcesKind {
71 AsAbsolutePath,
74 EmbedFiles,
76 EmbedForSoftwareRenderer,
79}
80
81impl Default for CompilerConfiguration {
82 fn default() -> Self {
83 Self {
84 config: i_slint_compiler::CompilerConfiguration::new(
85 i_slint_compiler::generator::OutputFormat::Rust,
86 ),
87 }
88 }
89}
90
91impl CompilerConfiguration {
92 pub fn new() -> Self {
94 Self::default()
95 }
96
97 #[must_use]
100 pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
101 let mut config = self.config;
102 config.include_paths = include_paths;
103 Self { config }
104 }
105
106 #[must_use]
133 pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
134 let mut config = self.config;
135 config.library_paths = library_paths;
136 Self { config }
137 }
138
139 #[must_use]
141 pub fn with_style(self, style: String) -> Self {
142 let mut config = self.config;
143 config.style = Some(style);
144 Self { config }
145 }
146
147 #[must_use]
151 pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
152 let mut config = self.config;
153 config.embed_resources = match kind {
154 EmbedResourcesKind::AsAbsolutePath => {
155 i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
156 }
157 EmbedResourcesKind::EmbedFiles => {
158 i_slint_compiler::EmbedResourcesKind::EmbedAllResources
159 }
160 EmbedResourcesKind::EmbedForSoftwareRenderer => {
161 i_slint_compiler::EmbedResourcesKind::EmbedTextures
162 }
163 };
164 Self { config }
165 }
166
167 #[must_use]
172 pub fn with_scale_factor(self, factor: f32) -> Self {
173 let mut config = self.config;
174 config.const_scale_factor = factor as f64;
175 Self { config }
176 }
177
178 #[must_use]
187 pub fn with_bundled_translations(
188 self,
189 path: impl Into<std::path::PathBuf>,
190 ) -> CompilerConfiguration {
191 let mut config = self.config;
192 config.translation_path_bundle = Some(path.into());
193 Self { config }
194 }
195
196 #[doc(hidden)]
201 #[must_use]
202 pub fn with_debug_info(self, enable: bool) -> Self {
203 let mut config = self.config;
204 config.debug_info = enable;
205 Self { config }
206 }
207
208 #[cfg(feature = "sdf-fonts")]
219 #[must_use]
220 pub fn with_sdf_fonts(self, enable: bool) -> Self {
221 let mut config = self.config;
222 config.use_sdf_fonts = enable;
223 Self { config }
224 }
225}
226
227#[derive(derive_more::Error, derive_more::Display, Debug)]
229#[non_exhaustive]
230pub enum CompileError {
231 #[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
233 NotRunViaCargo,
234 #[display("{_0:?}")]
236 CompileError(#[error(not(source))] Vec<String>),
237 #[display("Cannot write the generated file: {_0}")]
239 SaveError(std::io::Error),
240}
241
242struct CodeFormatter<Sink> {
243 indentation: usize,
244 in_string: bool,
246 in_char: usize,
248 escaped: bool,
250 sink: Sink,
251}
252
253impl<Sink> CodeFormatter<Sink> {
254 pub fn new(sink: Sink) -> Self {
255 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
256 }
257}
258
259impl<Sink: Write> Write for CodeFormatter<Sink> {
260 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
261 let len = s.len();
262 while let Some(idx) = s.iter().position(|c| match c {
263 b'{' if !self.in_string && self.in_char == 0 => {
264 self.indentation += 1;
265 true
266 }
267 b'}' if !self.in_string && self.in_char == 0 => {
268 self.indentation -= 1;
269 true
270 }
271 b';' if !self.in_string && self.in_char == 0 => true,
272 b'"' if !self.in_string && self.in_char == 0 => {
273 self.in_string = true;
274 self.escaped = false;
275 false
276 }
277 b'"' if self.in_string && !self.escaped => {
278 self.in_string = false;
279 false
280 }
281 b'\'' if !self.in_string && self.in_char == 0 => {
282 self.in_char = 1;
283 self.escaped = false;
284 false
285 }
286 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
287 self.in_char = 0;
288 false
289 }
290 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
291 self.in_char = 0;
293 false
294 }
295 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
296 self.escaped = true;
297 false
299 }
300 _ if self.in_char > 0 => {
301 self.in_char += 1;
302 self.escaped = false;
303 false
304 }
305 _ => {
306 self.escaped = false;
307 false
308 }
309 }) {
310 let idx = idx + 1;
311 self.sink.write_all(&s[..idx])?;
312 self.sink.write_all(b"\n")?;
313 for _ in 0..self.indentation {
314 self.sink.write_all(b" ")?;
315 }
316 s = &s[idx..];
317 }
318 self.sink.write_all(s)?;
319 Ok(len)
320 }
321 fn flush(&mut self) -> std::io::Result<()> {
322 self.sink.flush()
323 }
324}
325
326#[test]
327fn formatter_test() {
328 fn format_code(code: &str) -> String {
329 let mut res = Vec::new();
330 let mut formatter = CodeFormatter::new(&mut res);
331 formatter.write_all(code.as_bytes()).unwrap();
332 String::from_utf8(res).unwrap()
333 }
334
335 assert_eq!(
336 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
337 r#"fn main() {
338 if ';' == '}' {
339 return ";";
340 }
341 else {
342 panic!() }
343 }
344"#
345 );
346
347 assert_eq!(
348 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
349 r#"fn xx<'lt>(foo: &'lt str) {
350 println!("{}", '\u{f700}');
351 return Ok(());
352 }
353"#
354 );
355
356 assert_eq!(
357 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
358 r#"fn main() {
359 "";
360 "'";
361 "\"";
362 "{}";
363 "\\";
364 "\\\"";
365 }
366"#
367 );
368
369 assert_eq!(
370 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
371 r#"fn main() {
372 '"';
373 '\'';
374 '{';
375 '}';
376 '\\';
377 }
378"#
379 );
380}
381
382pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
405 compile_with_config(path, CompilerConfiguration::default())
406}
407
408pub fn compile_with_config(
418 relative_slint_file_path: impl AsRef<std::path::Path>,
419 config: CompilerConfiguration,
420) -> Result<(), CompileError> {
421 let path = Path::new(&env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?)
422 .join(relative_slint_file_path.as_ref());
423
424 let absolute_rust_output_file_path =
425 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
426 path.file_stem()
427 .map(Path::new)
428 .unwrap_or_else(|| Path::new("slint_out"))
429 .with_extension("rs"),
430 );
431
432 let paths_dependencies =
433 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
434
435 for path_dependency in paths_dependencies {
436 println!("cargo:rerun-if-changed={}", path_dependency.display());
437 }
438
439 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
440 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
441 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
442 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
443 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
444 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
445
446 println!(
447 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
448 absolute_rust_output_file_path.display()
449 );
450
451 Ok(())
452}
453
454pub fn compile_with_output_path(
464 input_slint_file_path: impl AsRef<std::path::Path>,
465 output_rust_file_path: impl AsRef<std::path::Path>,
466 config: CompilerConfiguration,
467) -> Result<Vec<std::path::PathBuf>, CompileError> {
468 let mut diag = BuildDiagnostics::default();
469 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
470
471 if diag.has_errors() {
472 let vec = diag.to_string_vec();
473 diag.print();
474 return Err(CompileError::CompileError(vec));
475 }
476
477 let mut compiler_config = config.config;
478 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
479
480 let syntax_node = syntax_node.expect("diags contained no compilation errors");
481
482 let (doc, diag, loader) =
484 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
485
486 if diag.has_errors() {
487 let vec = diag.to_string_vec();
488 diag.print();
489 return Err(CompileError::CompileError(vec));
490 }
491
492 let output_file =
493 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
494 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
495 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
496 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
497
498 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
499
500 for x in &diag.all_loaded_files {
501 if x.is_absolute() {
502 dependencies.push(x.clone());
503 }
504 }
505
506 diag.diagnostics_as_string().lines().for_each(|w| {
508 if !w.is_empty() {
509 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
510 }
511 });
512
513 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
514 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
515
516 for resource in doc.embedded_file_resources.borrow().keys() {
517 if !resource.starts_with("builtin:") {
518 dependencies.push(Path::new(resource).to_path_buf());
519 }
520 }
521
522 Ok(dependencies)
523}
524
525pub fn print_rustc_flags() -> std::io::Result<()> {
528 if let Some(board_config_path) =
529 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
530 {
531 let config = std::fs::read_to_string(board_config_path.as_path())?;
532 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
533
534 for link_arg in
535 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
536 {
537 if let Some(option) = link_arg.as_str() {
538 println!("cargo:rustc-link-arg={option}");
539 }
540 }
541
542 for link_search_path in
543 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
544 {
545 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
546 if path.is_relative() {
547 path = board_config_path.parent().unwrap().join(path);
548 }
549 println!("cargo:rustc-link-search={}", path.to_string_lossy());
550 }
551 }
552 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
553 println!("cargo:rerun-if-changed={}", board_config_path.display());
554 }
555
556 Ok(())
557}