Skip to main content

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