1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
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.5.0"
21...
22
23[build-dependencies]
24slint-build = "1.5.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
167/// Error returned by the `compile` function
168#[derive(thiserror::Error, Debug)]
169#[non_exhaustive]
170pub enum CompileError {
171 /// Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.
172 #[error("Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.")]
173 NotRunViaCargo,
174 /// Parse error. The error are printed in the stderr, and also are in the vector
175 #[error("{0:?}")]
176 CompileError(Vec<String>),
177 /// Cannot write the generated file
178 #[error("Cannot write the generated file: {0}")]
179 SaveError(std::io::Error),
180}
181
182struct CodeFormatter<Sink> {
183 indentation: usize,
184 /// We are currently in a string
185 in_string: bool,
186 /// number of bytes after the last `'`, 0 if there was none
187 in_char: usize,
188 /// In string or char, and the previous character was `\\`
189 escaped: bool,
190 sink: Sink,
191}
192
193impl<Sink> CodeFormatter<Sink> {
194 pub fn new(sink: Sink) -> Self {
195 Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
196 }
197}
198
199impl<Sink: Write> Write for CodeFormatter<Sink> {
200 fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
201 let len = s.len();
202 while let Some(idx) = s.iter().position(|c| match c {
203 b'{' if !self.in_string && self.in_char == 0 => {
204 self.indentation += 1;
205 true
206 }
207 b'}' if !self.in_string && self.in_char == 0 => {
208 self.indentation -= 1;
209 true
210 }
211 b';' if !self.in_string && self.in_char == 0 => true,
212 b'"' if !self.in_string && self.in_char == 0 => {
213 self.in_string = true;
214 self.escaped = false;
215 false
216 }
217 b'"' if self.in_string && !self.escaped => {
218 self.in_string = false;
219 false
220 }
221 b'\'' if !self.in_string && self.in_char == 0 => {
222 self.in_char = 1;
223 self.escaped = false;
224 false
225 }
226 b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
227 self.in_char = 0;
228 false
229 }
230 b' ' | b'>' if self.in_char > 2 && !self.escaped => {
231 // probably a lifetime
232 self.in_char = 0;
233 false
234 }
235 b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
236 self.escaped = true;
237 // no need to increment in_char since \ isn't a single character
238 false
239 }
240 _ if self.in_char > 0 => {
241 self.in_char += 1;
242 self.escaped = false;
243 false
244 }
245 _ => {
246 self.escaped = false;
247 false
248 }
249 }) {
250 let idx = idx + 1;
251 self.sink.write_all(&s[..idx])?;
252 self.sink.write_all(b"\n")?;
253 for _ in 0..self.indentation {
254 self.sink.write_all(b" ")?;
255 }
256 s = &s[idx..];
257 }
258 self.sink.write_all(s)?;
259 Ok(len)
260 }
261 fn flush(&mut self) -> std::io::Result<()> {
262 self.sink.flush()
263 }
264}
265
266#[test]
267fn formatter_test() {
268 fn format_code(code: &str) -> String {
269 let mut res = Vec::new();
270 let mut formatter = CodeFormatter::new(&mut res);
271 formatter.write_all(code.as_bytes()).unwrap();
272 String::from_utf8(res).unwrap()
273 }
274
275 assert_eq!(
276 format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
277 r#"fn main() {
278 if ';' == '}' {
279 return ";";
280 }
281 else {
282 panic!() }
283 }
284"#
285 );
286
287 assert_eq!(
288 format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
289 r#"fn xx<'lt>(foo: &'lt str) {
290 println!("{}", '\u{f700}');
291 return Ok(());
292 }
293"#
294 );
295
296 assert_eq!(
297 format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
298 r#"fn main() {
299 "";
300 "'";
301 "\"";
302 "{}";
303 "\\";
304 "\\\"";
305 }
306"#
307 );
308
309 assert_eq!(
310 format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
311 r#"fn main() {
312 '"';
313 '\'';
314 '{';
315 '}';
316 '\\';
317 }
318"#
319 );
320}
321
322/// Compile the `.slint` file and generate rust code for it.
323///
324/// The generated code code will be created in the directory specified by
325/// the `OUT` environment variable as it is expected for build script.
326///
327/// The following line need to be added within your crate in order to include
328/// the generated code.
329/// ```ignore
330/// slint::include_modules!();
331/// ```
332///
333/// The path is relative to the `CARGO_MANIFEST_DIR`.
334///
335/// In case of compilation error, the errors are shown in `stderr`, the error
336/// are also returned in the [`CompileError`] enum. You must `unwrap` the returned
337/// result to make sure that cargo make the compilation fail in case there were
338/// errors when generating the code.
339///
340/// Please check out the documentation of the `slint` crate for more information
341/// about how to use the generated code.
342pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
343 compile_with_config(path, config:CompilerConfiguration::default())
344}
345
346/// Same as [`compile`], but allow to specify a configuration.
347///
348/// Compile `ui/hello.slint` and select the "material" style:
349/// ```rust,no_run
350/// let config =
351/// slint_build::CompilerConfiguration::new()
352/// .with_style("material".into());
353/// slint_build::compile_with_config("ui/hello.slint", config).unwrap();
354/// ```
355pub fn compile_with_config(
356 path: impl AsRef<std::path::Path>,
357 config: CompilerConfiguration,
358) -> Result<(), CompileError> {
359 let path = Path::new(&env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?)
360 .join(path.as_ref());
361
362 let mut diag = BuildDiagnostics::default();
363 let syntax_node = i_slint_compiler::parser::parse_file(&path, &mut diag);
364
365 if diag.has_error() {
366 let vec = diag.to_string_vec();
367 diag.print();
368 return Err(CompileError::CompileError(vec));
369 }
370
371 let mut compiler_config = config.config;
372 compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
373
374 let syntax_node = syntax_node.expect("diags contained no compilation errors");
375
376 // 'spin_on' is ok here because the compiler in single threaded and does not block if there is no blocking future
377 let (doc, diag, _) =
378 spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
379
380 if diag.has_error() {
381 let vec = diag.to_string_vec();
382 diag.print();
383 return Err(CompileError::CompileError(vec));
384 }
385
386 let output_file_path = Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?)
387 .join(
388 path.file_stem()
389 .map(Path::new)
390 .unwrap_or_else(|| Path::new("slint_out"))
391 .with_extension("rs"),
392 );
393
394 let file = std::fs::File::create(&output_file_path).map_err(CompileError::SaveError)?;
395 let mut code_formatter = CodeFormatter::new(BufWriter::new(file));
396 let generated = i_slint_compiler::generator::rust::generate(&doc);
397
398 for x in &diag.all_loaded_files {
399 if x.is_absolute() {
400 println!("cargo:rerun-if-changed={}", x.display());
401 }
402 }
403
404 // print warnings
405 diag.diagnostics_as_string().lines().for_each(|w| {
406 if !w.is_empty() {
407 println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
408 }
409 });
410
411 write!(code_formatter, "{}", generated).map_err(CompileError::SaveError)?;
412 println!("cargo:rerun-if-changed={}", path.display());
413
414 for resource in doc.root_component.embedded_file_resources.borrow().keys() {
415 if !resource.starts_with("builtin:") {
416 println!("cargo:rerun-if-changed={}", resource);
417 }
418 }
419 println!("cargo:rerun-if-env-changed=SLINT_STYLE");
420 println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
421 println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
422 println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
423 println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
424
425 println!("cargo:rustc-env=SLINT_INCLUDE_GENERATED={}", output_file_path.display());
426
427 Ok(())
428}
429
430/// This function is for use the application's build script, in order to print any device specific
431/// build flags reported by the backend
432pub fn print_rustc_flags() -> std::io::Result<()> {
433 if let Some(board_config_path) =
434 std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
435 {
436 let config = std::fs::read_to_string(board_config_path.as_path())?;
437 let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
438
439 for link_arg in
440 toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
441 {
442 if let Some(option) = link_arg.as_str() {
443 println!("cargo:rustc-link-arg={}", option);
444 }
445 }
446
447 for link_search_path in
448 toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
449 {
450 if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
451 if path.is_relative() {
452 path = board_config_path.parent().unwrap().join(path);
453 }
454 println!("cargo:rustc-link-search={}", path.to_string_lossy());
455 }
456 }
457 println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
458 println!("cargo:rerun-if-changed={}", board_config_path.display());
459 }
460
461 Ok(())
462}
463