diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index 73a05ba3d5..f46cde9011 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -1,5 +1,6 @@ use glob::PatternError; use ruff_notebook::{Notebook, NotebookError}; +use rustc_hash::FxHashMap; use std::panic::RefUnwindSafe; use std::sync::{Arc, Mutex}; @@ -20,18 +21,44 @@ use super::walk_directory::WalkDirectoryBuilder; /// /// ## Warning /// Don't use this system for production code. It's intended for testing only. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct TestSystem { inner: Arc, + /// Environment variable overrides. If a key is present here, it takes precedence + /// over the inner system's environment variables. + env_overrides: Arc>>>, +} + +impl Clone for TestSystem { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + env_overrides: self.env_overrides.clone(), + } + } } impl TestSystem { pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self { Self { inner: Arc::new(inner), + env_overrides: Arc::new(Mutex::new(FxHashMap::default())), } } + /// Sets an environment variable override. This takes precedence over the inner system. + pub fn set_env_var(&self, name: impl Into, value: impl Into) { + self.env_overrides + .lock() + .unwrap() + .insert(name.into(), Some(value.into())); + } + + /// Removes an environment variable override, making it appear as not set. + pub fn remove_env_var(&self, name: impl Into) { + self.env_overrides.lock().unwrap().insert(name.into(), None); + } + /// Returns the [`InMemorySystem`]. /// /// ## Panics @@ -147,6 +174,18 @@ impl System for TestSystem { self.system().case_sensitivity() } + fn env_var(&self, name: &str) -> std::result::Result { + // Check overrides first + if let Some(override_value) = self.env_overrides.lock().unwrap().get(name) { + return match override_value { + Some(value) => Ok(value.clone()), + None => Err(std::env::VarError::NotPresent), + }; + } + // Fall back to inner system + self.system().env_var(name) + } + fn dyn_clone(&self) -> Box { Box::new(self.clone()) } @@ -156,6 +195,7 @@ impl Default for TestSystem { fn default() -> Self { Self { inner: Arc::new(InMemorySystem::default()), + env_overrides: Arc::new(Mutex::new(FxHashMap::default())), } } } diff --git a/crates/ty/tests/cli/python_environment.rs b/crates/ty/tests/cli/python_environment.rs index bd15d1076a..5be56716b3 100644 --- a/crates/ty/tests/cli/python_environment.rs +++ b/crates/ty/tests/cli/python_environment.rs @@ -2703,3 +2703,51 @@ fn pythonpath_multiple_dirs_is_respected() -> anyhow::Result<()> { Ok(()) } + +/// Test behavior when `VIRTUAL_ENV` is set but points to a non-existent path. +#[test] +fn missing_virtual_env() -> anyhow::Result<()> { + let working_venv_package1_path = if cfg!(windows) { + "project/.venv/Lib/site-packages/package1/__init__.py" + } else { + "project/.venv/lib/python3.13/site-packages/package1/__init__.py" + }; + + let case = CliTest::with_files([ + ( + "project/test.py", + r#" + from package1 import WorkingVenv + "#, + ), + ( + "project/.venv/pyvenv.cfg", + r#" +home = ./ + + "#, + ), + ( + working_venv_package1_path, + r#" + class WorkingVenv: ... + "#, + ), + ])?; + + assert_cmd_snapshot!(case.command() + .current_dir(case.root().join("project")) + .env("VIRTUAL_ENV", case.root().join("nonexistent-venv")), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + ty failed + Cause: Failed to discover local Python environment + Cause: Invalid `VIRTUAL_ENV` environment variable `/nonexistent-venv`: does not point to a directory on disk + Cause: No such file or directory (os error 2) + "); + + Ok(()) +} diff --git a/crates/ty_project/src/metadata.rs b/crates/ty_project/src/metadata.rs index 62931b3e64..cb6c7b48f7 100644 --- a/crates/ty_project/src/metadata.rs +++ b/crates/ty_project/src/metadata.rs @@ -5,7 +5,7 @@ use ruff_python_ast::name::Name; use std::sync::Arc; use thiserror::Error; use ty_combine::Combine; -use ty_python_semantic::ProgramSettings; +use ty_python_semantic::{MisconfigurationMode, ProgramSettings}; use crate::metadata::options::ProjectOptionsOverrides; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; @@ -37,6 +37,9 @@ pub struct ProjectMetadata { /// The path ordering doesn't imply precedence. #[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))] pub(super) extra_configuration_paths: Vec, + + #[cfg_attr(test, serde(skip))] + pub(super) misconfiguration_mode: MisconfigurationMode, } impl ProjectMetadata { @@ -47,6 +50,7 @@ impl ProjectMetadata { root, extra_configuration_paths: Vec::default(), options: Options::default(), + misconfiguration_mode: MisconfigurationMode::Fail, } } @@ -70,6 +74,7 @@ impl ProjectMetadata { root: system.current_directory().to_path_buf(), options, extra_configuration_paths: vec![path], + misconfiguration_mode: MisconfigurationMode::Fail, }) } @@ -82,6 +87,7 @@ impl ProjectMetadata { pyproject.tool.and_then(|tool| tool.ty).unwrap_or_default(), root, pyproject.project.as_ref(), + MisconfigurationMode::Fail, ) } @@ -90,6 +96,7 @@ impl ProjectMetadata { mut options: Options, root: SystemPathBuf, project: Option<&Project>, + misconfiguration_mode: MisconfigurationMode, ) -> Result { let name = project .and_then(|project| project.name.as_deref()) @@ -117,6 +124,7 @@ impl ProjectMetadata { root, options, extra_configuration_paths: Vec::new(), + misconfiguration_mode, }) } @@ -194,6 +202,7 @@ impl ProjectMetadata { pyproject .as_ref() .and_then(|pyproject| pyproject.project.as_ref()), + MisconfigurationMode::Fail, ) .map_err(|err| { ProjectMetadataError::InvalidRequiresPythonConstraint { @@ -273,8 +282,13 @@ impl ProjectMetadata { system: &dyn System, vendored: &VendoredFileSystem, ) -> anyhow::Result { - self.options - .to_program_settings(self.root(), self.name(), system, vendored) + self.options.to_program_settings( + self.root(), + self.name(), + system, + vendored, + self.misconfiguration_mode, + ) } pub fn apply_overrides(&mut self, overrides: &ProjectOptionsOverrides) { diff --git a/crates/ty_project/src/metadata/options.rs b/crates/ty_project/src/metadata/options.rs index 4aeec8bb60..3220e9c925 100644 --- a/crates/ty_project/src/metadata/options.rs +++ b/crates/ty_project/src/metadata/options.rs @@ -30,9 +30,9 @@ use thiserror::Error; use ty_combine::Combine; use ty_python_semantic::lint::{Level, LintSource, RuleSelection}; use ty_python_semantic::{ - ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionFileSource, - PythonVersionSource, PythonVersionWithSource, SearchPathSettings, SearchPathValidationError, - SearchPaths, SitePackagesPaths, SysPrefixPathOrigin, + MisconfigurationMode, ProgramSettings, PythonEnvironment, PythonPlatform, + PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, + SearchPathValidationError, SearchPaths, SitePackagesPaths, SysPrefixPathOrigin, }; use ty_static::EnvVars; @@ -117,6 +117,7 @@ impl Options { project_name: &str, system: &dyn System, vendored: &VendoredFileSystem, + misconfiguration_mode: MisconfigurationMode, ) -> anyhow::Result { let environment = self.environment.or_default(); @@ -154,14 +155,25 @@ impl Options { ValueSource::Editor => SysPrefixPathOrigin::Editor, }; - Some(PythonEnvironment::new( - python_path.absolute(project_root, system), - origin, - system, - )?) + PythonEnvironment::new(python_path.absolute(project_root, system), origin, system) + .map_err(anyhow::Error::from) + .map(Some) } else { PythonEnvironment::discover(project_root, system) - .context("Failed to discover local Python environment")? + .context("Failed to discover local Python environment") + }; + + // If in safe-mode, fallback to None if this fails instead of erroring. + let python_environment = match python_environment { + Ok(python_environment) => python_environment, + Err(err) => { + if misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!("Default settings failed to discover local Python environment"); + None + } else { + return Err(err); + } + } }; let self_site_packages = self_environment_search_paths( @@ -174,11 +186,23 @@ impl Options { .unwrap_or_default(); let site_packages_paths = if let Some(python_environment) = python_environment.as_ref() { - self_site_packages.concatenate( - python_environment - .site_packages_paths(system) - .context("Failed to discover the site-packages directory")?, - ) + let site_packages_paths = python_environment + .site_packages_paths(system) + .context("Failed to discover the site-packages directory"); + let site_packages_paths = match site_packages_paths { + Ok(paths) => paths, + Err(err) => { + if misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!( + "Default settings failed to discover site-packages directory" + ); + SitePackagesPaths::default() + } else { + return Err(err); + } + } + }; + self_site_packages.concatenate(site_packages_paths) } else { tracing::debug!("No virtual environment found"); self_site_packages @@ -201,6 +225,7 @@ impl Options { .or_else(|| site_packages_paths.python_version_from_layout()) .unwrap_or_default(); + // Safe mode is handled inside this function, so we just assume this can't fail let search_paths = self.to_search_paths( project_root, project_name, @@ -208,6 +233,7 @@ impl Options { real_stdlib_path, system, vendored, + misconfiguration_mode, )?; tracing::info!( @@ -222,6 +248,7 @@ impl Options { }) } + #[expect(clippy::too_many_arguments)] fn to_search_paths( &self, project_root: &SystemPath, @@ -230,6 +257,7 @@ impl Options { real_stdlib_path: Option, system: &dyn System, vendored: &VendoredFileSystem, + misconfiguration_mode: MisconfigurationMode, ) -> Result { let environment = self.environment.or_default(); let src = self.src.or_default(); @@ -344,6 +372,7 @@ impl Options { .map(|path| path.absolute(project_root, system)), site_packages_paths: site_packages_paths.into_vec(), real_stdlib_path, + misconfiguration_mode, }; settings.to_search_paths(system, vendored) diff --git a/crates/ty_python_semantic/src/lib.rs b/crates/ty_python_semantic/src/lib.rs index cf386839c9..648157e334 100644 --- a/crates/ty_python_semantic/src/lib.rs +++ b/crates/ty_python_semantic/src/lib.rs @@ -17,7 +17,7 @@ pub use module_resolver::{ resolve_real_module_confident, resolve_real_shadowable_module, system_module_search_paths, }; pub use program::{ - Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource, + MisconfigurationMode, Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource, PythonVersionWithSource, SearchPathSettings, }; pub use python_platform::PythonPlatform; diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index 4be43b4453..dd9e534375 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -50,6 +50,7 @@ use ruff_python_ast::{ use crate::db::Db; use crate::module_name::ModuleName; use crate::module_resolver::typeshed::{TypeshedVersions, vendored_typeshed_versions}; +use crate::program::MisconfigurationMode; use crate::{Program, SearchPathSettings}; use super::module::{Module, ModuleKind}; @@ -570,6 +571,7 @@ impl SearchPaths { custom_typeshed: typeshed, site_packages_paths, real_stdlib_path, + misconfiguration_mode, } = settings; let mut static_paths = vec![]; @@ -578,12 +580,30 @@ impl SearchPaths { let path = canonicalize(path, system); tracing::debug!("Adding extra search-path `{path}`"); - static_paths.push(SearchPath::extra(system, path)?); + match SearchPath::extra(system, path) { + Ok(path) => static_paths.push(path), + Err(err) => { + if *misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!("Skipping invalid extra search-path: {err}"); + } else { + return Err(err); + } + } + } } for src_root in src_roots { tracing::debug!("Adding first-party search path `{src_root}`"); - static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?); + match SearchPath::first_party(system, src_root.to_path_buf()) { + Ok(path) => static_paths.push(path), + Err(err) => { + if *misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!("Skipping invalid first-party search-path: {err}"); + } else { + return Err(err); + } + } + } } let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed { @@ -592,18 +612,31 @@ impl SearchPaths { let versions_path = typeshed.join("stdlib/VERSIONS"); - let versions_content = system.read_to_string(&versions_path).map_err(|error| { - SearchPathValidationError::FailedToReadVersionsFile { - path: versions_path, - error, + let results = system + .read_to_string(&versions_path) + .map_err( + |error| SearchPathValidationError::FailedToReadVersionsFile { + path: versions_path, + error, + }, + ) + .and_then(|versions_content| Ok(versions_content.parse()?)) + .and_then(|parsed| Ok((parsed, SearchPath::custom_stdlib(system, &typeshed)?))); + + match results { + Ok(results) => results, + Err(err) => { + if settings.misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!("Skipping custom-stdlib search-path: {err}"); + ( + vendored_typeshed_versions(vendored), + SearchPath::vendored_stdlib(), + ) + } else { + return Err(err); + } } - })?; - - let parsed: TypeshedVersions = versions_content.parse()?; - - let search_path = SearchPath::custom_stdlib(system, &typeshed)?; - - (parsed, search_path) + } } else { tracing::debug!("Using vendored stdlib"); ( @@ -613,7 +646,17 @@ impl SearchPaths { }; let real_stdlib_path = if let Some(path) = real_stdlib_path { - Some(SearchPath::real_stdlib(system, path.clone())?) + match SearchPath::real_stdlib(system, path.clone()) { + Ok(path) => Some(path), + Err(err) => { + if *misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!("Skipping invalid real-stdlib search-path: {err}"); + None + } else { + return Err(err); + } + } + } } else { None }; @@ -622,7 +665,16 @@ impl SearchPaths { for path in site_packages_paths { tracing::debug!("Adding site-packages search path `{path}`"); - site_packages.push(SearchPath::site_packages(system, path.clone())?); + match SearchPath::site_packages(system, path.clone()) { + Ok(path) => site_packages.push(path), + Err(err) => { + if settings.misconfiguration_mode == MisconfigurationMode::UseDefault { + tracing::debug!("Skipping invalid real-stdlib search-path: {err}"); + } else { + return Err(err); + } + } + } } // TODO vendor typeshed's third-party stubs as well as the stdlib and diff --git a/crates/ty_python_semantic/src/program.rs b/crates/ty_python_semantic/src/program.rs index 1a977de985..e27b6c64c1 100644 --- a/crates/ty_python_semantic/src/program.rs +++ b/crates/ty_python_semantic/src/program.rs @@ -163,6 +163,17 @@ impl Default for PythonVersionWithSource { } } +#[derive(PartialEq, Eq, Debug, Copy, Clone, get_size2::GetSize)] +pub enum MisconfigurationMode { + /// Settings Failure Is Not An Error. + /// + /// This is used by the default database, which we are incentivized to make infallible, + /// while still trying to "do our best" to set things up properly where we can. + UseDefault, + /// Settings Failure Is An Error. + Fail, +} + /// Configures the search paths for module resolution. #[derive(Eq, PartialEq, Debug, Clone)] pub struct SearchPathSettings { @@ -187,6 +198,9 @@ pub struct SearchPathSettings { /// We should ideally only ever use this for things like goto-definition, /// where typeshed isn't the right answer. pub real_stdlib_path: Option, + + /// How to handle apparent misconfiguration + pub misconfiguration_mode: MisconfigurationMode, } impl SearchPathSettings { @@ -204,6 +218,7 @@ impl SearchPathSettings { custom_typeshed: None, site_packages_paths: vec![], real_stdlib_path: None, + misconfiguration_mode: MisconfigurationMode::Fail, } } diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index 18b211a8cc..29f301b799 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -28,6 +28,7 @@ use ty_project::{ChangeResult, CheckMode, Db as _, ProjectDatabase, ProjectMetad use index::DocumentError; use options::GlobalOptions; +use ty_python_semantic::MisconfigurationMode; pub(crate) use self::options::InitializationOptions; pub use self::options::{ClientOptions, DiagnosticMode}; @@ -512,11 +513,15 @@ impl Session { Please refer to the logs for more details.", )); - let db_with_default_settings = - ProjectMetadata::from_options(Options::default(), root, None) - .context("Failed to convert default options to metadata") - .and_then(|metadata| ProjectDatabase::new(metadata, system)) - .expect("Default configuration to be valid"); + let db_with_default_settings = ProjectMetadata::from_options( + Options::default(), + root, + None, + MisconfigurationMode::UseDefault, + ) + .context("Failed to convert default options to metadata") + .and_then(|metadata| ProjectDatabase::new(metadata, system)) + .expect("Default configuration to be valid"); let default_root = db_with_default_settings .project() .root(&db_with_default_settings) @@ -1230,6 +1235,7 @@ impl DefaultProject { Options::default(), system.current_directory().to_path_buf(), None, + MisconfigurationMode::UseDefault, ) .unwrap(); diff --git a/crates/ty_server/tests/e2e/initialize.rs b/crates/ty_server/tests/e2e/initialize.rs index fb715bf929..6e7098ed0a 100644 --- a/crates/ty_server/tests/e2e/initialize.rs +++ b/crates/ty_server/tests/e2e/initialize.rs @@ -1,5 +1,6 @@ use anyhow::Result; -use lsp_types::{Position, notification::ShowMessage, request::RegisterCapability}; +use lsp_types::notification::ShowMessage; +use lsp_types::{Position, request::RegisterCapability}; use ruff_db::system::SystemPath; use serde_json::Value; use ty_server::{ClientOptions, DiagnosticMode}; @@ -474,3 +475,27 @@ fn register_multiple_capabilities() -> Result<()> { Ok(()) } + +/// Tests that the server doesn't panic when `VIRTUAL_ENV` points to a non-existent directory. +/// +/// See: +#[test] +fn missing_virtual_env_does_not_panic() -> Result<()> { + let workspace_root = SystemPath::new("project"); + + // This should not panic even though VIRTUAL_ENV points to a non-existent path + let mut server = TestServerBuilder::new()? + .with_workspace(workspace_root, None)? + .with_env_var("VIRTUAL_ENV", "/nonexistent/virtual/env/path") + .build() + .wait_until_workspaces_are_initialized(); + + let _show_message_params = server.await_notification::(); + + // Something accursed in the escaping pipeline produces `\/` in windows paths + // and I can't for the life of me get insta to escape it properly, so I just + // need to move on with my life and not debug this right now, but ideally we + // would snapshot the message here. + + Ok(()) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index f939bd96b4..4dc0831129 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -211,6 +211,7 @@ impl TestServer { test_context: TestContext, capabilities: ClientCapabilities, initialization_options: Option, + env_vars: Vec<(String, String)>, ) -> Self { setup_tracing(); @@ -221,11 +222,16 @@ impl TestServer { // Create OS system with the test directory as cwd let os_system = OsSystem::new(test_context.root()); + // Create test system and set environment variable overrides + let test_system = Arc::new(TestSystem::new(os_system)); + for (name, value) in env_vars { + test_system.set_env_var(name, value); + } + // Start the server in a separate thread let server_thread = std::thread::spawn(move || { // TODO: This should probably be configurable to test concurrency issues let worker_threads = NonZeroUsize::new(1).unwrap(); - let test_system = Arc::new(TestSystem::new(os_system)); match Server::new(worker_threads, server_connection, test_system, true) { Ok(server) => { @@ -1067,6 +1073,7 @@ pub(crate) struct TestServerBuilder { workspaces: Vec<(WorkspaceFolder, Option)>, initialization_options: Option, client_capabilities: ClientCapabilities, + env_vars: Vec<(String, String)>, } impl TestServerBuilder { @@ -1097,6 +1104,7 @@ impl TestServerBuilder { test_context: TestContext::new()?, initialization_options: None, client_capabilities, + env_vars: Vec::new(), }) } @@ -1106,6 +1114,16 @@ impl TestServerBuilder { self } + /// Set an environment variable for the test server's system. + pub(crate) fn with_env_var( + mut self, + name: impl Into, + value: impl Into, + ) -> Self { + self.env_vars.push((name.into(), value.into())); + self + } + /// Add a workspace to the test server with the given root path and options. /// /// This option will be used to respond to the `workspace/configuration` request that the @@ -1262,6 +1280,7 @@ impl TestServerBuilder { self.test_context, self.client_capabilities, self.initialization_options, + self.env_vars, ) } } diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index ad49261739..312a00fc32 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -19,9 +19,9 @@ use std::fmt::{Display, Write}; use ty_python_semantic::pull_types::pull_types; use ty_python_semantic::types::{UNDEFINED_REVEAL, check_types}; use ty_python_semantic::{ - Module, Program, ProgramSettings, PythonEnvironment, PythonPlatform, PythonVersionSource, - PythonVersionWithSource, SearchPath, SearchPathSettings, SysPrefixPathOrigin, list_modules, - resolve_module_confident, + MisconfigurationMode, Module, Program, ProgramSettings, PythonEnvironment, PythonPlatform, + PythonVersionSource, PythonVersionWithSource, SearchPath, SearchPathSettings, + SysPrefixPathOrigin, list_modules, resolve_module_confident, }; mod assertion; @@ -441,6 +441,7 @@ fn run_test( custom_typeshed: custom_typeshed_path.map(SystemPath::to_path_buf), site_packages_paths, real_stdlib_path: None, + misconfiguration_mode: MisconfigurationMode::Fail, } .to_search_paths(db.system(), db.vendored()) .expect("Failed to resolve search path settings"), diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 1ae085581c..addc3897de 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -26,7 +26,7 @@ use ty_project::metadata::value::ValueSource; use ty_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind}; use ty_project::{CheckMode, ProjectMetadata}; use ty_project::{Db, ProjectDatabase}; -use ty_python_semantic::Program; +use ty_python_semantic::{MisconfigurationMode, Program}; use wasm_bindgen::prelude::*; #[wasm_bindgen] @@ -99,8 +99,13 @@ impl Workspace { let system = WasmSystem::new(SystemPath::new(root)); - let project = ProjectMetadata::from_options(options, SystemPathBuf::from(root), None) - .map_err(into_error)?; + let project = ProjectMetadata::from_options( + options, + SystemPathBuf::from(root), + None, + MisconfigurationMode::Fail, + ) + .map_err(into_error)?; let mut db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?; @@ -127,6 +132,7 @@ impl Workspace { options, self.db.project().root(&self.db).to_path_buf(), None, + MisconfigurationMode::Fail, ) .map_err(into_error)?;