diff --git a/crates/pixi_manifest/src/discovery.rs b/crates/pixi_manifest/src/discovery.rs index c00c894aa3..0ad5a81d66 100644 --- a/crates/pixi_manifest/src/discovery.rs +++ b/crates/pixi_manifest/src/discovery.rs @@ -565,7 +565,13 @@ mod test { &mut snapshot, "Discovered workspace at: {}\n- Name: {}", rel_path.display().to_string().replace("\\", "/"), - &discovered.workspace.value.workspace.name + &discovered + .workspace + .value + .workspace + .name + .as_deref() + .unwrap_or("??") ) .unwrap(); @@ -620,7 +626,13 @@ mod test { &mut snapshot, "Discovered workspace at: {}\n- Name: {}", rel_path.display().to_string().replace("\\", "/"), - &discovered.workspace.value.workspace.name + &discovered + .workspace + .value + .workspace + .name + .as_deref() + .unwrap_or("??") ) .unwrap(); diff --git a/crates/pixi_manifest/src/manifests/workspace.rs b/crates/pixi_manifest/src/manifests/workspace.rs index 7c60642527..3e26f67d62 100644 --- a/crates/pixi_manifest/src/manifests/workspace.rs +++ b/crates/pixi_manifest/src/manifests/workspace.rs @@ -656,7 +656,7 @@ impl WorkspaceManifestMut<'_> { /// This function modifies both the workspace and the TOML document. Use /// `ManifestProvenance::save` to persist the changes to disk. pub fn set_name(&mut self, name: &str) -> miette::Result<()> { - self.workspace.workspace.name = name.to_string(); + self.workspace.workspace.name = Some(name.to_string()); self.document.set_name(name); Ok(()) } diff --git a/crates/pixi_manifest/src/pyproject.rs b/crates/pixi_manifest/src/pyproject.rs index 3476074836..be92cd0da2 100644 --- a/crates/pixi_manifest/src/pyproject.rs +++ b/crates/pixi_manifest/src/pyproject.rs @@ -392,8 +392,11 @@ impl PyProjectManifest { // For each group of optional dependency or dependency group, add pypi // dependencies, filtering out self-references in optional dependencies - let project_name = - pep508_rs::PackageName::new(workspace_manifest.workspace.name.clone()).ok(); + let project_name = workspace_manifest + .workspace + .name + .clone() + .and_then(|name| pep508_rs::PackageName::new(name).ok()); for (group, reqs) in pypi_dependency_groups { let feature_name = FeatureName::Named(group.to_string()); let target = workspace_manifest diff --git a/crates/pixi_manifest/src/toml/manifest.rs b/crates/pixi_manifest/src/toml/manifest.rs index 13926de886..ac8a91921c 100644 --- a/crates/pixi_manifest/src/toml/manifest.rs +++ b/crates/pixi_manifest/src/toml/manifest.rs @@ -427,7 +427,7 @@ impl TomlManifest { warnings: mut package_warnings, } = package.into_manifest( ExternalPackageProperties { - name: Some(workspace.name.clone()), + name: workspace.name.clone(), version: workspace.version.clone(), description: workspace.description.clone(), authors: workspace.authors.clone(), @@ -606,19 +606,34 @@ mod test { } #[test] - fn test_workspace_name_required() { + fn test_missing_package_name() { assert_snapshot!(expect_parse_failure( r#" [workspace] channels = [] platforms = [] preview = ["pixi-build"] + + [package] + # Since workspace doesnt define a name we expect an error here. + + [package.build] + backend = { name = "foobar", version = "*" } "#, - )); + ), @r###" + × missing field 'name' in table + ╭─[pixi.toml:7:9] + 6 │ + 7 │ [package] + · ────────── + 8 │ # Since workspace doesnt define a name we expect an error here. + 9 │ + ╰──── + "###); } #[test] - fn test_workspace_name_from_workspace() { + fn test_workspace_name_from_package() { let workspace_manifest = WorkspaceManifest::from_toml_str( r#" [workspace] @@ -636,7 +651,7 @@ mod test { ) .unwrap(); - assert_eq!(workspace_manifest.workspace.name, "foo"); + assert_eq!(workspace_manifest.workspace.name.as_deref(), Some("foo")); } #[test] diff --git a/crates/pixi_manifest/src/toml/workspace.rs b/crates/pixi_manifest/src/toml/workspace.rs index 260331b823..303765a7a7 100644 --- a/crates/pixi_manifest/src/toml/workspace.rs +++ b/crates/pixi_manifest/src/toml/workspace.rs @@ -16,7 +16,7 @@ use indexmap::{IndexMap, IndexSet}; use pixi_spec::TomlVersionSpecStr; use pixi_toml::{TomlFromStr, TomlHashMap, TomlIndexMap, TomlIndexSet, TomlWith}; use rattler_conda_types::{NamedChannelOrUrl, Platform, Version, VersionSpec}; -use toml_span::{de_helpers::TableHelper, DeserError, Error, ErrorKind, Span, Spanned, Value}; +use toml_span::{de_helpers::TableHelper, DeserError, Span, Spanned, Value}; use url::Url; #[derive(Debug, Clone)] @@ -109,11 +109,7 @@ impl TomlWorkspace { let warnings = preview_warnings; Ok(WithWarnings::from(Workspace { - name: self.name.or(external.name).ok_or(Error { - kind: ErrorKind::MissingField("name"), - span: self.span, - line_info: None, - })?, + name: self.name.or(external.name), version: self.version.or(external.version), description: self.description.or(external.description), authors: self.authors.or(external.authors), @@ -246,9 +242,8 @@ mod test { use insta::assert_snapshot; - use crate::toml::manifest::ExternalWorkspaceProperties; use crate::{ - toml::{FromTomlStr, TomlWorkspace}, + toml::{manifest::ExternalWorkspaceProperties, FromTomlStr, TomlWorkspace}, utils::test_utils::{expect_parse_failure, format_parse_error}, }; diff --git a/crates/pixi_manifest/src/workspace.rs b/crates/pixi_manifest/src/workspace.rs index 08e0e4a336..42ccb05f08 100644 --- a/crates/pixi_manifest/src/workspace.rs +++ b/crates/pixi_manifest/src/workspace.rs @@ -11,10 +11,10 @@ use super::pypi::pypi_options::PypiOptions; use crate::{preview::Preview, PrioritizedChannel, S3Options, Targets}; /// Describes the contents of the `[workspace]` section of the project manifest. -#[derive(Debug, Clone)] +#[derive(Debug, Default, Clone)] pub struct Workspace { /// The name of the project - pub name: String, + pub name: Option, /// The version of the project pub version: Option, diff --git a/docs/reference/pixi_manifest.md b/docs/reference/pixi_manifest.md index 84230242eb..fc32ad995a 100644 --- a/docs/reference/pixi_manifest.md +++ b/docs/reference/pixi_manifest.md @@ -36,14 +36,6 @@ The minimally required information in the `project` table is: --8<-- "docs/source_files/pixi_tomls/simple_pixi.toml:project" ``` -### `name` - -The name of the project. - -```toml ---8<-- "docs/source_files/pixi_tomls/main_pixi.toml:project_name" -``` - ### `channels` This is a list that defines the channels used to fetch the packages from. @@ -82,6 +74,15 @@ The available platforms are listed here: [link](https://docs.rs/rattler_conda_ty Fallback: If `osx-arm64` can't resolve, use `osx-64`. Running `osx-64` on Apple Silicon uses [Rosetta](https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment) for Intel binaries. +### `name` (optional) + +The name of the project. +If the name is not specified, the name of the directory that contains the project is used. + +```toml +--8<-- "docs/source_files/pixi_tomls/main_pixi.toml:project_name" +``` + ### `version` (optional) The version of the project. diff --git a/src/activation.rs b/src/activation.rs index 4d47f2428e..5c1bed1eb1 100644 --- a/src/activation.rs +++ b/src/activation.rs @@ -48,7 +48,10 @@ impl Workspace { format!("{PROJECT_PREFIX}ROOT"), self.root().to_string_lossy().into_owned(), ), - (format!("{PROJECT_PREFIX}NAME"), self.name().to_string()), + ( + format!("{PROJECT_PREFIX}NAME"), + self.display_name().to_string(), + ), ( format!("{PROJECT_PREFIX}MANIFEST"), self.workspace @@ -89,9 +92,9 @@ impl Environment<'_> { pub(crate) fn get_metadata_env(&self) -> IndexMap { let prompt = match self.name() { EnvironmentName::Named(name) => { - format!("{}:{}", self.workspace().name(), name) + format!("{}:{}", self.workspace().display_name(), name) } - EnvironmentName::Default => self.workspace().name().to_string(), + EnvironmentName::Default => self.workspace().display_name().to_string(), }; let mut map = IndexMap::from_iter([ (format!("{ENV_PREFIX}NAME"), self.name().to_string()), @@ -364,8 +367,10 @@ pub(crate) fn get_static_environment_variables<'p>( // Add the conda default env variable so that the existing tools know about the env. let env_name = match environment.name() { - EnvironmentName::Named(name) => format!("{}:{}", environment.workspace().name(), name), - EnvironmentName::Default => environment.workspace().name().to_string(), + EnvironmentName::Named(name) => { + format!("{}:{}", environment.workspace().display_name(), name) + } + EnvironmentName::Default => environment.workspace().display_name().to_string(), }; let mut shell_env = HashMap::new(); shell_env.insert("CONDA_DEFAULT_ENV".to_string(), env_name); @@ -517,7 +522,10 @@ mod tests { let project = Workspace::from_str(Path::new("pixi.toml"), project).unwrap(); let env = project.get_metadata_env(); - assert_eq!(env.get("PIXI_PROJECT_NAME").unwrap(), project.name()); + assert_eq!( + env.get("PIXI_PROJECT_NAME").unwrap(), + project.display_name() + ); assert_eq!( env.get("PIXI_PROJECT_ROOT").unwrap(), project.root().to_str().unwrap() diff --git a/src/cli/build.rs b/src/cli/build.rs index 78344a0c54..19a26bf1e8 100644 --- a/src/cli/build.rs +++ b/src/cli/build.rs @@ -178,7 +178,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { (Some(tmp), work_dir) }; - let progress = ProgressReporter::new(workspace.name()); + let progress = ProgressReporter::new(workspace.display_name()); // Build platform virtual packages let build_platform_virtual_packages: Vec = workspace diff --git a/src/cli/info.rs b/src/cli/info.rs index d9b271ea2f..b01b90d021 100644 --- a/src/cli/info.rs +++ b/src/cli/info.rs @@ -391,7 +391,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { }; let project_info = workspace.clone().map(|p| WorkspaceInfo { - name: p.name().to_string(), + name: p.display_name().to_string(), manifest_path: p.workspace.provenance.path.clone(), last_updated: last_updated(p.lock_file_path()).ok(), pixi_folder_size, diff --git a/src/cli/shell.rs b/src/cli/shell.rs index 92fd12497f..598ae07971 100644 --- a/src/cli/shell.rs +++ b/src/cli/shell.rs @@ -312,7 +312,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { tracing::info!("Starting shell: {:?}", interactive_shell); let prompt_hook = if workspace.config().change_ps1() { - let prompt_name = prompt::prompt_name(workspace.name(), environment.name()); + let prompt_name = prompt::prompt_name(workspace.display_name(), environment.name()); [ prompt::shell_prompt(&interactive_shell, prompt_name.as_str()), prompt::shell_hook(&interactive_shell) diff --git a/src/cli/shell_hook.rs b/src/cli/shell_hook.rs index eb4979e70a..4e7a7665a3 100644 --- a/src/cli/shell_hook.rs +++ b/src/cli/shell_hook.rs @@ -109,7 +109,7 @@ async fn generate_activation_script( let hook = prompt::shell_hook(&shell).unwrap_or_default().to_owned(); if project.config().change_ps1() { - let prompt_name = prompt::prompt_name(project.name(), environment.name()); + let prompt_name = prompt::prompt_name(project.display_name(), environment.name()); let shell_prompt = prompt::shell_prompt(&shell, prompt_name.as_str()); Ok([script, hook, shell_prompt].join("\n")) } else { diff --git a/src/cli/workspace/name/get.rs b/src/cli/workspace/name/get.rs index 8782a970ac..a7ff75679a 100644 --- a/src/cli/workspace/name/get.rs +++ b/src/cli/workspace/name/get.rs @@ -1,6 +1,6 @@ use crate::Workspace; pub async fn execute(workspace: Workspace) -> miette::Result<()> { - println!("{}", workspace.name()); + println!("{}", workspace.display_name()); Ok(()) } diff --git a/src/cli/workspace/name/set.rs b/src/cli/workspace/name/set.rs index f576122bfe..ec92be65a4 100644 --- a/src/cli/workspace/name/set.rs +++ b/src/cli/workspace/name/set.rs @@ -22,7 +22,12 @@ pub async fn execute(workspace: Workspace, args: Args) -> miette::Result<()> { eprintln!( "{}Updated workspace name to '{}'.", console::style(console::Emoji("✔ ", "")).green(), - workspace.workspace.value.workspace.name + workspace + .workspace + .value + .workspace + .name + .expect("workspace name must have been set") ); Ok(()) diff --git a/src/global/project/mod.rs b/src/global/project/mod.rs index cafa2f0951..ba97442ce4 100644 --- a/src/global/project/mod.rs +++ b/src/global/project/mod.rs @@ -1293,7 +1293,7 @@ impl Repodata for Project { mod tests { use std::{collections::HashMap, io::Write}; - use fake::{faker::filesystem::zh_tw::FilePath, Fake}; + use fake::{faker::filesystem::en::FilePath, Fake}; use itertools::Itertools; use rattler_conda_types::{ NamedChannelOrUrl, PackageRecord, Platform, RepoDataRecord, VersionWithSource, diff --git a/src/workspace/environment.rs b/src/workspace/environment.rs index 0d7c6d383d..150e80a8ea 100644 --- a/src/workspace/environment.rs +++ b/src/workspace/environment.rs @@ -45,7 +45,7 @@ pub struct Environment<'p> { impl Debug for Environment<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Environment") - .field("project", &self.workspace.name()) + .field("project", &self.workspace.display_name()) .field("environment", &self.environment.name) .finish() } diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index c32562653c..1fc0eac9a8 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -128,6 +128,11 @@ pub struct Workspace { /// Root folder of the workspace root: PathBuf, + /// The name of the workspace based on the location of the workspace. + /// This is used to determine the name of the workspace when no name is + /// specified. + manifest_location_name: Option, + /// Reqwest client shared for this workspace. /// This is wrapped in a `OnceLock` to allow for lazy initialization. // TODO: once https://github.com/rust-lang/rust/issues/109737 is stabilized, switch to OnceLock @@ -195,6 +200,9 @@ impl Workspace { .expect("manifest path should always have a parent") .to_owned(); + // Determine the name of the workspace based on the location of the manifest. + let manifest_location_name = root.file_name().map(|p| p.to_string_lossy().into_owned()); + let s3_options = manifest.workspace.value.workspace.s3_options.clone(); let s3_config = s3_options .unwrap_or_default() @@ -214,6 +222,7 @@ impl Workspace { let config = Config::load(&root); Self { root, + manifest_location_name, client: Default::default(), workspace: manifest.workspace, package: manifest.package, @@ -269,9 +278,22 @@ impl Workspace { WorkspaceMut::new(self) } - /// Returns the name of the workspace - pub fn name(&self) -> &str { - &self.workspace.value.workspace.name + /// Returns the display name of the workspace. This name should be used to + /// provide context to a user. + /// + /// This is the name of the workspace as defined in the manifest, or if no + /// name is specified the name of the root directory of the workspace. + /// + /// If the name of the root directory could not be determined, "workspace" + /// is used as a fallback. + pub fn display_name(&self) -> &str { + self.workspace + .value + .workspace + .name + .as_deref() + .or(self.manifest_location_name.as_deref()) + .unwrap_or("workspace") } /// Returns the root directory of the workspace @@ -290,7 +312,7 @@ impl Workspace { if let Ok(Some(detached_environments_path)) = self.config().detached_environments().path() { Some(detached_environments_path.join(format!( "{}-{}", - self.name(), + self.display_name(), xxh3_64(self.root.to_string_lossy().as_bytes()) ))) } else { @@ -834,6 +856,57 @@ mod tests { } } + #[test] + fn test_workspace_name_when_specified() { + const WORKSPACE_STR: &str = r#" + [workspace] + name = "foo" + channels = [] + "#; + + let temp_dir = tempfile::tempdir().unwrap(); + let workspace = Workspace::from_str( + &temp_dir.path().join(consts::WORKSPACE_MANIFEST), + WORKSPACE_STR, + ) + .unwrap(); + assert_eq!(workspace.display_name(), "foo"); + } + + #[test] + fn test_workspace_name_when_unspecified() { + const WORKSPACE_STR: &str = r#" + [workspace] + channels = [] + "#; + + let temp_dir = tempfile::tempdir().unwrap(); + let workspace = Workspace::from_str( + &temp_dir + .path() + .join("foobar") + .join(consts::WORKSPACE_MANIFEST), + WORKSPACE_STR, + ) + .unwrap(); + assert_eq!(workspace.display_name(), "foobar"); + } + + #[test] + fn test_workspace_name_when_undefined() { + const WORKSPACE_STR: &str = r#" + [workspace] + channels = [] + "#; + + let workspace = Workspace::from_str( + &Path::new("/").join(consts::WORKSPACE_MANIFEST), + WORKSPACE_STR, + ) + .unwrap(); + assert_eq!(workspace.display_name(), "workspace"); + } + fn format_dependencies(deps: pixi_manifest::CondaDependencies) -> String { deps.iter_specs() .map(|(name, spec)| format!("{} = {}", name.as_source(), spec.to_toml_value())) diff --git a/tests/integration_rust/init_tests.rs b/tests/integration_rust/init_tests.rs index 89b8478a7f..388a73f983 100644 --- a/tests/integration_rust/init_tests.rs +++ b/tests/integration_rust/init_tests.rs @@ -16,9 +16,9 @@ async fn init_creates_project_manifest() { let workspace = pixi.workspace().unwrap(); // Default configuration should be present in the file - assert!(!workspace.name().is_empty()); + assert!(!workspace.display_name().is_empty()); assert_eq!( - workspace.name(), + workspace.display_name(), &pixi.workspace_path().file_stem().unwrap().to_string_lossy(), "project name should match the directory name" );