Skip to content

feat(conda): replace custom backend with rattler crates#8325

Merged
jdx merged 6 commits intomainfrom
feat/conda-rattler
Feb 24, 2026
Merged

feat(conda): replace custom backend with rattler crates#8325
jdx merged 6 commits intomainfrom
feat/conda-rattler

Conversation

@jdx
Copy link
Owner

@jdx jdx commented Feb 24, 2026

Summary

Replaces the experimental conda backend's ~1,625 lines of custom code with the rattler Rust crates — the same crates that power pixi. Motivated by #8318 from a conda maintainer who noted the backend was directly using the unsupported api.anaconda.org API.

What was replaced:

  • Custom version matching and comparison logic
  • Recursive dependency resolution via raw anaconda.org API calls
  • Manual .conda ZIP+tar.zst and .tar.bz2 extraction
  • Hardcoded SKIP_PACKAGES list for virtual packages
  • Custom patchelf/install_name_tool/codesign binary patching (conda_common.rs, conda_linux.rs, conda_macos.rs deleted)

With rattler:

  • rattler_repodata_gateway — fetches repodata from conda channels (proper CDN-cached repodata.json)
  • rattler_solve (resolvo) — SAT-based dependency solver, same as used by pixi/conda-libmamba
  • rattler_package_streaming — extracts .conda and .tar.bz2 archives
  • rattler_virtual_packages — detects system virtual packages (__glibc, __osx, etc.) properly
  • rattler::install::link_package — proper conda installation with text AND binary prefix replacement, file permissions, and macOS codesigning

Key fix: The old code explicitly skipped binary files during prefix replacement (checking for null bytes). link_package correctly handles binary prefix replacement by padding with null bytes to preserve byte length, which is how conda is designed to work.

Kept unchanged:

  • lockfile.rsCondaPackageInfo type and [conda-packages] TOML format
  • All registry entries and settings

Since EXPERIMENTAL = true, breaking changes are acceptable.

Test plan

  • cargo build — compiles cleanly
  • mise run test:unit — 481 tests pass
  • mise run lint — all checks pass
  • mise run test:e2e test_conda:
    • conda:ruff@0.8.0 — installs with 24 deps, runs correctly
    • conda:bat@0.24.0 — installs with 4 deps, runs correctly
    • conda:jq@1.7.1 — installs with deps (oniguruma), runs correctly
    • conda:postgresql@17.2 — installs with 29 deps, runs correctly
  • mise lock — lockfile generated correctly with per-platform deps

🤖 Generated with Claude Code


Note

Medium Risk
Large rewrite of conda install/solve behavior (networking, dependency resolution, and prefix handling) that could change which packages get installed or how they’re linked; mitigated somewhat by being confined to the experimental conda backend and adding checksum-verified, lockfile-driven installs.

Overview
Switches the experimental conda backend from bespoke anaconda.org API calls, ad-hoc version/dependency selection, and manual archive extraction/prefix patching to the rattler ecosystem: rattler_repodata_gateway for repodata retrieval/caching, rattler_solve (resolvo) for dependency solving with virtual package detection, and rattler::install::link_package + rattler_package_streaming for extraction and correct text/binary prefix replacement.

Lockfile behavior is preserved but now populated from solver RepoDataRecords (URLs + sha256) and can perform deterministic installs by downloading the locked URL set with checksum verification; Windows bin path discovery is broadened to include both Library/bin and bin. The previous platform-specific patching modules (conda_common.rs, conda_linux.rs, conda_macos.rs) are removed, and Cargo dependencies/lockfile are updated to include the new rattler* crates and transitive deps.

Written by Cursor Bugbot for commit 21d3602. This will update automatically on new commits. Configure here.

Replaces ~985 lines of custom code (version matching, recursive dep
resolution via anaconda.org API, manual archive extraction) with the
rattler Rust crates that power pixi.

- rattler_repodata_gateway: fetches repodata from conda channels
- rattler_solve (resolvo): proper SAT-based dependency solving
- rattler_package_streaming: extracts .conda/.tar.bz2 archives
- rattler_virtual_packages: detects system virtual packages (__glibc, etc.)
- rattler_conda_types: shared types (Channel, MatchSpec, Platform, etc.)

Keeps platform-specific binary patching (patchelf/install_name_tool)
and text prefix fixing unchanged, as rattler's link_package assumes a
full conda environment layout incompatible with mise's extract-to-path
model. The CondaPackageInfo lockfile interface is preserved unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 24, 2026 03:22
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly overhauls the experimental Conda backend by replacing its custom implementation with the rattler Rust crates. This migration addresses previous limitations, such as direct reliance on the unsupported api.anaconda.org API, and significantly enhances the reliability and accuracy of Conda package management within mise. The change leverages rattler for all critical operations including repodata fetching, SAT-based dependency solving, package extraction, and virtual package detection, while retaining existing binary patching and lockfile formats.

