Skip to content

Commit

Permalink
Merge branch 'main' into perf/cache_environment_activation
Browse files Browse the repository at this point in the history
  • Loading branch information
ruben-arts authored Nov 19, 2024
2 parents e1d38c9 + a642b35 commit a312abb
Show file tree
Hide file tree
Showing 24 changed files with 635 additions and 86 deletions.
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ rstest = "0.19.0"
self-replace = "1.3.7"
serde = "1.0.198"
serde-untagged = "0.1.5"
serde-value = "0.7.0"
serde_ignored = "0.1.10"
serde_json = "1.0.116"
serde_with = "3.7.0"
Expand Down
7 changes: 5 additions & 2 deletions crates/pixi_config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,10 +738,13 @@ impl Config {
let mut config = Self::load_system();

for p in config_path_global() {
if !p.is_file() {
continue;
}
match Self::from_path(&p) {
Ok(c) => config = config.merge_config(c),
Err(e) => tracing::debug!(
"Failed to load global config: {} (error: {})",
Err(e) => tracing::warn!(
"Failed to load global config '{}' with error: {}",
p.display(),
e
),
Expand Down
3 changes: 2 additions & 1 deletion crates/pixi_manifest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pixi_spec = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde-untagged = { workspace = true }
serde-value = { workspace = true }
serde_with = { workspace = true }
spdx = { workspace = true }
strsim = { workspace = true }
Expand Down Expand Up @@ -47,5 +48,5 @@ fancy_display = { workspace = true }
glob = "0.3.1"
insta = { workspace = true, features = ["yaml"] }
rstest = { workspace = true }
serde_json = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] }
tempfile = { workspace = true }
2 changes: 2 additions & 0 deletions crates/pixi_manifest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod has_manifest_ref;
mod manifests;
mod metadata;
mod parsed_manifest;
mod preview;
pub mod pypi;
pub mod pyproject;
mod solve_group;
Expand Down Expand Up @@ -48,6 +49,7 @@ use thiserror::Error;
pub use features_ext::FeaturesExt;
pub use has_features_iter::HasFeaturesIter;
pub use has_manifest_ref::HasManifestRef;
pub use preview::{KnownPreviewFeature, Preview, PreviewFeature};

/// Errors that can occur when getting a feature.
#[derive(Debug, Clone, Error, Diagnostic)]
Expand Down
6 changes: 6 additions & 0 deletions crates/pixi_manifest/src/manifests/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{
consts,
error::{DependencyError, TomlError, UnknownFeature},
manifests::{ManifestSource, TomlManifest},
preview::Preview,
pypi::PyPiPackageName,
pyproject::PyProjectManifest,
to_options, DependencyOverwriteBehavior, Environment, EnvironmentName, Feature, FeatureName,
Expand Down Expand Up @@ -722,6 +723,11 @@ impl Manifest {
{
self.parsed.environments.find(name)
}

/// Returns the preview field of the project
pub fn preview(&self) -> Option<&Preview> {
self.parsed.project.preview.as_ref()
}
}

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions crates/pixi_manifest/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde_with::{serde_as, DisplayFromStr};
use url::Url;

use super::pypi::pypi_options::PypiOptions;
use crate::preview::Preview;
use crate::utils::PixiSpanned;

/// Describes the contents of the `[package]` section of the project manifest.
Expand Down Expand Up @@ -64,4 +65,7 @@ pub struct ProjectMetadata {

/// The pypi options supported in the project
pub pypi_options: Option<PypiOptions>,

/// Preview features
pub preview: Option<Preview>,
}
203 changes: 203 additions & 0 deletions crates/pixi_manifest/src/preview.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! This module contains the ability to parse the preview features of the project
//!
//! e.g.
//! ```toml
//! [project]
//! # .. other project metadata
//! preview = ["new-resolve"]
//! ```
//!
//! Features are split into Known and Unknown features. Basically you can use any string as a feature
//! but only the features defined in [`KnownFeature`] can be used.
//! We do this for backwards compatibility with the old features that may have been used in the past.
//! The [`KnownFeature`] enum contains all the known features. Extend this if you want to add support
//! for new features.
use serde::{Deserialize, Deserializer, Serialize};

#[derive(Debug, Serialize, Clone, PartialEq)]
#[serde(untagged)]
/// The preview features of the project
pub enum Preview {
/// All preview features are enabled
AllEnabled(bool), // For `preview = true`
/// Specific preview features are enabled
Features(Vec<PreviewFeature>), // For `preview = ["feature"]`
}

impl Preview {
/// Returns true if all preview features are enabled
pub fn all_enabled(&self) -> bool {
match self {
Preview::AllEnabled(enabled) => *enabled,
Preview::Features(_) => false,
}
}

/// Returns true if the given preview feature is enabled
pub fn is_enabled(&self, feature: KnownPreviewFeature) -> bool {
match self {
Preview::AllEnabled(_) => true,
Preview::Features(features) => features.iter().any(|f| *f == feature),
}
}

/// Return all unknown preview features
pub fn unknown_preview_features(&self) -> Vec<&str> {
match self {
Preview::AllEnabled(_) => vec![],
Preview::Features(features) => features
.iter()
.filter_map(|feature| match feature {
PreviewFeature::Unknown(feature) => Some(feature.as_str()),
_ => None,
})
.collect(),
}
}
}

impl<'de> Deserialize<'de> for Preview {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
serde_untagged::UntaggedEnumVisitor::new()
.bool(|bool| Ok(Preview::AllEnabled(bool)))
.seq(|seq| Ok(Preview::Features(seq.deserialize()?)))
.expecting("bool or list of features e.g `true` or `[\"new-resolve\"]`")
.deserialize(deserializer)
}
}

#[derive(Debug, Serialize, Clone, PartialEq)]
#[serde(untagged)]
/// A preview feature, can be either a known feature or an unknown feature
pub enum PreviewFeature {
/// This is a known preview feature
Known(KnownPreviewFeature),
/// Unknown preview feature
Unknown(String),
}

impl PartialEq<KnownPreviewFeature> for PreviewFeature {
fn eq(&self, other: &KnownPreviewFeature) -> bool {
match self {
PreviewFeature::Known(feature) => feature == other,
_ => false,
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
#[serde(rename_all = "kebab-case")]
/// Currently supported preview features are listed here
pub enum KnownPreviewFeature {
// Add known features here
}

impl<'de> Deserialize<'de> for PreviewFeature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = serde_value::Value::deserialize(deserializer)?;
let known = KnownPreviewFeature::deserialize(value.clone()).map(PreviewFeature::Known);
if let Ok(feature) = known {
Ok(feature)
} else {
let unknown = String::deserialize(value)
.map(PreviewFeature::Unknown)
.map_err(serde::de::Error::custom)?;
Ok(unknown)
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use insta::assert_snapshot;
use toml_edit::{de::from_str, ser::to_string};

/// Fake table to test the `Preview` enum
#[derive(Debug, Serialize, Deserialize)]
struct TopLevel {
preview: Preview,
}

#[test]
fn test_preview_all_enabled() {
let input = "preview = true";
let top: TopLevel = from_str(input).expect("should parse as `AllEnabled`");
assert_eq!(top.preview, Preview::AllEnabled(true));

let output = to_string(&top).expect("should serialize back to TOML");
assert_eq!(output.trim(), input);
}

#[test]
fn test_preview_with_unknown_feature() {
let input = r#"preview = ["build"]"#;
let top: TopLevel = from_str(input).expect("should parse as `Features` with known feature");
assert_eq!(
top.preview,
Preview::Features(vec![PreviewFeature::Unknown("build".to_string())])
);

let output = to_string(&top).expect("should serialize back to TOML");
assert_eq!(output.trim(), input);
}

#[test]
fn test_insta_error_invalid_bool() {
let input = r#"preview = "not-a-bool""#;
let result: Result<Preview, _> = from_str(input);

assert!(result.is_err());
assert_snapshot!(
format!("{:?}", result.unwrap_err()),
@r###"Error { inner: TomlError { message: "invalid type: map, expected bool or list of features e.g `true` or `[\"new-resolve\"]`", raw: Some("preview = \"not-a-bool\""), keys: [], span: Some(0..22) } }"###
);
}

#[test]
fn test_insta_error_invalid_list_item() {
let input = r#"preview = ["build", 123]"#;
let result: Result<TopLevel, _> = from_str(input);

assert!(result.is_err());
assert_snapshot!(
format!("{:?}", result.unwrap_err()),
@r###"Error { inner: TomlError { message: "Invalid type integer `123`. Expected a string\n", raw: Some("preview = [\"build\", 123]"), keys: ["preview"], span: Some(10..24) } }"###
);
}

#[test]
fn test_insta_error_invalid_top_level_type() {
let input = r#"preview = 123"#;
let result: Result<TopLevel, _> = from_str(input);

assert!(result.is_err());
assert_snapshot!(
format!("{:?}", result.unwrap_err()),
@r###"Error { inner: TomlError { message: "invalid type: integer `123`, expected bool or list of features e.g `true` or `[\"new-resolve\"]`", raw: Some("preview = 123"), keys: ["preview"], span: Some(10..13) } }"###
);
}

#[test]
fn test_feature_is_unknown() {
let input = r#"preview = ["new_parsing"]"#;
let top: TopLevel = from_str(input).unwrap();
match top.preview {
Preview::AllEnabled(_) => unreachable!("this arm should not be used"),
Preview::Features(vec) => {
assert_matches::assert_matches!(
&vec[0],
PreviewFeature::Unknown(s) => {
s == &"new_parsing".to_string()
}
);
}
}
}
}
16 changes: 16 additions & 0 deletions crates/pixi_manifest/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ impl ParsedManifest {
}
}

// Warn on any unknown preview features
if let Some(preview) = self.project.preview.as_ref() {
let preview = preview.unknown_preview_features();
if !preview.is_empty() {
let are = if preview.len() > 1 { "are" } else { "is" };
let s = if preview.len() > 1 { "s" } else { "" };
let preview_array = if preview.len() == 1 {
format!("{:?}", preview)
} else {
format!("[{:?}]", preview.iter().format(", "))
};
tracing::warn!(
"The preview feature{s}: {preview_array} {are} defined in the manifest but un-used pixi");
}
}

Ok(())
}

Expand Down
16 changes: 16 additions & 0 deletions docs/reference/project_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,22 @@ When an environment comprises several features (including the default feature):
- The `channels` of the environment is the union of the `channels` of all its features. Channel priorities can be specified in each feature, to ensure channels are considered in the right order in the environment.
- The `platforms` of the environment is the intersection of the `platforms` of all its features. Be aware that the platforms supported by a feature (including the default feature) will be considered as the `platforms` defined at project level (unless overridden in the feature). This means that it is usually a good idea to set the project `platforms` to all platforms it can support across its environments.

## Preview features
Pixi sometimes introduces new features that are not yet stable, but that we would like for users to test out. These features are called preview features. Preview features are disabled by default and can be enabled by setting the `preview` field in the project manifest. The preview field is an array of strings that specify the preview features to enable, or the boolean value `true` to enable all preview features.

An example of a preview feature in the project manifest:

```toml title="Example preview features in the project manifest"
[project]
name = "foo"
channels = []
platforms = []
preview = ["new-resolve"]
```

Preview features in the documentation will be marked as such on the relevant pages.


## Global configuration

The global configuration options are documented in the [global configuration](../reference/pixi_configuration.md) section.
Loading

0 comments on commit a312abb

Please sign in to comment.