diff --git a/Cargo.lock b/Cargo.lock index 06114b765..ffb7a2ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,6 +278,16 @@ dependencies = [ "xz2", ] +[[package]] +name = "ariadne" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72fe02fc62033df9ba41cba57ee19acf5e742511a140c7dbc3a873e19a19a1bd" +dependencies = [ + "unicode-width", + "yansi", +] + [[package]] name = "asn1-rs" version = "0.5.2" @@ -2124,6 +2134,7 @@ dependencies = [ "console", "lazy_static", "linked-hash-map", + "serde", "similar", "yaml-rust", ] @@ -2900,6 +2911,7 @@ name = "pixi" version = "0.0.3" dependencies = [ "anyhow", + "ariadne", "clap", "clap-verbosity-flag", "clap_complete", @@ -2907,6 +2919,7 @@ dependencies = [ "dirs 5.0.1", "dunce", "futures 0.3.28", + "indexmap", "indicatif", "insta", "is_executable", @@ -2922,6 +2935,7 @@ dependencies = [ "rattler_virtual_packages", "reqwest", "serde", + "serde_spanned", "serde_with", "shlex", "tempfile", @@ -5085,6 +5099,12 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "yasna" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index c7d7c253a..988e3a628 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,40 +16,43 @@ rustls-tls = ["reqwest/rustls-tls", "rattler_repodata_gateway/rustls-tls", "ratt [dependencies] anyhow = "1.0.70" +ariadne = "0.3.0" clap = { version = "4.2.4", default-features = false, features = ["derive", "usage", "wrap_help", "std", "color", "error-context"] } +clap-verbosity-flag = "2.0.1" clap_complete = "4.2.1" console = { version = "0.15.5", features = ["windows-console-colors"] } dirs = "5.0.1" dunce = "1.0.4" futures = "0.3.28" +indexmap = { version = "1.9.3", features = ["serde"] } indicatif = "0.17.3" -insta = "1.29.0" +insta = { version = "1.29.0", features=["yaml"] } is_executable = "1.0.1" itertools = "0.10.5" minijinja = { version = "0.32.0" } once_cell = "1.17.1" rattler = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main" } rattler_conda_types = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main" } +rattler_networking = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main" } rattler_repodata_gateway = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main", features = ["sparse"] } rattler_shell = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main", features = ["sysinfo"] } rattler_solve = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main" } rattler_virtual_packages = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main" } -rattler_networking = { version = "0.3.0", default-features = false, git = "https://github.com/mamba-org/rattler", branch = "main" } #rattler = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler" } #rattler_conda_types = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_conda_types" } #rattler_repodata_gateway = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_repodata_gateway", features = ["sparse"] } -#rattler_shell = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_shell" } +#rattler_shell = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_shell", features = ["sysinfo"] } #rattler_solve = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_solve" } #rattler_virtual_packages = { version = "0.2.0", default-features = false, path="../rattler/crates/rattler_virtual_packages" } reqwest = { version = "0.11.16", default-features = false } serde = "1.0.163" -serde_with = "3.0.0" +serde_spanned = "0.6.2" +serde_with = { version = "3.0.0", features = ["indexmap"] } shlex = "1.1.0" tempfile = "3.5.0" tokio = { version = "1.27.0", features = ["macros", "rt-multi-thread"] } -toml_edit = { version = "0.19.8", features = ["serde"] } +toml_edit = { version = "0.19.10", features = ["serde"] } tracing = "0.1.37" -clap-verbosity-flag = "2.0.1" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } [profile.release-lto] diff --git a/docs/getting_started.md b/docs/getting_started.md index 5d2e3f3f3..31249e109 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -121,6 +121,55 @@ For instance, to check the platform compatibility of the python package, you can Incorporating the appropriate platform configurations in your project ensures its broad usability and accessibility across various environments. +## The `dependencies` part + +As pixi is a package manager we obviously provide a way to specify dependencies. +Dependencies are specified using a "version" or "version range" which for Conda is a "MatchSpec" + +This is a conda specific way to specify dependencies, to avoid failing to write a good explanation I'll link you to some introductory reads: +- [Conda build documentation](https://docs.conda.io/projects/conda-build/en/latest/resources/package-spec.html#id6) +- [Excellent stackoverflow answer](https://stackoverflow.com/a/57734390/13258625) +- [Conda's python implementation](https://github.com/conda/conda/blob/main/conda/models/match_spec.py) +- [Rattler's rust implementation (ours)](https://github.com/mamba-org/rattler/blob/main/crates/rattler_conda_types/src/match_spec/mod.rs) + +Here are some examples: +```toml +[dependencies] +python = "3.*" +python = "3.7.*" +python = "3.7.10.*" +python = "3.8.2 h8356626_7_cpython" +python = ">3.8.2" +python = "<3.8.2" +python = ">=3.8.2" +python = "<=3.8.2" +python = ">=3.8,<3.9" +python = "3.11" +python = "3.10.9|3.11.*" +``` + +**Gotcha**: `python = "3"` resolves to `3.0.0` which is not a possible version. +To get the latest version of something always append with `.*` so that would be `python = "3.*"` + +### Dependencies per platform + +You can also specify a dependency specific to a certain platform: + +```toml +[dependencies] +python = "3.11" + +[target.osx-arm64.dependencies] +python = "3.10" +``` + +In the case above, we specify a specific dependency to OSX. +This overwrites the generic python dependency specified in the dependencies block. + +### Resolution order +As a rule the target specific dependencies take precedence over generic ones +in the order that they are specified, so should multiple targets match the last specification is used. + ## The `commands` part In addition to managing dependencies, `pixi` aims to provide a user-friendly interface that simplifies the execution of repetitive, complex commands. The commands section in your `pixi` configuration serves this purpose. @@ -147,31 +196,3 @@ The `depends_on` will run the specified command in there to be run before the co So in the example `build` will be run before `test`. `depends_on` can be a string or a list of strings e.g.: `depends_on="build"` or `depends_on=["build", "anything"]` -## The `dependencies` part -As pixi is a package manager we obviously provide a way to specify dependencies. -Dependencies are specified using a "version" or "version range" which for Conda is a "MatchSpec" - -This is a conda specific way to specify dependencies, to avoid failing to write a good explanation I'll link you to some excellent reads: -- [Conda build documentation](https://docs.conda.io/projects/conda-build/en/latest/resources/package-spec.html#id6) -- [Excelent stackoverflow answer](https://stackoverflow.com/a/57734390/13258625) -- [Conda's python implementation](https://github.com/conda/conda/blob/main/conda/models/match_spec.py) -- [Rattler's rust implementation(ours)](https://github.com/mamba-org/rattler/blob/main/crates/rattler_conda_types/src/match_spec/mod.rs) - -Here are some examples: -```toml -[dependencies] -python = "3.*" -python = "3.7.*" -python = "3.7.10.*" -python = "3.8.2 h8356626_7_cpython" -python = ">3.8.2" -python = "<3.8.2" -python = ">=3.8.2" -python = "<=3.8.2" -python = ">=3.8,<3.9" -python = "3.11" -python = "3.10.9|3.11.*" -``` - -**Gotcha**: `python = "3"` resolves to `3.0.0` which is not a possible version. -To get the latest version of something always append with `.*` so that would be `python = "3.*"` diff --git a/src/cli/add.rs b/src/cli/add.rs index 863d08ed0..9a7d33516 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -51,7 +51,6 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { .collect::>>()?; // Get the current specs - let current_specs = project.dependencies()?; // Fetch the repodata for the project let sparse_repo_data = project.fetch_sparse_repodata().await?; @@ -59,6 +58,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { // Determine the best version per platform let mut best_versions = HashMap::new(); for platform in project.platforms() { + let current_specs = project.dependencies(*platform)?; // Solve the environment with the new specs added let solved_versions = match determine_best_version( &new_specs, diff --git a/src/environment.rs b/src/environment.rs index 1a7ac02cb..812235da1 100644 --- a/src/environment.rs +++ b/src/environment.rs @@ -1,3 +1,4 @@ +use crate::report_error::ReportError; use crate::{ consts, prefix::Prefix, @@ -8,6 +9,7 @@ use crate::{ Project, }; use anyhow::Context; +use ariadne::{Label, Report, ReportKind, Source}; use futures::future::ready; use futures::{stream, FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use indicatif::ProgressBar; @@ -41,7 +43,23 @@ pub async fn get_up_to_date_prefix(project: &Project) -> anyhow::Result // Make sure the project supports the current platform let platform = Platform::current(); if !project.platforms().contains(&platform) { - anyhow::bail!("the project is not configured for your current platform. Add '{}' to the 'platforms' key in project's {} to include it", platform, consts::PROJECT_MANIFEST) + let span = project.manifest.project.platforms.span(); + let report = Report::build(ReportKind::Error, consts::PROJECT_MANIFEST, span.start) + .with_message("the project is not configured for your current platform") + .with_label( + Label::new((consts::PROJECT_MANIFEST, span)) + .with_message(format!("add '{platform}' here")), + ) + .with_help(format!( + "The project needs to be configured to support your platform ({platform})." + )) + .finish(); + + return Err(ReportError { + source: (consts::PROJECT_MANIFEST, Source::from(&project.source)), + report, + } + .into()); } // Make sure the system requirements are met @@ -126,11 +144,14 @@ pub fn lock_file_up_to_date(project: &Project, lock_file: &CondaLock) -> anyhow: return Ok(false); } - // Check if all dependencies exist in the lock-file. - let dependencies = project.dependencies()?.into_iter().collect::>(); - // For each platform, for platform in platforms.iter().cloned() { + // Check if all dependencies exist in the lock-file. + let dependencies = project + .dependencies(platform)? + .into_iter() + .collect::>(); + // Construct a queue of dependencies that we wanna find in the lock file let mut queue = dependencies.clone(); @@ -258,10 +279,6 @@ pub async fn update_lock_file( repodata: Option>, ) -> anyhow::Result { let platforms = project.platforms(); - let dependencies = project.dependencies()?; - - // Extract the package names from the dependencies - let package_names = dependencies.keys().collect_vec(); // Get the repodata for the project let sparse_repo_data = if let Some(sparse_repo_data) = repodata { @@ -276,14 +293,20 @@ pub async fn update_lock_file( .iter() .map(|channel| conda_lock::Channel::from(channel.base_url().to_string())); - let match_specs = dependencies - .iter() - .map(|(name, constraint)| MatchSpec::from_nameless(constraint.clone(), Some(name.clone()))) - .collect_vec(); - - let mut builder = - LockFileBuilder::new(channels, platforms.iter().cloned(), match_specs.clone()); + // Empty match-specs because these differ per platform + let mut builder = LockFileBuilder::new(channels, platforms.iter().cloned(), vec![]); for platform in platforms.iter().cloned() { + let dependencies = project.dependencies(platform)?; + let match_specs = dependencies + .iter() + .map(|(name, constraint)| { + MatchSpec::from_nameless(constraint.clone(), Some(name.clone())) + }) + .collect_vec(); + + // Extract the package names from the dependencies + let package_names = dependencies.keys().collect_vec(); + // Get the repodata for the current platform and for NoArch let platform_sparse_repo_data = sparse_repo_data.iter().filter(|sparse| { sparse.subdir() == platform.as_str() || sparse.subdir() == Platform::NoArch.as_str() diff --git a/src/main.rs b/src/main.rs index 05eeb7bb7..7a76b1b80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod prefix; mod progress; mod project; mod repodata; +mod report_error; mod util; mod virtual_packages; @@ -17,7 +18,12 @@ pub use project::Project; #[tokio::main] pub async fn main() { if let Err(err) = cli::execute().await { - eprintln!("{}: {:?}", style("error").bold().red(), err); + match err.downcast::() { + Ok(report) => report.eprint(), + Err(err) => { + eprintln!("{}: {:?}", style("error").bold().red(), err); + } + } std::process::exit(1); } } diff --git a/src/project/manifest.rs b/src/project/manifest.rs index e955f683b..c211f42f6 100644 --- a/src/project/manifest.rs +++ b/src/project/manifest.rs @@ -1,11 +1,20 @@ use crate::command::Command; +use crate::consts::PROJECT_MANIFEST; +use crate::report_error::ReportError; use ::serde::Deserialize; -use rattler_conda_types::{Channel, Platform, Version}; +use ariadne::{ColorGenerator, Fmt, Label, Report, ReportKind, Source}; +use indexmap::IndexMap; +use rattler_conda_types::{Channel, NamelessMatchSpec, Platform, Version}; use rattler_virtual_packages::{Archspec, Cuda, LibC, Linux, Osx, VirtualPackage}; -use serde_with::{serde_as, DisplayFromStr}; +use serde::Deserializer; +use serde_spanned::Spanned; +use serde_with::de::DeserializeAsWrap; +use serde_with::{serde_as, DeserializeAs, DisplayFromStr, PickFirst}; use std::collections::HashMap; +use std::ops::Range; /// Describes the contents of a project manifest. +#[serde_as] #[derive(Debug, Clone, Deserialize)] pub struct ProjectManifest { /// Information about the project @@ -18,6 +27,87 @@ pub struct ProjectManifest { /// Additional system requirements #[serde(default, rename = "system-requirements")] pub system_requirements: SystemRequirements, + + /// The dependencies of the project. + /// + /// We use an [`IndexMap`] to preserve the order in which the items where defined in the + /// manifest. + #[serde(default)] + #[serde_as(as = "IndexMap<_, DisplayFromStr>")] + pub dependencies: IndexMap, + + /// Target specific configuration. + /// + /// We use an [`IndexMap`] to preserve the order in which the items where defined in the + /// manifest. + #[serde(default)] + pub target: IndexMap, TargetMetadata>, +} + +impl ProjectManifest { + /// Validate the + pub fn validate(&self, contents: &str) -> anyhow::Result<()> { + // Check if the targets are defined for existing platforms + for target_sel in self.target.keys() { + match target_sel.as_ref() { + TargetSelector::Platform(p) => { + if !self.project.platforms.as_ref().contains(p) { + return Err(create_unsupported_platform_report( + contents, + target_sel.span(), + p, + ) + .into()); + } + } + } + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum TargetSelector { + // Platform specific configuration + Platform(Platform), + // TODO: Add minijinja coolness here. +} + +struct PlatformTargetSelector; + +impl<'de> DeserializeAs<'de, TargetSelector> for PlatformTargetSelector { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(TargetSelector::Platform(Platform::deserialize( + deserializer, + )?)) + } +} + +impl<'de> Deserialize<'de> for TargetSelector { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok( + DeserializeAsWrap::>::deserialize( + deserializer, + )? + .into_inner(), + ) + } +} + +#[serde_as] +#[derive(Debug, Clone, Deserialize)] +pub struct TargetMetadata { + /// Target specific dependencies + #[serde(default)] + #[serde_as(as = "IndexMap<_, DisplayFromStr>")] + pub dependencies: IndexMap, } /// Describes the contents of the `[package]` section of the project manifest. @@ -45,7 +135,7 @@ pub struct ProjectMetadata { /// The platforms this project supports // TODO: This is actually slightly different from the rattler_conda_types::Platform because it // should not include noarch. - pub platforms: Vec, + pub platforms: Spanned>, } #[serde_as] @@ -142,3 +232,81 @@ impl From for LibC { } } } + +// Create an error report for usign a platform that is not supported by the project. +fn create_unsupported_platform_report( + source: &str, + span: Range, + p: &Platform, +) -> ReportError { + let mut color_generator = ColorGenerator::new(); + let platform = color_generator.next(); + + let report = Report::build(ReportKind::Error, PROJECT_MANIFEST, span.start) + .with_message("Targeting a platform that this project does not support") + .with_label( + Label::new((PROJECT_MANIFEST, span)) + .with_message(format!("'{}' is not a supported platform", p.fg(platform))) + .with_color(platform), + ) + .with_help(format!( + "Add '{}' to the `project.platforms` array of the {PROJECT_MANIFEST} manifest.", + p.fg(platform) + )) + .finish(); + + ReportError { + report, + source: (PROJECT_MANIFEST, Source::from(source)), + } +} + +#[cfg(test)] +mod test { + use super::ProjectManifest; + use insta::{assert_debug_snapshot, assert_display_snapshot}; + + const PROJECT_BOILERPLATE: &str = r#" + [project] + name = "foo" + version = "0.1.0" + channels = [] + platforms = [] + "#; + + #[test] + fn test_target_specific() { + let contents = format!( + r#" + {PROJECT_BOILERPLATE} + + [target.win-64.dependencies] + foo = "3.4.5" + + [target.osx-64.dependencies] + foo = "1.2.3" + "# + ); + assert_debug_snapshot!( + toml_edit::de::from_str::(&contents).expect("parsing should succeed!") + ); + } + + #[test] + fn test_invalid_target_specific() { + let examples = [r#"[target.foobar.dependencies] + invalid_platform = "henk""#]; + + assert_display_snapshot!(examples + .into_iter() + .map( + |example| toml_edit::de::from_str::(&format!( + "{PROJECT_BOILERPLATE}\n{example}" + )) + .unwrap_err() + .to_string() + ) + .collect::>() + .join("\n")) + } +} diff --git a/src/project/mod.rs b/src/project/mod.rs index 153d0174e..35988d308 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -2,24 +2,27 @@ mod manifest; mod serde; use crate::consts; -use crate::project::manifest::ProjectManifest; +use crate::consts::PROJECT_MANIFEST; +use crate::project::manifest::{ProjectManifest, TargetMetadata, TargetSelector}; +use crate::report_error::ReportError; use anyhow::Context; +use ariadne::{Label, Report, ReportKind, Source}; use rattler_conda_types::{Channel, MatchSpec, NamelessMatchSpec, Platform, Version}; use rattler_virtual_packages::VirtualPackage; use std::{ collections::HashMap, env, fs, path::{Path, PathBuf}, - str::FromStr, }; -use toml_edit::{Document, Item, Table}; +use toml_edit::{Document, Item, Table, TomlError}; /// A project represented by a pixi.toml file. #[derive(Debug)] pub struct Project { root: PathBuf, + pub(crate) source: String, doc: Document, - manifest: ProjectManifest, + pub(crate) manifest: ProjectManifest, } impl Project { @@ -39,7 +42,7 @@ impl Project { let root = filename.parent().unwrap_or(Path::new(".")); // Load the TOML document - Self::from_manifest_str(root, &fs::read_to_string(filename)?).with_context(|| { + Self::from_manifest_str(root, fs::read_to_string(filename)?).with_context(|| { format!( "failed to parse {} from {}", consts::PROJECT_MANIFEST, @@ -49,44 +52,78 @@ impl Project { } /// Loads a project manifest. - pub fn from_manifest_str(root: &Path, contents: &str) -> anyhow::Result { - let manifest = toml_edit::de::from_str(contents)?; - let doc = contents.parse::()?; + pub fn from_manifest_str(root: &Path, contents: impl Into) -> anyhow::Result { + let contents = contents.into(); + let (manifest, doc) = match toml_edit::de::from_str::(&contents) + .map_err(TomlError::from) + .and_then(|manifest| contents.parse::().map(|doc| (manifest, doc))) + { + Ok(result) => result, + Err(e) => { + if let Some(span) = e.span() { + return Err(ReportError { + source: (PROJECT_MANIFEST, Source::from(&contents)), + report: Report::build(ReportKind::Error, PROJECT_MANIFEST, span.start) + .with_message("failed to parse project manifest") + .with_label( + Label::new((PROJECT_MANIFEST, span)).with_message(e.message()), + ) + .finish(), + } + .into()); + } else { + return Err(e.into()); + } + } + }; + + // Validate the contents of the manifest + manifest.validate(&contents)?; Ok(Self { root: root.to_path_buf(), + source: contents, doc, manifest, }) } - pub fn dependencies(&self) -> anyhow::Result> { - let deps = self - .doc - .get("dependencies") - .ok_or_else(|| { - anyhow::anyhow!("No dependencies found in {}", consts::PROJECT_MANIFEST) - })? - .as_table_like() - .ok_or_else(|| { - anyhow::anyhow!("dependencies in {} are malformed", consts::PROJECT_MANIFEST) - })?; - - let mut result = HashMap::with_capacity(deps.len()); - for (name, value) in deps.iter() { - let match_spec = value - .as_str() - .map(|str| NamelessMatchSpec::from_str(str).map_err(Into::into)) - .unwrap_or_else(|| { - Err(anyhow::anyhow!( - "dependencies in {} are malformed", - consts::PROJECT_MANIFEST - )) - })?; - result.insert(name.to_owned(), match_spec); - } + /// Returns the dependencies of the project. + pub fn dependencies( + &self, + platform: Platform, + ) -> anyhow::Result> { + // Get the base dependencies (defined in the `[dependencies]` section) + let base_dependencies = self.manifest.dependencies.iter(); + + // Get the platform specific dependencies in the order they were defined. + let platform_specific = self + .target_specific_metadata(platform) + .flat_map(|target| target.dependencies.iter()); + + // Combine the specs. + // + // Note that if a dependency was specified twice the platform specific one "wins". + Ok(base_dependencies + .chain(platform_specific) + .map(|(name, spec)| (name.clone(), spec.clone())) + .collect()) + } - Ok(result) + /// Returns all the targets specific metadata that apply with the given context. + /// TODO: Add more context here? + /// TODO: Should we return the selector too to provide better diagnostics later? + pub fn target_specific_metadata( + &self, + platform: Platform, + ) -> impl Iterator + '_ { + self.manifest + .target + .iter() + .filter_map(move |(selector, manifest)| match selector.as_ref() { + TargetSelector::Platform(p) if p == &platform => Some(manifest), + _ => None, + }) } /// Returns the name of the project @@ -165,7 +202,7 @@ impl Project { /// Returns the platforms this project targets pub fn platforms(&self) -> &[Platform] { - &self.manifest.project.platforms + self.manifest.project.platforms.as_ref().as_slice() } /// Get the command with the specified name or `None` if no such command exists. @@ -196,6 +233,7 @@ mod tests { use crate::project::manifest::SystemRequirements; use rattler_conda_types::ChannelConfig; use rattler_virtual_packages::{Archspec, Cuda, LibC, Linux, Osx, VirtualPackage}; + use std::str::FromStr; const PROJECT_BOILERPLATE: &str = r#" [project] @@ -215,7 +253,7 @@ mod tests { platforms = ["linux-64", "win-64"] "#; - let project = Project::from_manifest_str(Path::new(""), &file_content).unwrap(); + let project = Project::from_manifest_str(Path::new(""), file_content.to_string()).unwrap(); assert_eq!(project.name(), "pixi"); assert_eq!(project.version(), &Version::from_str("0.0.2").unwrap()); diff --git a/src/project/snapshots/pixi__project__manifest__test__invalid_target_specific.snap b/src/project/snapshots/pixi__project__manifest__test__invalid_target_specific.snap new file mode 100644 index 000000000..3ba58f179 --- /dev/null +++ b/src/project/snapshots/pixi__project__manifest__test__invalid_target_specific.snap @@ -0,0 +1,11 @@ +--- +source: src/project/manifest.rs +assertion_line: 230 +expression: "examples.into_iter().map(|example|\n toml_edit::de::from_str::(&format!(\"{PROJECT_BOILERPLATE}\\n{example}\")).unwrap_err().to_string()).collect::>().join(\"\\n\")" +--- +TOML parse error at line 8, column 9 + | +8 | [target.foobar.dependencies] + | ^^^^^^ +'foobar' is not a known platform + diff --git a/src/project/snapshots/pixi__project__manifest__test__target_specific.snap b/src/project/snapshots/pixi__project__manifest__test__target_specific.snap new file mode 100644 index 000000000..5ce1f8627 --- /dev/null +++ b/src/project/snapshots/pixi__project__manifest__test__target_specific.snap @@ -0,0 +1,89 @@ +--- +source: src/project/manifest.rs +assertion_line: 290 +expression: "toml_edit::de::from_str::(&contents).expect(\"parsing should succeed!\")" +--- +ProjectManifest { + project: ProjectMetadata { + name: "foo", + version: Version { + norm: "0.1.0", + version: [[0], [0], [1], [0]], + local: [], + }, + description: None, + authors: [], + channels: [], + platforms: Spanned { + span: 117..119, + value: [], + }, + }, + commands: {}, + system_requirements: SystemRequirements { + windows: None, + unix: None, + macos: None, + linux: None, + cuda: None, + libc: None, + archspec: None, + }, + dependencies: {}, + target: { + Spanned { + span: 146..152, + value: Platform( + Win64, + ), + }: TargetMetadata { + dependencies: { + "foo": NamelessMatchSpec { + version: Some( + Operator( + Equals, + Version { + norm: "3.4.5", + version: [[0], [3], [4], [5]], + local: [], + }, + ), + ), + build: None, + build_number: None, + file_name: None, + channel: None, + subdir: None, + namespace: None, + }, + }, + }, + Spanned { + span: 206..212, + value: Platform( + Osx64, + ), + }: TargetMetadata { + dependencies: { + "foo": NamelessMatchSpec { + version: Some( + Operator( + Equals, + Version { + norm: "1.2.3", + version: [[0], [1], [2], [3]], + local: [], + }, + ), + ), + build: None, + build_number: None, + file_name: None, + channel: None, + subdir: None, + namespace: None, + }, + }, + }, + }, +} diff --git a/src/report_error.rs b/src/report_error.rs new file mode 100644 index 000000000..5d2aace2e --- /dev/null +++ b/src/report_error.rs @@ -0,0 +1,27 @@ +use ariadne::{Report, Source}; +use std::{ + fmt::{Debug, Display, Formatter}, + ops::Range, +}; + +/// An error that contains a [`ariadne::Report`]. This allows the application to display a very +/// nicely formatted diagnostic. +#[derive(Debug)] +pub struct ReportError { + pub report: Report<'static, (&'static str, Range)>, + pub source: (&'static str, Source), +} + +impl std::error::Error for ReportError {} + +impl Display for ReportError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.report.fmt(f) + } +} + +impl ReportError { + pub fn eprint(self) { + self.report.eprint(self.source).unwrap() + } +}