slint_build/
lib.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4/*!
5This crate serves as a companion crate of the slint crate.
6It is meant to allow you to compile the `.slint` files from your `build.rs` script.
7
8The main entry point of this crate is the [`compile()`] function
9
10The generated code must be included in your crate by using the `slint::include_modules!()` macro.
11
12## Example
13
14In your Cargo.toml:
15
16```toml
17[package]
18...
19build = "build.rs"
20
21[dependencies]
22slint = "1.14"
23...
24
25[build-dependencies]
26slint-build = "1.14"
27```
28
29In the `build.rs` file:
30
31```ignore
32fn main() {
33    slint_build::compile("ui/hello.slint").unwrap();
34}
35```
36
37Then in your main file
38
39```ignore
40slint::include_modules!();
41fn main() {
42    HelloWorld::new().run();
43}
44```
45*/
46#![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/// The structure for configuring aspects of the compilation of `.slint` markup files to Rust.
63#[derive(Clone)]
64pub struct CompilerConfiguration {
65    config: i_slint_compiler::CompilerConfiguration,
66}
67
68/// How should the slint compiler embed images and fonts
69///
70/// Parameter of [`CompilerConfiguration::embed_resources()`]
71#[derive(Clone, PartialEq)]
72pub enum EmbedResourcesKind {
73    /// Paths specified in .slint files are made absolute and the absolute
74    /// paths will be used at run-time to load the resources from the file system.
75    AsAbsolutePath,
76    /// The raw files in .slint files are embedded in the application binary.
77    EmbedFiles,
78    /// File names specified in .slint files will be loaded by the Slint compiler,
79    /// optimized for use with the software renderer and embedded in the application binary.
80    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    /// Creates a new default configuration.
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Create a new configuration that includes sets the include paths used for looking up
100    /// `.slint` imports to the specified vector of paths.
101    #[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    /// Create a new configuration that sets the library paths used for looking up
109    /// `@library` imports to the specified map of paths.
110    ///
111    /// Each library path can either be a path to a `.slint` file or a directory.
112    /// If it's a file, the library is imported by its name prefixed by `@` (e.g.
113    /// `@example`). The specified file is the only entry-point for the library
114    /// and other files from the library won't be accessible from the outside.
115    /// If it's a directory, a specific file in that directory must be specified
116    /// when importing the library (e.g. `@example/widgets.slint`). This allows
117    /// exposing multiple entry-points for a single library.
118    ///
119    /// Compile `ui/main.slint` and specify an "example" library path:
120    /// ```rust,no_run
121    /// let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
122    /// let library_paths = std::collections::HashMap::from([(
123    ///     "example".to_string(),
124    ///     manifest_dir.join("third_party/example/ui/lib.slint"),
125    /// )]);
126    /// let config = slint_build::CompilerConfiguration::new().with_library_paths(library_paths);
127    /// slint_build::compile_with_config("ui/main.slint", config).unwrap();
128    /// ```
129    ///
130    /// Import the "example" library in `ui/main.slint`:
131    /// ```slint,ignore
132    /// import { Example } from "@example";
133    /// ```
134    #[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    /// Create a new configuration that selects the style to be used for widgets.
142    #[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    /// Selects how the resources such as images and font are processed.
150    ///
151    /// See [`EmbedResourcesKind`]
152    #[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    /// Sets the scale factor to be applied to all `px` to `phx` conversions
170    /// as constant value. This is only intended for MCU environments. Use
171    /// in combination with [`Self::embed_resources`] to pre-scale images and glyphs
172    /// accordingly.
173    #[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    /// Configures the compiler to bundle translations when compiling Slint code.
181    ///
182    /// It expects the path to be the root directory of the translation files.
183    ///
184    /// If given a relative path, it will be resolved relative to `$CARGO_MANIFEST_DIR`.
185    ///
186    /// The translation files should be in the gettext `.po` format and follow this pattern:
187    /// `<path>/<lang>/LC_MESSAGES/<crate>.po`
188    #[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    /// Configures the compiler to emit additional debug info when compiling Slint code.
199    ///
200    /// This is the equivalent to setting `SLINT_EMIT_DEBUG_INFO=1` and using the `slint!()` macro
201    /// and is primarily used by `i-slint-backend-testing`.
202    #[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    /// Configures the compiler to treat the Slint as part of a library.
211    ///
212    /// Use this when the components and types of the Slint code need
213    /// to be accessible from other modules.
214    ///
215    /// **Note**: This feature is experimental and may change or be removed in the future.
216    #[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    /// Specify the Rust module to place the generated code in.
225    ///
226    /// **Note**: This feature is experimental and may change or be removed in the future.
227    #[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    /// Configures the compiler to use Signed Distance Field (SDF) encoding for fonts.
235    ///
236    /// This flag only takes effect when `embed_resources` is set to [`EmbedResourcesKind::EmbedForSoftwareRenderer`],
237    /// and requires the `sdf-fonts` cargo feature to be enabled.
238    ///
239    /// [SDF](https://en.wikipedia.org/wiki/Signed_distance_function) reduces the binary size by
240    /// using an alternative representation for fonts, trading off some rendering quality
241    /// for a smaller binary footprint.
242    /// Rendering is slower and may result in slightly inferior visual output.
243    /// Use this on systems with limited flash memory.
244    #[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    /// Converts any relative include_paths or library_paths to absolute paths relative to the manifest_dir.
253    #[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/// Error returned by the `compile` function
276#[derive(derive_more::Error, derive_more::Display, Debug)]
277#[non_exhaustive]
278pub enum CompileError {
279    /// Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.
280    #[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
281    NotRunViaCargo,
282    /// Parse error. The error are printed in the stderr, and also are in the vector
283    #[display("{_0:?}")]
284    CompileError(#[error(not(source))] Vec<String>),
285    /// Cannot write the generated file
286    #[display("Cannot write the generated file: {_0}")]
287    SaveError(std::io::Error),
288}
289
290struct CodeFormatter<Sink> {
291    indentation: usize,
292    /// We are currently in a string
293    in_string: bool,
294    /// number of bytes after the last `'`, 0 if there was none
295    in_char: usize,
296    /// In string or char, and the previous character was `\\`
297    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                // probably a lifetime
340                self.in_char = 0;
341                false
342            }
343            b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
344                self.escaped = true;
345                // no need to increment in_char since \ isn't a single character
346                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
430/// Compile the `.slint` file and generate rust code for it.
431///
432/// The generated code code will be created in the directory specified by
433/// the `OUT` environment variable as it is expected for build script.
434///
435/// The following line need to be added within your crate in order to include
436/// the generated code.
437/// ```ignore
438/// slint::include_modules!();
439/// ```
440///
441/// The path is relative to the `CARGO_MANIFEST_DIR`.
442///
443/// In case of compilation error, the errors are shown in `stderr`, the error
444/// are also returned in the [`CompileError`] enum. You must `unwrap` the returned
445/// result to make sure that cargo make the compilation fail in case there were
446/// errors when generating the code.
447///
448/// Please check out the documentation of the `slint` crate for more information
449/// about how to use the generated code.
450///
451/// This function can only be called within a build script run by cargo.
452///
453/// See also [`compile_with_config()`] if you want to specify a configuration.
454pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
455    compile_with_config(path, CompilerConfiguration::default())
456}
457
458/// Same as [`compile`], but allow to specify a configuration.
459///
460/// Compile `ui/hello.slint` and select the "material" style:
461/// ```rust,no_run
462/// let config =
463///     slint_build::CompilerConfiguration::new()
464///     .with_style("material".into());
465/// slint_build::compile_with_config("ui/hello.slint", config).unwrap();
466/// ```
467pub 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
521/// Similar to [`compile_with_config`], but meant to be used independently of cargo.
522///
523/// Will compile the input file and write the result in the given output file.
524///
525/// Both input_slint_file_path and output_rust_file_path should be absolute paths.
526///
527/// Doesn't print any cargo messages.
528///
529/// Returns a list of all input files that were used to generate the output file. (dependencies)
530pub 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    // 'spin_on' is ok here because the compiler in single threaded and does not block if there is no blocking future
550    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    // print warnings
576    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
596/// This function is for use the application's build script, in order to print any device specific
597/// build flags reported by the backend
598pub 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}