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
60pub struct CompilerConfiguration {
62 config: i_slint_compiler::CompilerConfiguration,
63}
64
65#[derive(Clone, PartialEq)]
69pub enum EmbedResourcesKind {
70 AsAbsolutePath,
73 EmbedFiles,
75 EmbedForSoftwareRenderer,
78}
79
80impl Default for CompilerConfiguration {
81 fn default() -> Self {
82 Self {
83 config: i_slint_compiler::CompilerConfiguration::new(
84 i_slint_compiler::generator::OutputFormat::Rust,
85 ),
86 }
87 }
88}
89
90impl CompilerConfiguration {
91 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
99 pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
100 let mut config = self.config;
101 config.include_paths = include_paths;
102 Self { config }
103 }
104
105 #[must_use]
132 pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
133 let mut config = self.config;
134 config.library_paths = library_paths;
135 Self { config }
136 }
137
138 #[must_use]
140 pub fn with_style(self, style: String) -> Self {
141 let mut config = self.config;
142 config.style = Some(style);
143 Self { config }
144 }
145
146 #[must_use]
150 pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
151 let mut config = self.config;
152 config.embed_resources = match kind {
153 EmbedResourcesKind::AsAbsolutePath => {
154 i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
155 }
156 EmbedResourcesKind::EmbedFiles => {
157 i_slint_compiler::EmbedResourcesKind::EmbedAllResources
158 }
159 EmbedResourcesKind::EmbedForSoftwareRenderer => {
160 i_slint_compiler::EmbedResourcesKind::EmbedTextures
161 }
162 };
163 Self { config }
164 }
165
166 #[must_use]
171 pub fn with_scale_factor(self, factor: f32) -> Self {
172 let mut config = self.config;
173 config.const_scale_factor = factor as f64;
174 Self { config }
175 }
176
177 #[must_use]
184 pub fn with_bundled_translations(
185 self,
186 path: impl Into<std::path::PathBuf>,
187 ) -> CompilerConfiguration {
188 let mut config = self.config;
189 config.translation_path_bundle = Some(path.into());
190 Self { config }
191 }
192
193 #[cfg(feature = "sdf-fonts")]
204 #[must_use]
205 pub fn with_sdf_fonts(self, enable: bool) -> Self {
206 let mut config = self.config;
207 config.use_sdf_fonts = enable;
208 Self { config }
209 }
210}
211
212#[derive(derive_more::Error, derive_more::Display, Debug)]
214#[non_exhaustive]
215pub enum CompileError {
216 #[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
218 NotRunViaCargo,
219 #[display("{_0:?}")]
221 CompileError(#[error(not(source))] Vec<String>),
222 #[display("Cannot write the generated file: {_0}")]
224 SaveError(std::io::Error),
225}
226
227struct CodeFormatter<Sink> {
228 indentation: usize,
229 in_string: bool,
231 in_char: usize,
233 escaped: bool,
235 sink: Sink,
236}
237
238impl<Sink> CodeFormatter<Sink> {
239 pub fn new(sink: Sink) -> Self {
240 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
241 }
242}
243
244impl<Sink: Write> Write for CodeFormatter<Sink> {
245 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
246 let len = s.len();
247 while let Some(idx) = s.iter().position(|c| match c {
248 b'{' if !self.in_string && self.in_char == 0 => {
249 self.indentation += 1;
250 true
251 }
252 b'}' if !self.in_string && self.in_char == 0 => {
253 self.indentation -= 1;
254 true
255 }
256 b';' if !self.in_string && self.in_char == 0 => true,
257 b'"' if !self.in_string && self.in_char == 0 => {
258 self.in_string = true;
259 self.escaped = false;
260 false
261 }
262 b'"' if self.in_string && !self.escaped => {
263 self.in_string = false;
264 false
265 }
266 b'\'' if !self.in_string && self.in_char == 0 => {
267 self.in_char = 1;
268 self.escaped = false;
269 false
270 }
271 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
272 self.in_char = 0;
273 false
274 }
275 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
276 self.in_char = 0;
278 false
279 }
280 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
281 self.escaped = true;
282 false
284 }
285 _ if self.in_char > 0 => {
286 self.in_char += 1;
287 self.escaped = false;
288 false
289 }
290 _ => {
291 self.escaped = false;
292 false
293 }
294 }) {
295 let idx = idx + 1;
296 self.sink.write_all(&s[..idx])?;
297 self.sink.write_all(b"\n")?;
298 for _ in 0..self.indentation {
299 self.sink.write_all(b" ")?;
300 }
301 s = &s[idx..];
302 }
303 self.sink.write_all(s)?;
304 Ok(len)
305 }
306 fn flush(&mut self) -> std::io::Result<()> {
307 self.sink.flush()
308 }
309}
310
311#[test]
312fn formatter_test() {
313 fn format_code(code: &str) -> String {
314 let mut res = Vec::new();
315 let mut formatter = CodeFormatter::new(&mut res);
316 formatter.write_all(code.as_bytes()).unwrap();
317 String::from_utf8(res).unwrap()
318 }
319
320 assert_eq!(
321 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
322 r#"fn main() {
323 if ';' == '}' {
324 return ";";
325 }
326 else {
327 panic!() }
328 }
329"#
330 );
331
332 assert_eq!(
333 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
334 r#"fn xx<'lt>(foo: &'lt str) {
335 println!("{}", '\u{f700}');
336 return Ok(());
337 }
338"#
339 );
340
341 assert_eq!(
342 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
343 r#"fn main() {
344 "";
345 "'";
346 "\"";
347 "{}";
348 "\\";
349 "\\\"";
350 }
351"#
352 );
353
354 assert_eq!(
355 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
356 r#"fn main() {
357 '"';
358 '\'';
359 '{';
360 '}';
361 '\\';
362 }
363"#
364 );
365}
366
367pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
390 compile_with_config(path, CompilerConfiguration::default())
391}
392
393pub fn compile_with_config(
403 relative_slint_file_path: impl AsRef<std::path::Path>,
404 config: CompilerConfiguration,
405) -> Result<(), CompileError> {
406 let path = Path::new(&env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?)
407 .join(relative_slint_file_path.as_ref());
408
409 let absolute_rust_output_file_path =
410 Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
411 path.file_stem()
412 .map(Path::new)
413 .unwrap_or_else(|| Path::new("slint_out"))
414 .with_extension("rs"),
415 );
416
417 let paths_dependencies =
418 compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
419
420 for path_dependency in paths_dependencies {
421 println!("cargo:rerun-if-changed={}", path_dependency.display());
422 }
423
424 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
425 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
426 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
427 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
428 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
429 println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
430
431 println!(
432 "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
433 absolute_rust_output_file_path.display()
434 );
435
436 Ok(())
437}
438
439pub fn compile_with_output_path(
449 input_slint_file_path: impl AsRef<std::path::Path>,
450 output_rust_file_path: impl AsRef<std::path::Path>,
451 config: CompilerConfiguration,
452) -> Result<Vec<std::path::PathBuf>, CompileError> {
453 let mut diag = BuildDiagnostics::default();
454 let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
455
456 if diag.has_errors() {
457 let vec = diag.to_string_vec();
458 diag.print();
459 return Err(CompileError::CompileError(vec));
460 }
461
462 let mut compiler_config = config.config;
463 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
464
465 let syntax_node = syntax_node.expect("diags contained no compilation errors");
466
467 let (doc, diag, loader) =
469 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
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 output_file =
478 std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
479 let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
480 let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
481 .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
482
483 let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
484
485 for x in &diag.all_loaded_files {
486 if x.is_absolute() {
487 dependencies.push(x.clone());
488 }
489 }
490
491 diag.diagnostics_as_string().lines().for_each(|w| {
493 if !w.is_empty() {
494 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
495 }
496 });
497
498 write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
499 dependencies.push(input_slint_file_path.as_ref().to_path_buf());
500
501 for resource in doc.embedded_file_resources.borrow().keys() {
502 if !resource.starts_with("builtin:") {
503 dependencies.push(Path::new(resource).to_path_buf());
504 }
505 }
506
507 Ok(dependencies)
508}
509
510pub fn print_rustc_flags() -> std::io::Result<()> {
513 if let Some(board_config_path) =
514 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
515 {
516 let config = std::fs::read_to_string(board_config_path.as_path())?;
517 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
518
519 for link_arg in
520 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
521 {
522 if let Some(option) = link_arg.as_str() {
523 println!("cargo:rustc-link-arg={option}");
524 }
525 }
526
527 for link_search_path in
528 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
529 {
530 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
531 if path.is_relative() {
532 path = board_config_path.parent().unwrap().join(path);
533 }
534 println!("cargo:rustc-link-search={}", path.to_string_lossy());
535 }
536 }
537 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
538 println!("cargo:rerun-if-changed={}", board_config_path.display());
539 }
540
541 Ok(())
542}