Highlights

  • Conda Backend Refactor: Replaced approximately 985 lines of custom Conda backend code with the robust rattler Rust crates, which also power pixi.
  • Improved Dependency Resolution: Migrated to rattler_solve (using resolvo) for SAT-based dependency solving, offering more reliable and accurate dependency management.
  • Enhanced Repodata Handling: Switched to rattler_repodata_gateway for fetching repodata from proper CDN-cached repodata.json files, replacing direct calls to the unsupported api.anaconda.org API.
  • Modernized Package Extraction: Integrated rattler_package_streaming for efficient and correct extraction of both .conda (ZIP+tar.zst) and .tar.bz2 archive formats.
  • Accurate Virtual Package Detection: Implemented rattler_virtual_packages to correctly detect system virtual packages like __glibc and __osx, replacing a hardcoded SKIP_PACKAGES list.
Changelog
  • Cargo.lock
    • Updated to include numerous new dependencies required by the rattler crates and their transitive dependencies.
  • Cargo.toml
    • Added rattler_conda_types, rattler_repodata_gateway, rattler_solve, rattler_package_streaming, and rattler_virtual_packages as new dependencies.
    • Updated TLS features for rattler_repodata_gateway.
  • src/backend/conda.rs
    • Removed extensive custom logic for version matching, dependency resolution, package fetching from api.anaconda.org, and archive extraction.
    • Integrated rattler_conda_types for Conda data structures, rattler_repodata_gateway for channel repodata queries, rattler_solve for SAT-based dependency resolution, rattler_package_streaming for .conda and .tar.bz2 extraction, and rattler_virtual_packages for system virtual package detection.
    • Refactored install_version to use either install_fresh (for new solves) or install_from_locked (for reproducible installs from lockfile).
    • Updated _list_remote_versions and get_platform_info to query available versions and platform-specific package information using rattler components.
    • Simplified list_bin_paths as rattler handles the internal package layout.
    • Introduced new helper functions for rattler integration, such as create_gateway, target_to_conda_platform, detect_virtual_packages, flatten_repodata, solve_packages, url_filename, record_basename, format_sha256, download_record, download_url, extract_package, and `make_bins_executable.
    • Removed old internal structs like CondaPackageFile, CondaPackageAttrs, and ResolvedPackage, along with associated utility functions.
Activity
  • The pull request was generated using Claude Code.
  • All 481 unit tests passed successfully.
  • All lint checks passed.
  • End-to-end tests for conda:ruff@0.8.0, conda:bat@0.24.0, conda:jq@1.7.1, and conda:postgresql@17.2 passed, confirming successful installation and functionality, including library path patching.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request replaces the custom conda backend with the rattler Rust crates, which significantly refactors the conda integration. This change improves dependency resolution, package extraction, and virtual package detection by leveraging a more robust and actively maintained library. The changes to Cargo.lock and Cargo.toml reflect the new dependencies. The core logic in src/backend/conda.rs has been updated to use rattler's functionalities for fetching repodata, solving packages, and extracting archives, replacing the previous custom implementations. The _list_remote_versions and resolve_lock_info methods have been adapted to interact with the new rattler components. The install_version_ method now differentiates between fresh installs (solving dependencies) and locked installs (using pre-defined URLs). Overall, this is a substantial and positive change that moves the experimental conda backend towards a more stable and feature-rich foundation.

native-tls = [
"gix/blocking-http-transport-reqwest-native-tls",
"reqwest/native-tls",
"rattler_repodata_gateway/native-tls",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's good practice to keep feature names consistent. Since rattler_repodata_gateway is being used, consider renaming this feature to rattler-repodata-gateway/native-tls for clarity and consistency with other feature names.

rustls = [
"gix/blocking-http-transport-reqwest-rust-tls",
"reqwest/rustls-tls",
"rattler_repodata_gateway/rustls-tls",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the native-tls feature, consider renaming this feature to rattler-repodata-gateway/rustls-tls for consistency.

rustls-native-roots = [
"gix/blocking-http-transport-reqwest-rust-tls",
"reqwest/rustls-tls-native-roots",
"rattler_repodata_gateway/rustls-tls",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency, consider renaming this feature to rattler-repodata-gateway/rustls-tls to align with the naming convention used for other rattler features.

Comment on lines +76 to +80
fn channel(&self) -> Result<Channel> {
let name = self.channel_name();
let config = ChannelConfig::default_with_root_dir(std::path::PathBuf::from("/"));
Channel::from_str(&name, &config)
.map_err(|e| eyre::eyre!("invalid conda channel '{}': {}", name, e))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ChannelConfig::default_with_root_dir is initialized with /, which might not be the intended root directory for all environments. Consider making the root directory configurable or using a more appropriate default if / is not universally applicable.

Comment on lines +160 to +164
fn url_filename(url: &url::Url) -> String {
url.path_segments()
.and_then(|s| s.last())
.unwrap_or("package")
.to_string()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The url_filename function is used to extract the filename from a URL. While unwrap_or("package") provides a fallback, it might be more robust to return a Result or handle the None case more explicitly if url.path_segments() could genuinely be empty or malformed in a way that last() returns None for valid URLs.

Comment on lines +285 to +286
#[cfg(any(target_os = "macos", target_os = "linux"))]
conda_common::fix_text_prefixes(&install_path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The conda_common::fix_text_prefixes function is conditionally compiled for macOS and Linux. Similar to fix_library_paths, a comment explaining its absence or different handling on Windows would improve clarity.

Comment on lines +375 to +376
#[cfg(any(target_os = "macos", target_os = "linux"))]
platform::fix_library_paths(ctx, &install_path)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The platform::fix_library_paths function is conditionally compiled for macOS and Linux. It would be beneficial to add a comment explaining why this is not needed or handled differently on Windows, if applicable.

Comment on lines +378 to +379
#[cfg(any(target_os = "macos", target_os = "linux"))]
conda_common::fix_text_prefixes(&install_path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The conda_common::fix_text_prefixes function is conditionally compiled for macOS and Linux. Similar to fix_library_paths, a comment explaining its absence or different handling on Windows would improve clarity.

Comment on lines +455 to +456
let match_spec = MatchSpec::from_str(&tool_name, ParseStrictness::Lenient)
.map_err(|e| eyre::eyre!("invalid match spec for '{}': {}", tool_name, e))?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The MatchSpec::from_str call uses ParseStrictness::Lenient. While this might be desired for flexibility, it could lead to unexpected behavior if the input tool_name is ambiguous or malformed. Consider if ParseStrictness::Strict or a more controlled parsing approach is needed to ensure robustness.

Comment on lines +528 to +529
let match_spec = match MatchSpec::from_str(&spec_str, ParseStrictness::Lenient) {
Ok(s) => s,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The MatchSpec::from_str call uses ParseStrictness::Lenient. While this might be desired for flexibility, it could lead to unexpected behavior if the input tool_name is ambiguous or malformed. Consider if ParseStrictness::Strict or a more controlled parsing approach is needed to ensure robustness.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the experimental Conda backend’s custom package metadata fetching, dependency resolution, and archive extraction logic with the rattler crate ecosystem (repodata gateway + SAT solver + streaming extractor + virtual package detection), while keeping existing post-extraction patching behavior.

Changes:

  • Reworked conda backend to solve dependencies via rattler_repodata_gateway + rattler_solve and extract archives via rattler_package_streaming.
  • Added rattler_* crate dependencies and wired TLS feature flags for rattler_repodata_gateway.
  • Simplified lockfile dependency handling to store URLs/checksums derived from repodata records.

Reviewed changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 6 comments.

File Description
src/backend/conda.rs Replaces custom API-based resolution/extraction with rattler repodata + SAT solve + extraction; updates lockfile integration.
Cargo.toml Adds rattler_* crates and forwards TLS feature flags for the gateway crate.
Cargo.lock Locks transitive dependencies introduced by the new rattler crates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +114 to +127
/// Fetch repodata and solve the conda environment for the given specs and platform.
async fn solve_packages(
&self,
specs: Vec<MatchSpec>,
platform: CondaPlatform,
) -> Result<Vec<RepoDataRecord>> {
let channel = self.channel()?;
let gateway = Self::create_gateway();

let repodata: Vec<RepoData> = gateway
.query([channel], [platform, CondaPlatform::NoArch], specs.clone())
.recursive(true)
.await
.map_err(|e| eyre::eyre!("failed to fetch repodata: {}", e))?;
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solve_packages uses rattler_repodata_gateway directly, which bypasses the project-wide offline guard in crate::http (see HTTP.get_async_with_headers / Settings::offline()). In offline mode this backend will still attempt network access. Consider adding an explicit ensure!(!Settings::get().offline(), "offline mode is enabled") (or equivalent) before querying the gateway.

Copilot uses AI. Check for mistakes.
platform_to_conda_subdir(OS.as_str(), ARCH.as_str())
fn channel(&self) -> Result<Channel> {
let name = self.channel_name();
let config = ChannelConfig::default_with_root_dir(std::path::PathBuf::from("/"));
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChannelConfig::default_with_root_dir(PathBuf::from("/")) hard-codes / as the base for resolving relative channels (e.g. ./local-channel, file:...) and is also not a valid root on Windows. Use a real root dir such as the current working directory (preferred) or dirs::HOME, so relative channels resolve predictably on all platforms.

Suggested change
let config = ChannelConfig::default_with_root_dir(std::path::PathBuf::from("/"));
let root_dir = std::env::current_dir().unwrap_or_else(|_| dirs::HOME.clone());
let config = ChannelConfig::default_with_root_dir(root_dir);

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +198
/// Download a single package archive to the shared conda data dir.
async fn download_record(record: RepoDataRecord) -> Result<PathBuf> {
let url_str = record.url.to_string();
let filename = Self::url_filename(&record.url);
let dest = Self::conda_data_dir().join(&filename);

// Handle compound specs like ">=1.0,<2.0" by splitting on comma
if spec.contains(',') {
return spec
.split(',')
.all(|part| Self::version_matches(version, part.trim()));
if dest.exists() {
return Ok(dest);
}

// Comparison operators (>=, <=, >, <, ==, !=)
Self::check_version_constraint(version, spec)
file::create_dir_all(&Self::conda_data_dir())?;
HTTP.download_file(&url_str, &dest, None).await?;
Ok(dest)
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downloads are not verified against the sha256 provided by repodata/lockfile. This is an integrity regression: a corrupted/partial file in conda_data_dir will be silently reused (because of dest.exists()), and locked installs won't be deterministic. Consider verifying record.package_record.sha256 after download (and re-downloading on mismatch), and also verifying existing cached files before reusing them.

Copilot uses AI. Check for mistakes.
Comment on lines +344 to +363
let dep_basenames = platform_info.conda_deps.clone().unwrap_or_default();
let lockfile = self.read_lockfile_for_tool(tv)?;

// Check if file already exists with valid checksum
if tarball_path.exists() {
if Self::verify_checksum(&tarball_path, pkg.sha256.as_deref()).is_ok() {
return Ok(tarball_path);
// Collect dep URLs from lockfile (deps first, main last)
let mut urls: Vec<String> = vec![];
for basename in &dep_basenames {
if let Some(pkg_info) = lockfile.get_conda_package(platform_key, basename) {
urls.push(pkg_info.url.clone());
} else {
warn!(
"conda package {} not found in lockfile for {}",
basename, platform_key
);
}
// Corrupted file - delete it
let _ = std::fs::remove_file(&tarball_path);
}
urls.push(main_url);

// Download to a temp file first, then rename after verification
// This ensures the final path never contains a corrupted file
let temp_path = tarball_path.with_extension(format!(
"{}.tmp.{}",
tarball_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or(""),
std::process::id()
));

// Clean up any stale temp file from previous runs
let _ = std::fs::remove_file(&temp_path);

HTTP.download_file(&pkg.download_url, &temp_path, None)
.await
.wrap_err_with(|| format!("failed to download {}", pkg.download_url))?;

// Verify checksum of downloaded file
let file_size = std::fs::metadata(&temp_path).map(|m| m.len()).unwrap_or(0);
Self::verify_checksum(&temp_path, pkg.sha256.as_deref()).wrap_err_with(|| {
format!(
"checksum verification failed for {} (file size: {} bytes)",
pkg.name, file_size
)
})?;
ctx.pr
.set_message(format!("downloading {} packages", urls.len()));
let downloaded = parallel::parallel(urls, Self::download_url).await?;
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locked installs currently ignore per-package checksums stored in the lockfile (both for deps and the main package). To make the locked path actually reproducible, look up each package's checksum in the lockfile and verify it after download; if the checksum is missing, consider failing or at least not reusing an existing cached file without verification.

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +356
warn!(
"conda package {} not found in lockfile for {}",
basename, platform_key
);
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the locked install path, if a basename listed in platform_info.conda_deps is missing from the lockfile's [conda-packages] section, the code only logs a warning and continues. That can produce an incomplete/incorrect environment while still claiming to be using locked dependencies. It would be safer to return an error when a referenced locked package cannot be found.

Suggested change
warn!(
"conda package {} not found in lockfile for {}",
basename, platform_key
);
return Err(eyre::eyre!(
"conda package {} not found in lockfile for {}",
basename,
platform_key
));

Copilot uses AI. Check for mistakes.
Comment on lines +507 to +508
.and_then(|p| p.conda_deps.as_ref())
.is_some();
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has_locked is determined solely by the presence of conda_deps. If a tool has zero deps (so conda_deps is None) but the lockfile still contains a locked url/checksum for the main package, this will incorrectly take the fresh-solve path every time. Consider using platform_info.url.is_some() (or a similar check) to decide whether the lockfile should be used, independent of whether there are deps.

Suggested change
.and_then(|p| p.conda_deps.as_ref())
.is_some();
.map(|p| p.url.is_some() || p.conda_deps.as_ref().is_some())
.unwrap_or(false);

Copilot uses AI. Check for mistakes.
autofix-ci bot and others added 2 commits February 24, 2026 03:30
…link_package

Use rattler's link_package() for proper conda prefix replacement instead
of custom patchelf/install_name_tool invocations. This handles both text
and binary prefix replacement correctly, including codesigning on macOS.

Removes conda_common.rs, conda_linux.rs, conda_macos.rs (-638 lines).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Feb 24, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.19 x -- echo 20.7 ± 1.0 18.7 31.9 1.00
mise x -- echo 23.1 ± 1.4 20.9 36.5 1.12 ± 0.08
⚠️ Warning: Performance variance for x -- echo is 12%

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.19 env 20.0 ± 1.0 18.3 35.7 1.00
mise env 21.9 ± 0.7 20.0 24.4 1.10 ± 0.07

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.19 hook-env 19.7 ± 0.4 18.7 23.1 1.00
mise hook-env 22.2 ± 1.2 19.9 34.8 1.12 ± 0.06
⚠️ Warning: Performance variance for hook-env is 12%

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.19 ls 18.7 ± 0.8 17.0 27.8 1.00
mise ls 21.3 ± 0.8 19.5 32.0 1.14 ± 0.06
⚠️ Warning: Performance variance for ls is 14%

xtasks/test/perf

Command mise-2026.2.19 mise Variance
install (cached) 99ms ⚠️ 124ms -20%
ls (cached) 67ms 75ms -10%
bin-paths (cached) 69ms 77ms -10%
task-ls (cached) 2678ms ✅ 758ms +253%

⚠️ Warning: install cached performance variance is -20%
✅ Performance improvement: task-ls cached is 253%

jdx and others added 2 commits February 24, 2026 04:03
- Fix cross-platform virtual packages: use detect_for_platform() so
  lockfile generation on macOS correctly uses __glibc/__linux for Linux
  targets instead of __osx
- Fix zero-dep packages bypassing lockfile: check url.is_some() instead
  of conda_deps.is_some(), and always store conda_deps (even if empty)
- Error instead of warn when locked dep is missing from lockfile
- Use cwd instead of hardcoded "/" for ChannelConfig root dir

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify SHA256 checksums after downloading conda packages and before
reusing cached files. Uses atomic writes (download to .tmp, verify,
rename) to prevent partial/corrupt files from being cached.

Both fresh solves (checksum from repodata) and locked installs
(checksum from lockfile) are verified.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
size: None,
url_api: None,
conda_deps: Some(dep_basenames),
}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lockfile conda_deps always Some, even when empty

Low Severity

In resolve_lock_info, conda_deps is set to Some(dep_basenames) even when the list is empty, whereas the old code explicitly checked if conda_deps.is_empty() { None } else { Some(conda_deps) }. This causes unnecessary conda_deps = [] entries in lockfiles for packages that have no dependencies, adding noise to lockfile diffs. While consuming code handles both None and Some([]) identically via unwrap_or_default(), the inconsistency with the pattern used in install_fresh (which stores Some(dep_basenames.clone()) without the check) means lockfile output differs from what the old code produced.

Fix in Cursor Fix in Web

Conda packages on Windows can place binaries in either `bin/` (MSVC
builds) or `Library/bin/` (MSYS2/MinGW builds). The rattler solver
may pick a different build variant than the old custom resolver, so
we need to include both paths to ensure binaries are found regardless
of which build variant is installed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx enabled auto-merge (squash) February 24, 2026 05:31
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

} else {
bail!("unsupported conda package format: {}", filename);
file::create_dir_all(Self::conda_data_dir())?;
let temp = dest.with_extension("tmp");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temp file lacks unique suffix, enabling concurrent write races

Medium Severity

The download_to method creates a temp file using dest.with_extension("tmp"), producing a fixed temp path per destination. The old code included std::process::id() in the temp filename to prevent concurrent processes from clobbering each other's downloads. Without a unique suffix (e.g., PID or random), two mise processes downloading the same package simultaneously will write to the same temp file, risking data corruption before the atomic rename.

Fix in Cursor Fix in Web

Ok(tarball_path)
Self::make_bins_executable(&install_path)?;

Ok(())
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locked install path drops conda package data for lockfile

Medium Severity

install_from_locked takes tv as an immutable reference (&ToolVersion), so it cannot populate tv.conda_packages. The old code populated tv.conda_packages in both the locked and unlocked paths (the old comment explicitly stated "Store resolved packages in tv.conda_packages for lockfile update"). In the fresh path, install_fresh correctly writes to tv.conda_packages, but in the locked path this data is left empty. If any downstream code relies on tv.conda_packages to persist the shared [conda-packages] lockfile section after install, the locked path would silently lose those entries.

Additional Locations (1)

Fix in Cursor Fix in Web

@jdx jdx merged commit 9d901e5 into main Feb 24, 2026
34 of 35 checks passed
@jdx jdx deleted the feat/conda-rattler branch February 24, 2026 05:44
adamliang0 pushed a commit to adamliang0/mise that referenced this pull request Feb 24, 2026
## Summary

Replaces the experimental conda backend's ~1,625 lines of custom code
with the [rattler](https://github.com/conda/rattler) Rust crates — the
same crates that power [pixi](https://github.com/prefix-dev/pixi).
Motivated by [jdx#8318](jdx#8318) from
a conda maintainer who noted the backend was directly using the
unsupported `api.anaconda.org` API.

**What was replaced:**
- Custom version matching and comparison logic
- Recursive dependency resolution via raw anaconda.org API calls
- Manual `.conda` ZIP+tar.zst and `.tar.bz2` extraction
- Hardcoded `SKIP_PACKAGES` list for virtual packages
- Custom patchelf/install_name_tool/codesign binary patching
(`conda_common.rs`, `conda_linux.rs`, `conda_macos.rs` deleted)

**With rattler:**
- `rattler_repodata_gateway` — fetches repodata from conda channels
(proper CDN-cached repodata.json)
- `rattler_solve` (resolvo) — SAT-based dependency solver, same as used
by pixi/conda-libmamba
- `rattler_package_streaming` — extracts `.conda` and `.tar.bz2`
archives
- `rattler_virtual_packages` — detects system virtual packages
(`__glibc`, `__osx`, etc.) properly
- `rattler::install::link_package` — proper conda installation with text
AND binary prefix replacement, file permissions, and macOS codesigning

**Key fix:** The old code explicitly skipped binary files during prefix
replacement (checking for null bytes). `link_package` correctly handles
binary prefix replacement by padding with null bytes to preserve byte
length, which is how conda is designed to work.

**Kept unchanged:**
- `lockfile.rs` — `CondaPackageInfo` type and `[conda-packages]` TOML
format
- All registry entries and settings

Since `EXPERIMENTAL = true`, breaking changes are acceptable.

## Test plan

- [x] `cargo build` — compiles cleanly
- [x] `mise run test:unit` — 481 tests pass
- [x] `mise run lint` — all checks pass
- [x] `mise run test:e2e test_conda`:
  - `conda:ruff@0.8.0` — installs with 24 deps, runs correctly
  - `conda:bat@0.24.0` — installs with 4 deps, runs correctly
  - `conda:jq@1.7.1` — installs with deps (oniguruma), runs correctly
  - `conda:postgresql@17.2` — installs with 29 deps, runs correctly
- [x] `mise lock` — lockfile generated correctly with per-platform deps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Large rewrite of conda install/solve behavior (networking, dependency
resolution, and prefix handling) that could change which packages get
installed or how they’re linked; mitigated somewhat by being confined to
the experimental conda backend and adding checksum-verified,
lockfile-driven installs.
> 
> **Overview**
> Switches the experimental `conda` backend from bespoke anaconda.org
API calls, ad-hoc version/dependency selection, and manual archive
extraction/prefix patching to the `rattler` ecosystem:
`rattler_repodata_gateway` for repodata retrieval/caching,
`rattler_solve` (resolvo) for dependency solving with virtual package
detection, and `rattler::install::link_package` +
`rattler_package_streaming` for extraction and correct text/binary
prefix replacement.
> 
> Lockfile behavior is preserved but now populated from solver
`RepoDataRecord`s (URLs + sha256) and can perform deterministic installs
by downloading the locked URL set with checksum verification; Windows
bin path discovery is broadened to include both `Library/bin` and `bin`.
The previous platform-specific patching modules (`conda_common.rs`,
`conda_linux.rs`, `conda_macos.rs`) are removed, and Cargo
dependencies/lockfile are updated to include the new `rattler*` crates
and transitive deps.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
21d3602. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
@jezdez
Copy link

jezdez commented Feb 24, 2026

Thank you @jdx!

@jdx
Copy link
Owner Author

jdx commented Feb 24, 2026

I should be thanking you!

mise-en-dev added a commit that referenced this pull request Feb 25, 2026
### 🚀 Features

- **(conda)** replace custom backend with rattler crates by @jdx in
[#8325](#8325)
- **(task)** enforce per-task timeout configuration by @tvararu in
[#8250](#8250)
- **(vsix)** added vsix archives to http backend by @sosumappu in
[#8306](#8306)
- add core dotnet plugin for .NET SDK management by @jdx in
[#8326](#8326)

### 🐛 Bug Fixes

- **(conda)** preserve conda_packages on locked install and fix temp
file race by @jdx in [#8335](#8335)
- **(conda)** deduplicate repodata records to fix solver error on Linux
by @jdx in [#8337](#8337)
- **(env)** include watch_files in fast-path early exit check by @jdx in
[#8317](#8317)
- **(env)** clear fish completions when setting/unsetting shell aliases
by @jdx in [#8324](#8324)
- **(lockfile)** prevent lockfile writes when --locked is set by @jdx in
[#8308](#8308)
- **(lockfile)** prune orphan tool entries on mise lock by @mackwic in
[#8265](#8265)
- **(lockfile)** error on contradictory locked=true + lockfile=false
config by @jdx in [#8329](#8329)
- **(regal)** Update package location by @charlieegan3 in
[#8315](#8315)
- **(release)** strip markdown heading prefix from communique release
title by @jdx in [#8303](#8303)
- **(schema)** enforce additionalProperties constraint for env by
@adamliang0 in [#8328](#8328)

### 📚 Documentation

- Remove incorrect oh-my-zsh plugin ordering comment by @bvosk in
[#8323](#8323)
- require AI disclosure on GitHub comments by @jdx in
[#8330](#8330)

### 📦 Registry

- add `oxfmt` by @taoufik07 in
[#8316](#8316)

### New Contributors

- @adamliang0 made their first contribution in
[#8328](#8328)
- @tvararu made their first contribution in
[#8250](#8250)
- @bvosk made their first contribution in
[#8323](#8323)
- @taoufik07 made their first contribution in
[#8316](#8316)
- @charlieegan3 made their first contribution in
[#8315](#8315)
- @sosumappu made their first contribution in
[#8306](#8306)

## 📦 Aqua Registry Updates

#### New Packages (3)

- [`Tyrrrz/FFmpegBin`](https://github.com/Tyrrrz/FFmpegBin)
- [`elixir-lang/expert`](https://github.com/elixir-lang/expert)
- [`erikjuhani/basalt`](https://github.com/erikjuhani/basalt)

#### Updated Packages (5)

- [`caarlos0/fork-cleaner`](https://github.com/caarlos0/fork-cleaner)
-
[`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local)
- [`jackchuka/mdschema`](https://github.com/jackchuka/mdschema)
-
[`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases)
- [`peco/peco`](https://github.com/peco/peco)
@jezdez
Copy link

jezdez commented Feb 25, 2026

@jdx Well, the alternative would have been to block requests by mise at the repo level since you were not following the conda standards and misunderstood how it worked (or your agents?), and would have led to a suboptimal user experience. No hard feelings!

Fortunately for us, @wolfv and the folks at @prefix-dev had already implemented part of the conda specs in the rattler crates and kindly donated it to the conda OSS organization, which I very much appreciate because of exactly these types of cases of downstream projects.

Thank you for taking my feedback and running with it since it will make things work better for users

risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
## Summary

Replaces the experimental conda backend's ~1,625 lines of custom code
with the [rattler](https://github.com/conda/rattler) Rust crates — the
same crates that power [pixi](https://github.com/prefix-dev/pixi).
Motivated by [jdx#8318](jdx#8318) from
a conda maintainer who noted the backend was directly using the
unsupported `api.anaconda.org` API.

**What was replaced:**
- Custom version matching and comparison logic
- Recursive dependency resolution via raw anaconda.org API calls
- Manual `.conda` ZIP+tar.zst and `.tar.bz2` extraction
- Hardcoded `SKIP_PACKAGES` list for virtual packages
- Custom patchelf/install_name_tool/codesign binary patching
(`conda_common.rs`, `conda_linux.rs`, `conda_macos.rs` deleted)

**With rattler:**
- `rattler_repodata_gateway` — fetches repodata from conda channels
(proper CDN-cached repodata.json)
- `rattler_solve` (resolvo) — SAT-based dependency solver, same as used
by pixi/conda-libmamba
- `rattler_package_streaming` — extracts `.conda` and `.tar.bz2`
archives
- `rattler_virtual_packages` — detects system virtual packages
(`__glibc`, `__osx`, etc.) properly
- `rattler::install::link_package` — proper conda installation with text
AND binary prefix replacement, file permissions, and macOS codesigning

**Key fix:** The old code explicitly skipped binary files during prefix
replacement (checking for null bytes). `link_package` correctly handles
binary prefix replacement by padding with null bytes to preserve byte
length, which is how conda is designed to work.

**Kept unchanged:**
- `lockfile.rs` — `CondaPackageInfo` type and `[conda-packages]` TOML
format
- All registry entries and settings

Since `EXPERIMENTAL = true`, breaking changes are acceptable.

## Test plan

- [x] `cargo build` — compiles cleanly
- [x] `mise run test:unit` — 481 tests pass
- [x] `mise run lint` — all checks pass
- [x] `mise run test:e2e test_conda`:
  - `conda:ruff@0.8.0` — installs with 24 deps, runs correctly
  - `conda:bat@0.24.0` — installs with 4 deps, runs correctly
  - `conda:jq@1.7.1` — installs with deps (oniguruma), runs correctly
  - `conda:postgresql@17.2` — installs with 29 deps, runs correctly
- [x] `mise lock` — lockfile generated correctly with per-platform deps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Large rewrite of conda install/solve behavior (networking, dependency
resolution, and prefix handling) that could change which packages get
installed or how they’re linked; mitigated somewhat by being confined to
the experimental conda backend and adding checksum-verified,
lockfile-driven installs.
> 
> **Overview**
> Switches the experimental `conda` backend from bespoke anaconda.org
API calls, ad-hoc version/dependency selection, and manual archive
extraction/prefix patching to the `rattler` ecosystem:
`rattler_repodata_gateway` for repodata retrieval/caching,
`rattler_solve` (resolvo) for dependency solving with virtual package
detection, and `rattler::install::link_package` +
`rattler_package_streaming` for extraction and correct text/binary
prefix replacement.
> 
> Lockfile behavior is preserved but now populated from solver
`RepoDataRecord`s (URLs + sha256) and can perform deterministic installs
by downloading the locked URL set with checksum verification; Windows
bin path discovery is broadened to include both `Library/bin` and `bin`.
The previous platform-specific patching modules (`conda_common.rs`,
`conda_linux.rs`, `conda_macos.rs`) are removed, and Cargo
dependencies/lockfile are updated to include the new `rattler*` crates
and transitive deps.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
21d3602. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
### 🚀 Features

- **(conda)** replace custom backend with rattler crates by @jdx in
[jdx#8325](jdx#8325)
- **(task)** enforce per-task timeout configuration by @tvararu in
[jdx#8250](jdx#8250)
- **(vsix)** added vsix archives to http backend by @sosumappu in
[jdx#8306](jdx#8306)
- add core dotnet plugin for .NET SDK management by @jdx in
[jdx#8326](jdx#8326)

### 🐛 Bug Fixes

- **(conda)** preserve conda_packages on locked install and fix temp
file race by @jdx in [jdx#8335](jdx#8335)
- **(conda)** deduplicate repodata records to fix solver error on Linux
by @jdx in [jdx#8337](jdx#8337)
- **(env)** include watch_files in fast-path early exit check by @jdx in
[jdx#8317](jdx#8317)
- **(env)** clear fish completions when setting/unsetting shell aliases
by @jdx in [jdx#8324](jdx#8324)
- **(lockfile)** prevent lockfile writes when --locked is set by @jdx in
[jdx#8308](jdx#8308)
- **(lockfile)** prune orphan tool entries on mise lock by @mackwic in
[jdx#8265](jdx#8265)
- **(lockfile)** error on contradictory locked=true + lockfile=false
config by @jdx in [jdx#8329](jdx#8329)
- **(regal)** Update package location by @charlieegan3 in
[jdx#8315](jdx#8315)
- **(release)** strip markdown heading prefix from communique release
title by @jdx in [jdx#8303](jdx#8303)
- **(schema)** enforce additionalProperties constraint for env by
@adamliang0 in [jdx#8328](jdx#8328)

### 📚 Documentation

- Remove incorrect oh-my-zsh plugin ordering comment by @bvosk in
[jdx#8323](jdx#8323)
- require AI disclosure on GitHub comments by @jdx in
[jdx#8330](jdx#8330)

### 📦 Registry

- add `oxfmt` by @taoufik07 in
[jdx#8316](jdx#8316)

### New Contributors

- @adamliang0 made their first contribution in
[jdx#8328](jdx#8328)
- @tvararu made their first contribution in
[jdx#8250](jdx#8250)
- @bvosk made their first contribution in
[jdx#8323](jdx#8323)
- @taoufik07 made their first contribution in
[jdx#8316](jdx#8316)
- @charlieegan3 made their first contribution in
[jdx#8315](jdx#8315)
- @sosumappu made their first contribution in
[jdx#8306](jdx#8306)

## 📦 Aqua Registry Updates

#### New Packages (3)

- [`Tyrrrz/FFmpegBin`](https://github.com/Tyrrrz/FFmpegBin)
- [`elixir-lang/expert`](https://github.com/elixir-lang/expert)
- [`erikjuhani/basalt`](https://github.com/erikjuhani/basalt)

#### Updated Packages (5)

- [`caarlos0/fork-cleaner`](https://github.com/caarlos0/fork-cleaner)
-
[`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local)
- [`jackchuka/mdschema`](https://github.com/jackchuka/mdschema)
-
[`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases)
- [`peco/peco`](https://github.com/peco/peco)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants