Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion crates/pixi_cli/src/global/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use indexmap::IndexMap;
use clap::Parser;
use fancy_display::FancyDisplay;
use itertools::Itertools;
use miette::Report;
use miette::{IntoDiagnostic, Report};
use rattler_conda_types::{MatchSpec, NamedChannelOrUrl, Platform};

use crate::global::{global_specs::GlobalSpecs, revert_environment_after_error};
Expand All @@ -14,6 +14,7 @@ use pixi_global::{
self, EnvChanges, EnvState, EnvironmentName, Mapping, Project, StateChange, StateChanges,
common::{NotChangedReason, contains_menuinst_document},
list::list_all_global_environments,
overrides_from_env,
project::{ExposedType, GlobalSpec},
};

Expand Down Expand Up @@ -203,6 +204,14 @@ async fn setup_environment(
project.manifest.set_platform(env_name, platform)?;
}

// Record any `CONDA_OVERRIDE_*` environment variables as overrides in the
// manifest, so that operations that re-solve the environment later (like
// `pixi global update`) use the same values.
let env_overrides = overrides_from_env().into_diagnostic()?;
if !env_overrides.is_empty() {
project.manifest.set_overrides(env_name, &env_overrides)?;
}

let converted_with_inclusions = args
.with
.iter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ impl CommandDispatcherBuilder {
let tool_platform = self.tool_platform.unwrap_or_else(|| {
let platform = Platform::current();
let virtual_packages =
VirtualPackages::detect(&VirtualPackageOverrides::default()).unwrap_or_default();
VirtualPackages::detect(&VirtualPackageOverrides::from_env()).unwrap_or_default();
(
platform,
virtual_packages.into_generic_virtual_packages().collect(),
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_global/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub mod trampoline;

pub use common::{BinDir, EnvChanges, EnvDir, EnvRoot, EnvState, StateChange, StateChanges};
use pixi_utils::executable_from_path;
pub use project::{EnvironmentName, ExposedName, Mapping, Project};
pub use project::{EnvironmentName, ExposedName, Mapping, Project, overrides_from_env};

use pixi_utils::prefix::{Executable, Prefix};
use rattler_conda_types::PrefixRecord;
Expand Down
112 changes: 111 additions & 1 deletion crates/pixi_global/src/project/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use indexmap::IndexSet;
use miette::IntoDiagnostic;
use pixi_config::Config;
use pixi_consts::consts;
use pixi_manifest::{PrioritizedChannel, toml::TomlDocument};
use pixi_manifest::{
LibCSystemRequirement, PrioritizedChannel, SystemRequirements, toml::TomlDocument,
};
use pixi_toml::TomlIndexMap;
use pixi_utils::{executable_from_path, strip_executable_extension};
use rattler_conda_types::{NamedChannelOrUrl, PackageName, Platform};
Expand Down Expand Up @@ -242,6 +244,73 @@ impl Manifest {
Ok(())
}

/// Records virtual package overrides for a specific environment in the
/// manifest. Only the overrides that are specified are written, overrides
/// that were recorded earlier are kept.
pub fn set_overrides(
&mut self,
env_name: &EnvironmentName,
overrides: &SystemRequirements,
) -> miette::Result<()> {
// Ensure the environment exists
let env = self.parsed.envs.get_mut(env_name).ok_or_else(|| {
miette::miette!("Environment {} doesn't exist", env_name.fancy_display())
})?;

if overrides.is_empty() {
return Ok(());
}

// Update self.parsed, overrides that are specified overwrite
// previously recorded values.
if let Some(cuda) = &overrides.cuda {
env.overrides.cuda = Some(cuda.clone());
}
if let Some(linux) = &overrides.linux {
env.overrides.linux = Some(linux.clone());
}
if let Some(macos) = &overrides.macos {
env.overrides.macos = Some(macos.clone());
}
if let Some(libc) = &overrides.libc {
env.overrides.libc = Some(libc.clone());
}

// Update self.document
let table =
self.document
.get_or_insert_nested_table(&["envs", env_name.as_str(), "overrides"])?;
if let Some(cuda) = &overrides.cuda {
table.insert("cuda", toml_edit::value(cuda.to_string()));
}
if let Some(linux) = &overrides.linux {
table.insert("linux", toml_edit::value(linux.to_string()));
}
if let Some(macos) = &overrides.macos {
table.insert("macos", toml_edit::value(macos.to_string()));
}
if let Some(libc) = &overrides.libc {
let value = match libc {
LibCSystemRequirement::GlibC(version) => toml_edit::value(version.to_string()),
LibCSystemRequirement::OtherFamily(family_and_version) => {
let mut libc_table = toml_edit::InlineTable::new();
if let Some(family) = &family_and_version.family {
libc_table.insert("family", family.as_str().into());
}
libc_table.insert("version", family_and_version.version.to_string().into());
Item::Value(toml_edit::Value::InlineTable(libc_table))
}
};
table.insert("libc", value);
}

tracing::debug!(
"Set virtual package overrides for environment {} in toml document",
env_name.fancy_display()
);
Ok(())
}

#[allow(dead_code)]
/// Adds a channel to the manifest
pub fn add_channel(
Expand Down Expand Up @@ -1092,6 +1161,47 @@ mod tests {
assert_eq!(actual_platform, platform);
}

#[test]
fn test_set_overrides() {
let mut manifest = Manifest::default();
let env_name = EnvironmentName::from_str("test-env").unwrap();
manifest.add_environment(&env_name, None).unwrap();

let overrides = SystemRequirements {
cuda: Some("12.0".parse().unwrap()),
macos: Some("13.0".parse().unwrap()),
..Default::default()
};
manifest.set_overrides(&env_name, &overrides).unwrap();

// Recording again only overwrites the specified fields.
let overrides = SystemRequirements {
cuda: Some("11.0".parse().unwrap()),
libc: Some(LibCSystemRequirement::GlibC("2.17".parse().unwrap())),
..Default::default()
};
manifest.set_overrides(&env_name, &overrides).unwrap();

// Check parsed
let parsed = &manifest.parsed.envs.get(&env_name).unwrap().overrides;
assert_eq!(parsed.cuda, Some("11.0".parse().unwrap()));
assert_eq!(parsed.macos, Some("13.0".parse().unwrap()));
assert_eq!(
parsed.libc,
Some(LibCSystemRequirement::GlibC("2.17".parse().unwrap()))
);

// Check that the document round-trips to the same requirements.
let reparsed =
Manifest::from_str(Path::new("global.toml"), manifest.document.to_string()).unwrap();
assert_eq!(
&reparsed.parsed.envs.get(&env_name).unwrap().overrides,
parsed
);

assert_snapshot!(manifest.document.to_string());
}

#[test]
fn test_add_channel() {
let mut manifest = Manifest::default();
Expand Down
156 changes: 141 additions & 15 deletions crates/pixi_global/src/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ use pixi_command_dispatcher::{
use pixi_config::{Config, RunPostLinkScripts, default_channel_config, pixi_home};
use pixi_consts::consts::{self};
use pixi_core::repodata::Repodata;
use pixi_manifest::PrioritizedChannel;
use pixi_manifest::{
GLIBC_FAMILY, LibCFamilyAndVersion, LibCSystemRequirement, PrioritizedChannel,
SystemRequirements,
};
use pixi_path::AbsPathBuf;
use pixi_reporters::TopLevelProgress;
use pixi_spec::{BinarySpec, PathBinarySpec};
Expand All @@ -48,7 +51,8 @@ use rattler_networking::LazyClient;
use rattler_repodata_gateway::Gateway;
// Removed unused rattler_solve imports
use rattler_virtual_packages::{
DetectVirtualPackageError, VirtualPackage, VirtualPackageOverrides,
Archspec, Cuda, DetectVirtualPackageError, EnvOverride, LibC, Linux, Osx, VirtualPackage,
VirtualPackageOverrides, Windows,
};
use tokio::sync::Semaphore;
use toml_edit::DocumentMut;
Expand Down Expand Up @@ -561,25 +565,57 @@ impl Project {
self.config.global_channel_config()
}

/// Check if the platform matches the current platform (OS)
/// We only need to detect virtual packages if the platform is the current
/// one. Otherwise, we use an empty list
/// Returns the virtual packages to use when solving an environment for
/// the given platform.
///
/// The base set is detected from the current machine, but only if the
/// platform matches the current platform (OS). Otherwise, nothing can be
/// detected. On top of that, the overrides recorded in the manifest take
/// precedence over the detected values, and the `CONDA_OVERRIDE_*`
/// environment variables take precedence over both, so users can always
/// override the recorded values ephemerally.
pub(crate) fn virtual_packages_for(
platform: &Platform,
overrides: &SystemRequirements,
) -> Result<Vec<GenericVirtualPackage>, DetectVirtualPackageError> {
if platform
let is_current_os = platform
.only_platform()
.map(|p| p == Platform::current().only_platform().unwrap_or(""))
.unwrap_or(false)
{
Ok(VirtualPackage::detect(&VirtualPackageOverrides::default())?
.iter()
.cloned()
.map(GenericVirtualPackage::from)
.collect())
.unwrap_or(false);

let mut virtual_packages = if is_current_os {
VirtualPackage::detect(&VirtualPackageOverrides::default())?
} else {
Ok(vec![])
Vec::new()
};

for requirement in overrides.virtual_packages() {
upsert_virtual_package(&mut virtual_packages, requirement);
}

apply_env_override::<Windows>(&mut virtual_packages, |vp| {
matches!(vp, VirtualPackage::Win(_))
})?;
apply_env_override::<Linux>(&mut virtual_packages, |vp| {
matches!(vp, VirtualPackage::Linux(_))
})?;
apply_env_override::<Osx>(&mut virtual_packages, |vp| {
matches!(vp, VirtualPackage::Osx(_))
})?;
apply_env_override::<LibC>(&mut virtual_packages, |vp| {
matches!(vp, VirtualPackage::LibC(_))
})?;
apply_env_override::<Cuda>(&mut virtual_packages, |vp| {
matches!(vp, VirtualPackage::Cuda(_))
})?;
apply_env_override::<Archspec>(&mut virtual_packages, |vp| {
matches!(vp, VirtualPackage::Archspec(_))
})?;

Ok(virtual_packages
.into_iter()
.map(GenericVirtualPackage::from)
.collect())
}

pub async fn install_environment(
Expand Down Expand Up @@ -628,7 +664,7 @@ impl Project {

let build_environment = BuildEnvironment::simple(
platform,
Self::virtual_packages_for(&platform).into_diagnostic()?,
Self::virtual_packages_for(&platform, &environment.overrides).into_diagnostic()?,
);
// Create solve spec (compute-engine keys path).
let solve_spec = SolvePixiEnvironmentSpec {
Expand Down Expand Up @@ -1571,6 +1607,64 @@ impl Project {
}
}

/// Inserts the given virtual package into the list, replacing an existing
/// virtual package of the same kind.
fn upsert_virtual_package(virtual_packages: &mut Vec<VirtualPackage>, package: VirtualPackage) {
if let Some(existing) = virtual_packages
.iter_mut()
.find(|vp| std::mem::discriminant(&**vp) == std::mem::discriminant(&package))
{
*existing = package;
} else {
virtual_packages.push(package);
}
}

/// Applies the `CONDA_OVERRIDE_*` environment variable of the given virtual
/// package kind on top of the given virtual packages. Setting the variable to
/// an empty string removes the virtual package entirely.
fn apply_env_override<T: EnvOverride + Into<VirtualPackage>>(
virtual_packages: &mut Vec<VirtualPackage>,
is_kind: fn(&VirtualPackage) -> bool,
) -> Result<(), DetectVirtualPackageError> {
match std::env::var(T::DEFAULT_ENV_NAME) {
Ok(value) => {
virtual_packages.retain(|vp| !is_kind(vp));
if let Some(package) = T::parse_version_opt(&value)? {
virtual_packages.push(package.into());
}
Ok(())
}
Err(std::env::VarError::NotPresent) => Ok(()),
Err(err) => Err(err.into()),
}
}

/// Constructs [`SystemRequirements`] from the `CONDA_OVERRIDE_*` environment
/// variables that are currently set. This is used to record the overrides in
/// the manifest so subsequent solves use the same values.
pub fn overrides_from_env() -> Result<SystemRequirements, DetectVirtualPackageError> {
Ok(SystemRequirements {
cuda: Cuda::from_env_var_name_or(Cuda::DEFAULT_ENV_NAME, || Ok(None))?
.map(|cuda| cuda.version),
linux: Linux::from_env_var_name_or(Linux::DEFAULT_ENV_NAME, || Ok(None))?
.map(|linux| linux.version),
macos: Osx::from_env_var_name_or(Osx::DEFAULT_ENV_NAME, || Ok(None))?
.map(|osx| osx.version),
libc: LibC::from_env_var_name_or(LibC::DEFAULT_ENV_NAME, || Ok(None))?.map(|libc| {
if libc.family == GLIBC_FAMILY {
LibCSystemRequirement::GlibC(libc.version)
} else {
LibCSystemRequirement::OtherFamily(LibCFamilyAndVersion {
family: Some(libc.family),
version: libc.version,
})
}
}),
archspec: None,
})
}

impl Repodata for Project {
/// Returns the [`Gateway`] used by this project.
fn repodata_gateway(&self) -> miette::Result<&Gateway> {
Expand Down Expand Up @@ -1611,6 +1705,38 @@ mod tests {
dummy = "dummy"
"#;

#[test]
fn test_virtual_packages_for_overrides() {
let overrides = SystemRequirements {
cuda: Some("12.0".parse().unwrap()),
..Default::default()
};

// A recorded override replaces the detected value (or adds it when
// nothing was detected).
let virtual_packages =
Project::virtual_packages_for(&Platform::current(), &overrides).unwrap();
let cuda = virtual_packages
.iter()
.find(|vp| vp.name.as_normalized() == "__cuda")
.expect("cuda virtual package should be present");
assert_eq!(cuda.version.to_string(), "12.0");

// For a platform with a different OS nothing is detected, but the
// recorded overrides are still used.
let other_platform = if Platform::current().is_windows() {
Platform::Linux64
} else {
Platform::Win64
};
let virtual_packages = Project::virtual_packages_for(&other_platform, &overrides).unwrap();
let names = virtual_packages
.iter()
.map(|vp| vp.name.as_normalized())
.collect_vec();
assert_eq!(names, vec!["__cuda"]);
}

#[tokio::test]
async fn test_project_from_str() {
// Use a deterministic path with a parent. `FilePath().fake()` from
Expand Down
Loading
Loading