Skip to main content

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.16.0"
23...
24
25[build-dependencies]
26slint-build = "1.16.0"
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/// Argument of [`CompilerConfiguration::with_default_translation_context()`]
63///
64pub use i_slint_compiler::DefaultTranslationContext;
65
66/// The structure for configuring aspects of the compilation of `.slint` markup files to Rust.
67#[derive(Clone)]
68pub struct CompilerConfiguration {
69    config: i_slint_compiler::CompilerConfiguration,
70}
71
72/// How should the Slint compiler embed images and fonts
73///
74/// Parameter of [`CompilerConfiguration::embed_resources()`]
75#[derive(Clone, PartialEq)]
76pub enum EmbedResourcesKind {
77    /// Resources are loaded from their absolute path at run-time.
78    ///
79    /// Only useful for debugging, since the files must still be present at the same path on the
80    /// machine running the application.
81    AsAbsolutePath,
82    /// The files referenced from .slint files are embedded in the binary as-is (for example
83    /// a PNG stays compressed), and decoded at run-time.
84    EmbedFiles,
85    /// Images and fonts are pre-processed at compile time and embedded as uncompressed pixel
86    /// data, ready to be drawn by the software renderer without any decoding at run-time.
87    ///
88    /// Useful for MCUs with no file system and little RAM.
89    /// Only the Slint software renderer can use these resources; Skia and FemtoVG can't.
90    EmbedForSoftwareRenderer,
91}
92
93impl Default for CompilerConfiguration {
94    fn default() -> Self {
95        Self {
96            config: i_slint_compiler::CompilerConfiguration::new(
97                i_slint_compiler::generator::OutputFormat::Rust,
98            ),
99        }
100    }
101}
102
103impl CompilerConfiguration {
104    /// Creates a new default configuration.
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Create a new configuration that includes sets the include paths used for looking up
110    /// `.slint` imports to the specified vector of paths.
111    #[must_use]
112    pub fn with_include_paths(self, include_paths: Vec<std::path::PathBuf>) -> Self {
113        let mut config = self.config;
114        config.include_paths = include_paths;
115        Self { config }
116    }
117
118    /// Create a new configuration that sets the library paths used for looking up
119    /// `@library` imports to the specified map of paths.
120    ///
121    /// Each library path can either be a path to a `.slint` file or a directory.
122    /// If it's a file, the library is imported by its name prefixed by `@` (e.g.
123    /// `@example`). The specified file is the only entry-point for the library
124    /// and other files from the library won't be accessible from the outside.
125    /// If it's a directory, a specific file in that directory must be specified
126    /// when importing the library (e.g. `@example/widgets.slint`). This allows
127    /// exposing multiple entry-points for a single library.
128    ///
129    /// Compile `ui/main.slint` and specify an "example" library path:
130    /// ```rust,no_run
131    /// let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
132    /// let library_paths = std::collections::HashMap::from([(
133    ///     "example".to_string(),
134    ///     manifest_dir.join("third_party/example/ui/lib.slint"),
135    /// )]);
136    /// let config = slint_build::CompilerConfiguration::new().with_library_paths(library_paths);
137    /// slint_build::compile_with_config("ui/main.slint", config).unwrap();
138    /// ```
139    ///
140    /// Import the "example" library in `ui/main.slint`:
141    /// ```slint,ignore
142    /// import { Example } from "@example";
143    /// ```
144    #[must_use]
145    pub fn with_library_paths(self, library_paths: HashMap<String, std::path::PathBuf>) -> Self {
146        let mut config = self.config;
147        config.library_paths = library_paths;
148        Self { config }
149    }
150
151    /// Create a new configuration that selects the style to be used for widgets.
152    #[must_use]
153    pub fn with_style(self, style: String) -> Self {
154        let mut config = self.config;
155        config.style = Some(style);
156        Self { config }
157    }
158
159    /// Selects how the resources such as images and font are processed.
160    ///
161    /// See [`EmbedResourcesKind`]
162    #[must_use]
163    pub fn embed_resources(self, kind: EmbedResourcesKind) -> Self {
164        let mut config = self.config;
165        config.embed_resources = match kind {
166            EmbedResourcesKind::AsAbsolutePath => {
167                i_slint_compiler::EmbedResourcesKind::OnlyBuiltinResources
168            }
169            EmbedResourcesKind::EmbedFiles => {
170                i_slint_compiler::EmbedResourcesKind::EmbedAllResources
171            }
172            EmbedResourcesKind::EmbedForSoftwareRenderer => {
173                i_slint_compiler::EmbedResourcesKind::EmbedTextures
174            }
175        };
176        Self { config }
177    }
178
179    /// Sets the scale factor to be applied to all `px` to `phx` conversions
180    /// as constant value. This is only intended for MCU environments. Use
181    /// in combination with [`Self::embed_resources`] to pre-scale images and glyphs
182    /// accordingly.
183    ///
184    /// If this is set, changing the scale factor at runtime will not have any effect.
185    #[must_use]
186    pub fn with_scale_factor(mut self, factor: f32) -> Self {
187        self.config.const_scale_factor = Some(factor);
188        self
189    }
190
191    /// Configures the compiler to bundle translations when compiling Slint code.
192    ///
193    /// It expects the path to be the root directory of the translation files.
194    ///
195    /// If given a relative path, it will be resolved relative to `$CARGO_MANIFEST_DIR`.
196    ///
197    /// The translation files should be in the gettext `.po` format and follow this pattern:
198    /// `<path>/<lang>/LC_MESSAGES/<crate>.po`
199    #[must_use]
200    pub fn with_bundled_translations(
201        self,
202        path: impl Into<std::path::PathBuf>,
203    ) -> CompilerConfiguration {
204        let mut config = self.config;
205        config.translation_path_bundle = Some(path.into());
206        Self { config }
207    }
208
209    /// Unless explicitly specified with the `@tr("context" => ...)`, the default translation context is the component name.
210    /// Use this option with [`DefaultTranslationContext::None`] to disable the default translation context.
211    ///
212    /// The translation file must also not have context
213    /// (`--no-default-translation-context` argument of `slint-tr-extractor`)
214    #[must_use]
215    pub fn with_default_translation_context(
216        mut self,
217        default_translation_context: DefaultTranslationContext,
218    ) -> Self {
219        self.config.default_translation_context = default_translation_context;
220        self
221    }
222
223    /// Configures the compiler to emit additional debug info when compiling Slint code.
224    ///
225    /// This is the equivalent to setting `SLINT_EMIT_DEBUG_INFO=1` and using the `slint!()` macro
226    /// and is primarily used by `i-slint-backend-testing`.
227    #[doc(hidden)]
228    #[must_use]
229    pub fn with_debug_info(self, enable: bool) -> Self {
230        let mut config = self.config;
231        config.debug_info = enable;
232        Self { config }
233    }
234
235    /// Configures the compiler to treat the Slint as part of a library.
236    ///
237    /// Use this when the components and types of the Slint code need
238    /// to be accessible from other modules.
239    ///
240    /// **Note**: This feature is experimental and may change or be removed in the future.
241    #[cfg(feature = "experimental-module-builds")]
242    #[must_use]
243    pub fn as_library(self, library_name: &str) -> Self {
244        let mut config = self.config;
245        config.library_name = Some(library_name.to_string());
246        Self { config }
247    }
248
249    /// Specify the Rust module to place the generated code in.
250    ///
251    /// **Note**: This feature is experimental and may change or be removed in the future.
252    #[cfg(feature = "experimental-module-builds")]
253    #[must_use]
254    pub fn rust_module(self, rust_module: &str) -> Self {
255        let mut config = self.config;
256        config.rust_module = Some(rust_module.to_string());
257        Self { config }
258    }
259    /// Configures the compiler to use Signed Distance Field (SDF) encoding for fonts.
260    ///
261    /// This flag only takes effect when `embed_resources` is set to [`EmbedResourcesKind::EmbedForSoftwareRenderer`],
262    /// and requires the `sdf-fonts` cargo feature to be enabled.
263    ///
264    /// [SDF](https://en.wikipedia.org/wiki/Signed_distance_function) reduces the binary size by
265    /// using an alternative representation for fonts, trading off some rendering quality
266    /// for a smaller binary footprint.
267    /// Rendering is slower and may result in slightly inferior visual output.
268    /// Use this on systems with limited flash memory.
269    #[cfg(feature = "sdf-fonts")]
270    #[must_use]
271    pub fn with_sdf_fonts(self, enable: bool) -> Self {
272        let mut config = self.config;
273        config.use_sdf_fonts = enable;
274        Self { config }
275    }
276
277    /// Converts any relative include_paths or library_paths to absolute paths relative to the manifest_dir.
278    #[must_use]
279    fn with_absolute_paths(self, manifest_dir: &std::path::Path) -> Self {
280        let mut config = self.config;
281
282        let to_absolute_path = |path: &mut std::path::PathBuf| {
283            if path.is_relative() {
284                *path = manifest_dir.join(&path);
285            }
286        };
287
288        for path in config.library_paths.values_mut() {
289            to_absolute_path(path);
290        }
291
292        for path in config.include_paths.iter_mut() {
293            to_absolute_path(path);
294        }
295
296        Self { config }
297    }
298}
299
300/// Error returned by the `compile` function
301#[derive(derive_more::Error, derive_more::Display, Debug)]
302#[non_exhaustive]
303pub enum CompileError {
304    /// Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo.
305    #[display(
306        "Cannot read environment variable CARGO_MANIFEST_DIR or OUT_DIR. The build script need to be run via cargo."
307    )]
308    NotRunViaCargo,
309    /// Parse error. The error are printed in the stderr, and also are in the vector
310    #[display("{_0:?}")]
311    CompileError(#[error(not(source))] Vec<String>),
312    /// Cannot write the generated file
313    #[display("Cannot write the generated file: {_0}")]
314    SaveError(std::io::Error),
315}
316
317struct CodeFormatter<Sink> {
318    indentation: usize,
319    /// We are currently in a string
320    in_string: bool,
321    /// number of bytes after the last `'`, 0 if there was none
322    in_char: usize,
323    /// In string or char, and the previous character was `\\`
324    escaped: bool,
325    sink: Sink,
326}
327
328impl<Sink> CodeFormatter<Sink> {
329    pub fn new(sink: Sink) -> Self {
330        Self { indentation: 0, in_string: false, in_char: 0, escaped: false, sink }
331    }
332}
333
334impl<Sink: Write> Write for CodeFormatter<Sink> {
335    fn write(&mut self, mut s: &[u8]) -> std::io::Result<usize> {
336        let len = s.len();
337        while let Some(idx) = s.iter().position(|c| match c {
338            b'{' if !self.in_string && self.in_char == 0 => {
339                self.indentation += 1;
340                true
341            }
342            b'}' if !self.in_string && self.in_char == 0 => {
343                self.indentation -= 1;
344                true
345            }
346            b';' if !self.in_string && self.in_char == 0 => true,
347            b'"' if !self.in_string && self.in_char == 0 => {
348                self.in_string = true;
349                self.escaped = false;
350                false
351            }
352            b'"' if self.in_string && !self.escaped => {
353                self.in_string = false;
354                false
355            }
356            b'\'' if !self.in_string && self.in_char == 0 => {
357                self.in_char = 1;
358                self.escaped = false;
359                false
360            }
361            b'\'' if !self.in_string && self.in_char > 0 && !self.escaped => {
362                self.in_char = 0;
363                false
364            }
365            b' ' | b'>' if self.in_char > 2 && !self.escaped => {
366                // probably a lifetime
367                self.in_char = 0;
368                false
369            }
370            b'\\' if (self.in_string || self.in_char > 0) && !self.escaped => {
371                self.escaped = true;
372                // no need to increment in_char since \ isn't a single character
373                false
374            }
375            _ if self.in_char > 0 => {
376                self.in_char += 1;
377                self.escaped = false;
378                false
379            }
380            _ => {
381                self.escaped = false;
382                false
383            }
384        }) {
385            let idx = idx + 1;
386            self.sink.write_all(&s[..idx])?;
387            self.sink.write_all(b"\n")?;
388            for _ in 0..self.indentation {
389                self.sink.write_all(b"    ")?;
390            }
391            s = &s[idx..];
392        }
393        self.sink.write_all(s)?;
394        Ok(len)
395    }
396    fn flush(&mut self) -> std::io::Result<()> {
397        self.sink.flush()
398    }
399}
400
401#[test]
402fn formatter_test() {
403    fn format_code(code: &str) -> String {
404        let mut res = Vec::new();
405        let mut formatter = CodeFormatter::new(&mut res);
406        formatter.write_all(code.as_bytes()).unwrap();
407        String::from_utf8(res).unwrap()
408    }
409
410    assert_eq!(
411        format_code("fn main() { if ';' == '}' { return \";\"; } else { panic!() } }"),
412        r#"fn main() {
413     if ';' == '}' {
414         return ";";
415         }
416     else {
417         panic!() }
418     }
419"#
420    );
421
422    assert_eq!(
423        format_code(r#"fn xx<'lt>(foo: &'lt str) { println!("{}", '\u{f700}'); return Ok(()); }"#),
424        r#"fn xx<'lt>(foo: &'lt str) {
425     println!("{}", '\u{f700}');
426     return Ok(());
427     }
428"#
429    );
430
431    assert_eq!(
432        format_code(r#"fn main() { ""; "'"; "\""; "{}"; "\\"; "\\\""; }"#),
433        r#"fn main() {
434     "";
435     "'";
436     "\"";
437     "{}";
438     "\\";
439     "\\\"";
440     }
441"#
442    );
443
444    assert_eq!(
445        format_code(r#"fn main() { '"'; '\''; '{'; '}'; '\\'; }"#),
446        r#"fn main() {
447     '"';
448     '\'';
449     '{';
450     '}';
451     '\\';
452     }
453"#
454    );
455}
456
457/// Compile the `.slint` file and generate rust code for it.
458///
459/// The generated code code will be created in the directory specified by
460/// the `OUT` environment variable as it is expected for build script.
461///
462/// The following line need to be added within your crate in order to include
463/// the generated code.
464/// ```ignore
465/// slint::include_modules!();
466/// ```
467///
468/// The path is relative to the `CARGO_MANIFEST_DIR`.
469///
470/// In case of compilation error, the errors are shown in `stderr`, the error
471/// are also returned in the [`CompileError`] enum. You must `unwrap` the returned
472/// result to make sure that cargo make the compilation fail in case there were
473/// errors when generating the code.
474///
475/// Please check out the documentation of the `slint` crate for more information
476/// about how to use the generated code.
477///
478/// This function can only be called within a build script run by cargo.
479///
480/// See also [`compile_with_config()`] if you want to specify a configuration.
481pub fn compile(path: impl AsRef<std::path::Path>) -> Result<(), CompileError> {
482    compile_with_config(path, CompilerConfiguration::default())
483}
484
485/// Same as [`compile`], but allow to specify a configuration.
486///
487/// Compile `ui/hello.slint` and select the "material" style:
488/// ```rust,no_run
489/// let config =
490///     slint_build::CompilerConfiguration::new()
491///     .with_style("material".into());
492/// slint_build::compile_with_config("ui/hello.slint", config).unwrap();
493/// ```
494pub fn compile_with_config(
495    relative_slint_file_path: impl AsRef<std::path::Path>,
496    config: CompilerConfiguration,
497) -> Result<(), CompileError> {
498    let manifest_path = std::path::PathBuf::from(
499        env::var_os("CARGO_MANIFEST_DIR").ok_or(CompileError::NotRunViaCargo)?,
500    );
501    let config = config.with_absolute_paths(&manifest_path);
502
503    let path = manifest_path.join(relative_slint_file_path.as_ref());
504
505    let absolute_rust_output_file_path =
506        Path::new(&env::var_os("OUT_DIR").ok_or(CompileError::NotRunViaCargo)?).join(
507            path.file_stem()
508                .map(Path::new)
509                .unwrap_or_else(|| Path::new("slint_out"))
510                .with_extension("rs"),
511        );
512
513    #[cfg(feature = "experimental-module-builds")]
514    if let Some(library_name) = config.config.library_name.clone() {
515        println!("cargo::metadata=SLINT_LIBRARY_NAME={}", library_name);
516        println!(
517            "cargo::metadata=SLINT_LIBRARY_PACKAGE={}",
518            std::env::var("CARGO_PKG_NAME").ok().unwrap_or_default()
519        );
520        println!("cargo::metadata=SLINT_LIBRARY_SOURCE={}", path.display());
521        if let Some(rust_module) = &config.config.rust_module {
522            println!("cargo::metadata=SLINT_LIBRARY_MODULE={}", rust_module);
523        }
524    }
525    let paths_dependencies =
526        compile_with_output_path(path, absolute_rust_output_file_path.clone(), config)?;
527
528    for path_dependency in paths_dependencies {
529        println!("cargo:rerun-if-changed={}", path_dependency.display());
530    }
531
532    println!("cargo:rerun-if-env-changed=SLINT_STYLE");
533    println!("cargo:rerun-if-env-changed=SLINT_FONT_SIZES");
534    println!("cargo:rerun-if-env-changed=SLINT_SCALE_FACTOR");
535    println!("cargo:rerun-if-env-changed=SLINT_ASSET_SECTION");
536    println!("cargo:rerun-if-env-changed=SLINT_EMBED_RESOURCES");
537    println!("cargo:rerun-if-env-changed=SLINT_EMIT_DEBUG_INFO");
538    println!("cargo:rerun-if-env-changed=SLINT_LIVE_PREVIEW");
539
540    println!(
541        "cargo:rustc-env=SLINT_INCLUDE_GENERATED={}",
542        absolute_rust_output_file_path.display()
543    );
544
545    Ok(())
546}
547
548/// Similar to [`compile_with_config`], but meant to be used independently of cargo.
549///
550/// Will compile the input file and write the result in the given output file.
551///
552/// Both input_slint_file_path and output_rust_file_path should be absolute paths.
553///
554/// Doesn't print any cargo messages.
555///
556/// Returns a list of all input files that were used to generate the output file. (dependencies)
557pub fn compile_with_output_path(
558    input_slint_file_path: impl AsRef<std::path::Path>,
559    output_rust_file_path: impl AsRef<std::path::Path>,
560    config: CompilerConfiguration,
561) -> Result<Vec<std::path::PathBuf>, CompileError> {
562    let mut diag = BuildDiagnostics::default();
563    let syntax_node = i_slint_compiler::parser::parse_file(&input_slint_file_path, &mut diag);
564
565    if diag.has_errors() {
566        let vec = diag.to_string_vec();
567        diag.print();
568        return Err(CompileError::CompileError(vec));
569    }
570
571    let mut compiler_config = config.config;
572    compiler_config.translation_domain = std::env::var("CARGO_PKG_NAME").ok();
573
574    let syntax_node = syntax_node.expect("diags contained no compilation errors");
575
576    // 'spin_on' is ok here because the compiler in single threaded and does not block if there is no blocking future
577    let (doc, diag, loader) =
578        spin_on::spin_on(i_slint_compiler::compile_syntax_node(syntax_node, diag, compiler_config));
579
580    if diag.has_errors()
581        || (!diag.is_empty() && std::env::var("SLINT_COMPILER_DENY_WARNINGS").is_ok())
582    {
583        let vec = diag.to_string_vec();
584        diag.print();
585        return Err(CompileError::CompileError(vec));
586    }
587
588    let output_file =
589        std::fs::File::create(&output_rust_file_path).map_err(CompileError::SaveError)?;
590    let mut code_formatter = CodeFormatter::new(BufWriter::new(output_file));
591    let generated = i_slint_compiler::generator::rust::generate(&doc, &loader.compiler_config)
592        .map_err(|e| CompileError::CompileError(vec![e.to_string()]))?;
593
594    let mut dependencies: Vec<std::path::PathBuf> = Vec::new();
595
596    for x in &diag.all_loaded_files {
597        if x.is_absolute() {
598            dependencies.push(x.clone());
599        }
600    }
601
602    // print warnings
603    diag.diagnostics_as_string().lines().for_each(|w| {
604        if !w.is_empty() {
605            println!("cargo:warning={}", w.strip_prefix("warning: ").unwrap_or(w))
606        }
607    });
608
609    write!(code_formatter, "{generated}").map_err(CompileError::SaveError)?;
610    dependencies.push(input_slint_file_path.as_ref().to_path_buf());
611
612    for er in doc.embedded_file_resources.borrow().iter() {
613        if let Some(resource) = er.path.as_deref()
614            && !resource.starts_with("builtin:")
615        {
616            dependencies.push(Path::new(resource).to_path_buf());
617        }
618    }
619
620    code_formatter.sink.flush().map_err(CompileError::SaveError)?;
621
622    Ok(dependencies)
623}
624
625/// This function is for use the application's build script, in order to print any device specific
626/// build flags reported by the backend
627pub fn print_rustc_flags() -> std::io::Result<()> {
628    if let Some(board_config_path) =
629        std::env::var_os("DEP_MCU_BOARD_SUPPORT_BOARD_CONFIG_PATH").map(std::path::PathBuf::from)
630    {
631        let config = std::fs::read_to_string(board_config_path.as_path())?;
632        let toml = config.parse::<toml_edit::DocumentMut>().expect("invalid board config toml");
633
634        for link_arg in
635            toml.get("link_args").and_then(toml_edit::Item::as_array).into_iter().flatten()
636        {
637            if let Some(option) = link_arg.as_str() {
638                println!("cargo:rustc-link-arg={option}");
639            }
640        }
641
642        for link_search_path in
643            toml.get("link_search_path").and_then(toml_edit::Item::as_array).into_iter().flatten()
644        {
645            if let Some(mut path) = link_search_path.as_str().map(std::path::PathBuf::from) {
646                if path.is_relative() {
647                    path = board_config_path.parent().unwrap().join(path);
648                }
649                println!("cargo:rustc-link-search={}", path.to_string_lossy());
650            }
651        }
652        println!("cargo:rerun-if-env-changed=DEP_MCU_BOARD_SUPPORT_MCU_BOARD_CONFIG_PATH");
653        println!("cargo:rerun-if-changed={}", board_config_path.display());
654    }
655
656    Ok(())
657}
658
659#[cfg(test)]
660fn root_path_prefix() -> std::path::PathBuf {
661    #[cfg(windows)]
662    return std::path::PathBuf::from("C:/");
663    #[cfg(not(windows))]
664    return std::path::PathBuf::from("/");
665}
666
667#[test]
668fn with_absolute_library_paths_test() {
669    use std::path::PathBuf;
670
671    let library_paths = std::collections::HashMap::from([
672        ("relative".to_string(), PathBuf::from("some/relative/path")),
673        ("absolute".to_string(), root_path_prefix().join("some/absolute/path")),
674    ]);
675    let config = CompilerConfiguration::new().with_library_paths(library_paths);
676
677    let manifest_path = root_path_prefix().join("path/to/manifest");
678    let absolute_config = config.clone().with_absolute_paths(&manifest_path);
679    let relative = &absolute_config.config.library_paths["relative"];
680    assert!(relative.is_absolute());
681    assert!(relative.starts_with(&manifest_path));
682
683    assert!(!absolute_config.config.library_paths["absolute"].starts_with(&manifest_path));
684}
685
686#[test]
687fn with_absolute_include_paths_test() {
688    use std::path::PathBuf;
689
690    let config = CompilerConfiguration::new().with_include_paths(Vec::from([
691        root_path_prefix().join("some/absolute/path"),
692        PathBuf::from("some/relative/path"),
693    ]));
694
695    let manifest_path = root_path_prefix().join("path/to/manifest");
696    let absolute_config = config.clone().with_absolute_paths(&manifest_path);
697    assert_eq!(
698        absolute_config.config.include_paths,
699        Vec::from([
700            root_path_prefix().join("some/absolute/path"),
701            manifest_path.join("some/relative/path"),
702        ])
703    )
704}