slint_build/
lib.rs

1// Copyright © SixtyFPS GmbH <[email protected]>
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.12"
21...
22
23[build-dependencies]
24slint-build = "1.12"
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.
61#[derive(Clone)]
62pub struct CompilerConfiguration {
63    config: i_slint_compiler::CompilerConfiguration,
64}
65
66/// How should the slint compiler embed images and fonts
67///
68/// Parameter of [`CompilerConfiguration::embed_resources()`]
69#[derive(Clone, PartialEq)]
70pub enum EmbedResourcesKind {
71    /// Paths specified in .slint files are made absolute and the absolute
72    /// paths will be used at run-time to load the resources from the file system.
73    AsAbsolutePath,
74    /// The raw files in .slint files are embedded in the application binary.
75    EmbedFiles,
76    /// File names specified in .slint files will be loaded by the Slint compiler,
77    /// optimized for use with the software renderer and embedded in the application binary.
78    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    /// Creates a new default configuration.
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Create a new configuration that includes sets the include paths used for looking up
98    /// `.slint` imports to the specified vector of paths.
99    #[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    /// Create a new configuration that sets the library paths used for looking up
107    /// `@library` imports to the specified map of paths.
108    ///
109    /// Each library path can either be a path to a `.slint` file or a directory.
110    /// If it's a file, the library is imported by its name prefixed by `@` (e.g.
111    /// `@example`). The specified file is the only entry-point for the library
112    /// and other files from the library won't be accessible from the outside.
113    /// If it's a directory, a specific file in that directory must be specified
114    /// when importing the library (e.g. `@example/widgets.slint`). This allows
115    /// exposing multiple entry-points for a single library.
116    ///
117    /// Compile `ui/main.slint` and specify an "example" library path:
118    /// ```rust,no_run
119    /// let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
120    /// let library_paths = std::collections::HashMap::from([(
121    ///     "example".to_string(),
122    ///     manifest_dir.join("third_party/example/ui/lib.slint"),
123    /// )]);
124    /// let config = slint_build::CompilerConfiguration::new().with_library_paths(library_paths);
125    /// slint_build::compile_with_config("ui/main.slint", config).unwrap();
126    /// ```
127    ///
128    /// Import the "example" library in `ui/main.slint`:
129    /// ```slint,ignore
130    /// import { Example } from "@example";
131    /// ```
132    #[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    /// Create a new configuration that selects the style to be used for widgets.
140    #[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    /// Selects how the resources such as images and font are processed.
148    ///
149    /// See [`EmbedResourcesKind`]
150    #[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    /// Sets the scale factor to be applied to all `px` to `phx` conversions
168    /// as constant value. This is only intended for MCU environments. Use
169    /// in combination with [`Self::embed_resources`] to pre-scale images and glyphs
170    /// accordingly.
171    #[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    /// Configures the compiler to bundle translations when compiling Slint code.
179    ///
180    /// It expects the path to be the root directory of the translation files.
181    ///
182    /// If given a relative path, it will be resolved relative to `$CARGO_MANIFEST_DIR`.
183    ///
184    /// The translation files should be in the gettext `.po` format and follow this pattern:
185    /// `<path>/<lang>/LC_MESSAGES/<crate>.po`
186    #[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    /// Configures the compiler to emit additional debug info when compiling Slint code.
197    ///
198    /// This is the equivalent to setting `SLINT_EMIT_DEBUG_INFO=1` and using the `slint!()` macro
199    /// and is primarily used by `i-slint-backend-testing`.
200    #[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    /// Configures the compiler to use Signed Distance Field (SDF) encoding for fonts.
209    ///
210    /// This flag only takes effect when `embed_resources` is set to [`EmbedResourcesKind::EmbedForSoftwareRenderer`],
211    /// and requires the `sdf-fonts` cargo feature to be enabled.
212    ///
213    /// [SDF](https://en.wikipedia.org/wiki/Signed_distance_function) reduces the binary size by
214    /// using an alternative representation for fonts, trading off some rendering quality
215    /// for a smaller binary footprint.
216    /// Rendering is slower and may result in slightly inferior visual output.
217    /// Use this on systems with limited flash memory.
218    #[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/// Error returned by the `compile` function
228#[derive(derive_more::Error, derive_more::Display, Debug)]
229#[non_exhaustive]
230pub enum CompileError {
231    /// Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.
232    #[display("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
233    NotRunViaCargo,
234    /// Parse error. The error are printed in the stderr, and also are in the vector
235    #[display("{_0:?}")]
236    CompileError(#[error(not(source))] Vec<String>),
237    /// Cannot write the generated file
238    #[display("Cannot write the generated file: {_0}")]
239    SaveError(std::io::Error),
240}
241
242struct CodeFormatter<Sink> {
243    indentation: usize,
244    /// We are currently in a string
245    in_string: bool,
246    /// number of bytes after the last `'`, 0 if there was none
247    in_char: usize,
248    /// In string or char, and the previous character was `\\`
249    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                // probably a lifetime
292                self.in_char = 0;
293                false
294            }
295            b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
296                self.escaped = true;
297                // no need to increment in_char since \ isn't a single character
298                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
382/// Compile the `.slint` file and generate rust code for it.
383///
384/// The generated code code will be created in the directory specified by
385/// the `OUT` environment variable as it is expected for build script.
386///
387/// The following line need to be added within your crate in order to include
388/// the generated code.
389/// ```ignore
390/// slint::include_modules!();
391/// ```
392///
393/// The path is relative to the `CARGO_MANIFEST_DIR`.
394///
395/// In case of compilation error, the errors are shown in `stderr`, the error
396/// are also returned in the [`CompileError`] enum. You must `unwrap` the returned
397/// result to make sure that cargo make the compilation fail in case there were
398/// errors when generating the code.
399///
400/// Please check out the documentation of the `slint` crate for more information
401/// about how to use the generated code.
402///
403/// This function can only be called within a build script run by cargo.
404pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
405    compile_with_config(path, CompilerConfiguration::default())
406}
407
408/// Same as [`compile`], but allow to specify a configuration.
409///
410/// Compile `ui/hello.slint` and select the "material" style:
411/// ```rust,no_run
412/// let config =
413///     slint_build::CompilerConfiguration::new()
414///     .with_style("material".into());
415/// slint_build::compile_with_config("ui/hello.slint", config).unwrap();
416/// ```
417pub 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
454/// Similar to [`compile_with_config`], but meant to be used independently of cargo.
455///
456/// Will compile the input file and write the result in the given output file.
457///
458/// Both input_slint_file_path and output_rust_file_path should be absolute paths.
459///
460/// Doesn't print any cargo messages.
461///
462/// Returns a list of all input files that were used to generate the output file. (dependencies)
463pub 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    // 'spin_on' is ok here because the compiler in single threaded and does not block if there is no blocking future
483    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    // print warnings
507    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
525/// This function is for use the application's build script, in order to print any device specific
526/// build flags reported by the backend
527pub 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}