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 | /*! |
5 | This crate serves as a companion crate of the slint crate. |
6 | It is meant to allow you to compile the `.slint` files from your `build.rs` script. |
7 | |
8 | The main entry point of this crate is the [`compile()`] function |
9 | |
10 | ## Example |
11 | |
12 | In your Cargo.toml: |
13 | |
14 | ```toml |
15 | [package] |
16 | ... |
17 | build = "build.rs" |
18 | |
19 | [dependencies] |
20 | slint = "1.5.0" |
21 | ... |
22 | |
23 | [build-dependencies] |
24 | slint-build = "1.5.0" |
25 | ``` |
26 | |
27 | In the `build.rs` file: |
28 | |
29 | ```ignore |
30 | fn main() { |
31 | slint_build::compile("ui/hello.slint" ).unwrap(); |
32 | } |
33 | ``` |
34 | |
35 | Then in your main file |
36 | |
37 | ```ignore |
38 | slint::include_modules!(); |
39 | fn 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" ))] |
48 | compile_error!( |
49 | "The feature `default` must be enabled to ensure \ |
50 | forward compatibility with future version of this crate" |
51 | ); |
52 | |
53 | use std::collections::HashMap; |
54 | use std::env; |
55 | use std::io::{BufWriter, Write}; |
56 | use std::path::Path; |
57 | |
58 | use i_slint_compiler::diagnostics::BuildDiagnostics; |
59 | |
60 | /// The structure for configuring aspects of the compilation of `.slint` markup files to Rust. |
61 | pub 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)] |
69 | pub 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 | |
80 | impl 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 | |
90 | impl 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 ] |
170 | pub 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 | |
182 | struct 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 | |
193 | impl<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 | |
199 | impl<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 ] |
267 | fn 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. |
342 | pub 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 | /// ``` |
355 | pub 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 |
432 | pub 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 | |