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
10## Example
11
12In your Cargo.toml:
13
14```toml
15[package]
16...
17build = "build.rs"
18
19[dependencies]
20slint = "1.9.0"
21...
22
23[build-dependencies]
24slint-build = "1.9.0"
25```
26
27In the `build.rs` file:
28
29```ignore
30fn main() {
31    slint_build::compile("ui/hello.slint").unwrap();
32}
33```
34
35Then in your main file
36
37```ignore
38slint::include_modules!();
39fn main() {
40    HelloWorld::new().run();
41}
42```
43*/
44#![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/// The structure for configuring aspects of the compilation of `.slint` markup files to Rust.
61pub struct CompilerConfiguration {
62    config: i_slint_compiler::CompilerConfiguration,
63}
64
65/// How should the slint compiler embed images and fonts
66///
67/// Parameter of [`CompilerConfiguration::embed_resources()`]
68#[derive(Clone, PartialEq)]
69pub enum EmbedResourcesKind {
70    /// Paths specified in .slint files are made absolute and the absolute
71    /// paths will be used at run-time to load the resources from the file system.
72    AsAbsolutePath,
73    /// The raw files in .slint files are embedded in the application binary.
74    EmbedFiles,
75    /// File names specified in .slint files will be loaded by the Slint compiler,
76    /// optimized for use with the software renderer and embedded in the application binary.
77    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    /// Creates a new default configuration.
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Create a new configuration that includes sets the include paths used for looking up
97    /// `.slint` imports to the specified vector of paths.
98    #[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    /// Create a new configuration that sets the library paths used for looking up
106    /// `@library` imports to the specified map of paths.
107    ///
108    /// Each library path can either be a path to a `.slint` file or a directory.
109    /// If it's a file, the library is imported by its name prefixed by `@` (e.g.
110    /// `@example`). The specified file is the only entry-point for the library
111    /// and other files from the library won't be accessible from the outside.
112    /// If it's a directory, a specific file in that directory must be specified
113    /// when importing the library (e.g. `@example/widgets.slint`). This allows
114    /// exposing multiple entry-points for a single library.
115    ///
116    /// Compile `ui/main.slint` and specify an "example" library path:
117    /// ```rust,no_run
118    /// let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
119    /// let library_paths = std::collections::HashMap::from([(
120    ///     "example".to_string(),
121    ///     manifest_dir.join("third_party/example/ui/lib.slint"),
122    /// )]);
123    /// let config = slint_build::CompilerConfiguration::new().with_library_paths(library_paths);
124    /// slint_build::compile_with_config("ui/main.slint", config).unwrap();
125    /// ```
126    ///
127    /// Import the "example" library in `ui/main.slint`:
128    /// ```slint,ignore
129    /// import { Example } from "@example";
130    /// ```
131    #[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    /// Create a new configuration that selects the style to be used for widgets.
139    #[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    /// Selects how the resources such as images and font are processed.
147    ///
148    /// See [`EmbedResourcesKind`]
149    #[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    /// Sets the scale factor to be applied to all `px` to `phx` conversions
167    /// as constant value. This is only intended for MCU environments. Use
168    /// in combination with [`Self::embed_resources`] to pre-scale images and glyphs
169    /// accordingly.
170    #[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    /// Configures the compiler to bundle translations when compiling Slint code.
178    ///
179    /// It expects the path to be the root directory of the translation files.
180    ///
181    /// The translation files should be in the gettext `.po` format and follow this pattern:
182    /// `<path>/<lang>/LC_MESSAGES/<crate>.po`
183    #[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    /// Configures the compiler to use Signed Distance Field (SDF) encoding for fonts.
194    ///
195    /// This flag only takes effect when `embed_resources` is set to [`EmbedResourcesKind::EmbedForSoftwareRenderer`],
196    /// and requires the `sdf-fonts` cargo feature to be enabled.
197    ///
198    /// [SDF](https://en.wikipedia.org/wiki/Signed_distance_function) reduces the binary size by
199    /// using an alternative representation for fonts, trading off some rendering quality
200    /// for a smaller binary footprint.
201    /// Rendering is slower and may result in slightly inferior visual output.
202    /// Use this on systems with limited flash memory.
203    #[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/// Error returned by the `compile` function
213#[derive(derive_more::Error, derive_more::Display, Debug)]
214#[non_exhaustive]
215pub enum CompileError {
216    /// Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.
217    #[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
218    NotRunViaCargo,
219    /// Parse error. The error are printed in the stderr, and also are in the vector
220    #[display("{_0:?}")]
221    CompileError(#[error(not(source))] Vec<String>),
222    /// Cannot write the generated file
223    #[display("Cannot write the generated file: {_0}")]
224    SaveError(std::io::Error),
225}
226
227struct CodeFormatter<Sink> {
228    indentation: usize,
229    /// We are currently in a string
230    in_string: bool,
231    /// number of bytes after the last `'`, 0 if there was none
232    in_char: usize,
233    /// In string or char, and the previous character was `\\`
234    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                // probably a lifetime
277                self.in_char = 0;
278                false
279            }
280            b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
281                self.escaped = true;
282                // no need to increment in_char since \ isn't a single character
283                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
367/// Compile the `.slint` file and generate rust code for it.
368///
369/// The generated code code will be created in the directory specified by
370/// the `OUT` environment variable as it is expected for build script.
371///
372/// The following line need to be added within your crate in order to include
373/// the generated code.
374/// ```ignore
375/// slint::include_modules!();
376/// ```
377///
378/// The path is relative to the `CARGO_MANIFEST_DIR`.
379///
380/// In case of compilation error, the errors are shown in `stderr`, the error
381/// are also returned in the [`CompileError`] enum. You must `unwrap` the returned
382/// result to make sure that cargo make the compilation fail in case there were
383/// errors when generating the code.
384///
385/// Please check out the documentation of the `slint` crate for more information
386/// about how to use the generated code.
387///
388/// This function can only be called within a build script run by cargo.
389pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
390    compile_with_config(path, CompilerConfiguration::default())
391}
392
393/// Same as [`compile`], but allow to specify a configuration.
394///
395/// Compile `ui/hello.slint` and select the "material" style:
396/// ```rust,no_run
397/// let config =
398///     slint_build::CompilerConfiguration::new()
399///     .with_style("material".into());
400/// slint_build::compile_with_config("ui/hello.slint", config).unwrap();
401/// ```
402pub 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
439/// Similar to [`compile_with_config`], but meant to be used independently of cargo.
440///
441/// Will compile the input file and write the result in the given output file.
442///
443/// Both input_slint_file_path and output_rust_file_path should be absolute paths.
444///
445/// Doesn't print any cargo messages.
446///
447/// Returns a list of all input files that were used to generate the output file. (dependencies)
448pub 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    // 'spin_on' is ok here because the compiler in single threaded and does not block if there is no blocking future
468    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    // print warnings
492    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
510/// This function is for use the application's build script, in order to print any device specific
511/// build flags reported by the backend
512pub 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}