use crate::backend::VersionInfo;
use crate::backend::backend_type::BackendType;
use crate::backend::platform_target::PlatformTarget;
use crate::cli::args::BackendArg;
use crate::config::Config;
use crate::config::Settings;
use crate::http::HTTP;
use crate::install_context::InstallContext;
use crate::lockfile::{self, Lockfile, PlatformInfo};
use crate::toolset::ToolSource;
use crate::toolset::ToolVersion;
use crate::{backend::Backend, dirs, parallel};
use crate::{file, hash};
use async_trait::async_trait;
use eyre::Result;
use itertools::Itertools;
use rattler::install::{InstallDriver, InstallOptions, PythonInfo, link_package};
use rattler_conda_types::{
Channel, ChannelConfig, GenericVirtualPackage, MatchSpec, ParseStrictness,
Platform as CondaPlatform, RepoDataRecord, prefix::Prefix, prefix_record::PathsEntry,
};
use rattler_repodata_gateway::{Gateway, RepoData};
use rattler_solve::{
ChannelPriority, SolveStrategy, SolverImpl, SolverTask, resolvo::Solver as ResolvoSolver,
};
use rattler_virtual_packages::{VirtualPackageOverrides, VirtualPackages};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashSet};
use std::fmt::Debug;
use std::path::PathBuf;
use std::sync::Arc;
use versions::Versioning;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CondaPackageInfo {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum: Option<String>,
}
pub const EXPERIMENTAL: bool = true;
#[derive(Debug)]
pub struct CondaBackend {
ba: Arc<BackendArg>,
}
impl CondaBackend {
pub fn from_arg(ba: BackendArg) -> Self {
Self { ba: Arc::new(ba) }
}
fn channel_name(&self) -> String {
self.ba
.opts()
.get("channel")
.map(|s| s.to_string())
.unwrap_or_else(|| Settings::get().conda.channel.clone())
}
fn channel(&self) -> Result<Channel> {
let name = self.channel_name();
let root_dir = std::env::current_dir().unwrap_or_else(|_| dirs::HOME.to_path_buf());
let config = ChannelConfig::default_with_root_dir(root_dir);
Channel::from_str(&name, &config)
.map_err(|e| eyre::eyre!("invalid conda channel '{}': {}", name, e))
}
fn create_gateway() -> Gateway {
Gateway::builder()
.with_cache_dir(dirs::CACHE.join("conda"))
.finish()
}
fn target_to_conda_platform(target: &PlatformTarget) -> CondaPlatform {
match (target.os_name(), target.arch_name()) {
("linux", "x64") => CondaPlatform::Linux64,
("linux", "arm64") => CondaPlatform::LinuxAarch64,
("macos", "x64") => CondaPlatform::Osx64,
("macos", "arm64") => CondaPlatform::OsxArm64,
("windows", "x64") => CondaPlatform::Win64,
_ => CondaPlatform::NoArch,
}
}
fn detect_virtual_packages(platform: CondaPlatform) -> Vec<GenericVirtualPackage> {
VirtualPackages::detect_for_platform(platform, &VirtualPackageOverrides::default())
.map(|vp| vp.into_generic_virtual_packages().collect())
.unwrap_or_default()
}
fn flatten_repodata(repodata: &[RepoData]) -> Vec<RepoDataRecord> {
let mut seen = HashSet::new();
repodata
.iter()
.flat_map(|rd| rd.iter().cloned())
.filter(|r| seen.insert(r.url.clone()))
.collect()
}
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))?;
let flat_records = Self::flatten_repodata(&repodata);
let virtual_packages = Self::detect_virtual_packages(platform);
let task = SolverTask {
available_packages: [flat_records.as_slice()],
specs,
virtual_packages,
locked_packages: vec![],
pinned_packages: vec![],
constraints: vec![],
timeout: None,
channel_priority: ChannelPriority::Strict,
exclude_newer: None,
strategy: SolveStrategy::Highest,
dependency_overrides: vec![],
};
let mut solver = ResolvoSolver;
let result = solver
.solve(task)
.map_err(|e| eyre::eyre!("conda solve failed: {}", e))?;
Ok(result.records)
}
fn conda_data_dir() -> PathBuf {
dirs::DATA.join("conda-packages")
}
fn url_filename(url: &url::Url) -> String {
url.path_segments()
.and_then(|mut s| s.next_back())
.unwrap_or("package")
.to_string()
}
fn record_basename(record: &RepoDataRecord) -> String {
let filename = Self::url_filename(&record.url);
filename
.strip_suffix(".conda")
.or_else(|| filename.strip_suffix(".tar.bz2"))
.unwrap_or(&filename)
.to_string()
}
fn format_sha256(record: &RepoDataRecord) -> Option<String> {
record
.package_record
.sha256
.as_ref()
.map(|h| format!("sha256:{}", hex::encode(h)))
}
fn verify_checksum(path: &std::path::Path, expected: Option<&str>) -> Result<bool> {
let Some(expected) = expected else {
return Ok(true);
};
let Some(expected_hex) = expected.strip_prefix("sha256:") else {
return Ok(true);
};
let actual_hex = hash::file_hash_sha256(path, None)?;
Ok(actual_hex == expected_hex)
}
async fn download_to(url: &str, dest: &std::path::Path, checksum: Option<&str>) -> Result<()> {
if dest.exists() && Self::verify_checksum(dest, checksum)? {
return Ok(());
}
file::create_dir_all(Self::conda_data_dir())?;
let temp = dest.with_extension(format!("tmp.{}", std::process::id()));
HTTP.download_file(url, &temp, None).await?;
if !Self::verify_checksum(&temp, checksum)? {
let _ = file::remove_all(&temp);
let display_checksum = checksum.unwrap_or("unknown");
return Err(eyre::eyre!(
"checksum mismatch for {}: expected {}",
url,
display_checksum,
));
}
file::rename(&temp, dest)?;
Ok(())
}
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);
let checksum = Self::format_sha256(&record);
Self::download_to(&url_str, &dest, checksum.as_deref()).await?;
Ok(dest)
}
async fn download_url_with_checksum(
(url_str, checksum): (String, Option<String>),
) -> Result<PathBuf> {
let filename = url_str.rsplit('/').next().unwrap_or("package").to_string();
let dest = Self::conda_data_dir().join(&filename);
Self::download_to(&url_str, &dest, checksum.as_deref()).await?;
Ok(dest)
}
async fn extract_package(archive: &std::path::Path, dest: &std::path::Path) -> Result<()> {
rattler_package_streaming::tokio::fs::extract(archive, dest)
.await
.map_err(|e| eyre::eyre!("failed to extract {}: {}", archive.display(), e))?;
Ok(())
}
async fn install_package(
archive: &std::path::Path,
prefix: &Prefix,
driver: &InstallDriver,
python_info: Option<PythonInfo>,
) -> Result<Vec<PathsEntry>> {
let temp_dir = tempfile::tempdir()?;
Self::extract_package(archive, temp_dir.path()).await?;
let install_options = InstallOptions {
python_info,
..InstallOptions::default()
};
let paths = link_package(temp_dir.path(), prefix, driver, install_options)
.await
.map_err(|e| eyre::eyre!("failed to link {}: {}", archive.display(), e))?;
Ok(paths)
}
fn python_info_from_records(
records: &[RepoDataRecord],
platform: CondaPlatform,
) -> Option<PythonInfo> {
records
.iter()
.find(|r| r.package_record.name.as_normalized() == "python")
.and_then(|r| {
PythonInfo::from_version(
r.package_record.version.version(),
r.package_record.python_site_packages_path.as_deref(),
platform,
)
.ok()
})
}
fn python_info_from_basenames(
basenames: &[String],
platform: CondaPlatform,
) -> Option<PythonInfo> {
use rattler_conda_types::Version;
use rattler_conda_types::package::ArchiveIdentifier;
use std::str::FromStr;
basenames.iter().find_map(|b| {
let id = ArchiveIdentifier::from_str(b).ok()?;
if id.name != "python" {
return None;
}
let version = Version::from_str(&id.version).ok()?;
PythonInfo::from_version(&version, None, platform).ok()
})
}
fn read_lockfile_for_tool(&self, tv: &ToolVersion) -> Result<Lockfile> {
match tv.request.source() {
ToolSource::MiseToml(path) => {
let (lockfile_path, _) = lockfile::lockfile_path_for_config(path);
Lockfile::read(&lockfile_path)
}
_ => Ok(Lockfile::default()),
}
}
async fn install_fresh(
&self,
ctx: &InstallContext,
tv: &mut ToolVersion,
platform_key: &str,
) -> Result<()> {
let tool_name = self.tool_name();
let spec_str = format!("{}=={}", tool_name, tv.version);
let match_spec = MatchSpec::from_str(&spec_str, ParseStrictness::Lenient)
.map_err(|e| eyre::eyre!("invalid conda spec '{}': {}", spec_str, e))?;
ctx.pr.set_message("fetching repodata".to_string());
let records = self
.solve_packages(vec![match_spec], CondaPlatform::current())
.await?;
let tool_name_norm = tool_name.to_lowercase();
let (main_vec, dep_records): (Vec<_>, Vec<_>) = records
.into_iter()
.partition(|r| r.package_record.name.as_normalized() == tool_name_norm);
let main_record = main_vec
.into_iter()
.next()
.ok_or_else(|| eyre::eyre!("main package {} not found in solve result", tool_name))?;
let mut all_records = dep_records;
all_records.push(main_record.clone());
let python_info = Self::python_info_from_records(&all_records, CondaPlatform::current());
ctx.pr
.set_message(format!("downloading {} packages", all_records.len()));
let downloaded = parallel::parallel(all_records.clone(), Self::download_record).await?;
let install_path = tv.install_path();
file::remove_all(&install_path)?;
file::create_dir_all(&install_path)?;
let prefix = Prefix::create(&install_path)
.map_err(|e| eyre::eyre!("failed to create conda prefix: {}", e))?;
let driver = InstallDriver::default();
let mut main_paths = Vec::new();
for (record, archive) in all_records.iter().zip(downloaded.iter()) {
let name = record.package_record.name.as_normalized();
let is_main = name == tool_name_norm;
ctx.pr.set_message(format!("installing {name}"));
let paths =
Self::install_package(archive, &prefix, &driver, python_info.clone()).await?;
if is_main {
main_paths = paths;
}
}
Self::make_bins_executable(&install_path)?;
self.create_symlink_bin_dir(tv, &main_paths)?;
let n_deps = all_records.len() - 1; let dep_basenames: Vec<String> = all_records[..n_deps]
.iter()
.map(Self::record_basename)
.collect();
let platform_info = tv
.lock_platforms
.entry(platform_key.to_string())
.or_default();
platform_info.url = Some(main_record.url.to_string());
platform_info.checksum = Self::format_sha256(&main_record);
platform_info.conda_deps = Some(dep_basenames.clone());
for record in &all_records[..n_deps] {
let basename = Self::record_basename(record);
tv.conda_packages.insert(
(platform_key.to_string(), basename),
CondaPackageInfo {
url: record.url.to_string(),
checksum: Self::format_sha256(record),
},
);
}
Ok(())
}
async fn install_from_locked(
&self,
ctx: &InstallContext,
tv: &mut ToolVersion,
platform_key: &str,
) -> Result<()> {
ctx.pr.set_message("using locked dependencies".to_string());
let platform_info = tv
.lock_platforms
.get(platform_key)
.ok_or_else(|| eyre::eyre!("no lock info for platform {}", platform_key))?;
let main_url = platform_info
.url
.as_ref()
.ok_or_else(|| eyre::eyre!("no URL in lockfile for {}", self.tool_name()))?
.clone();
let main_checksum = platform_info.checksum.clone();
let dep_basenames = platform_info.conda_deps.clone().unwrap_or_default();
let lockfile = self.read_lockfile_for_tool(tv)?;
let python_info =
Self::python_info_from_basenames(&dep_basenames, CondaPlatform::current());
let mut downloads: Vec<(String, Option<String>)> = vec![];
for basename in &dep_basenames {
if let Some(pkg_info) = lockfile.get_conda_package(platform_key, basename) {
downloads.push((pkg_info.url.clone(), pkg_info.checksum.clone()));
} else {
return Err(eyre::eyre!(
"conda package {} not found in lockfile for {}",
basename,
platform_key
));
}
}
downloads.push((main_url, main_checksum));
ctx.pr
.set_message(format!("downloading {} packages", downloads.len()));
let downloaded = parallel::parallel(downloads, Self::download_url_with_checksum).await?;
let install_path = tv.install_path();
file::remove_all(&install_path)?;
file::create_dir_all(&install_path)?;
let prefix = Prefix::create(&install_path)
.map_err(|e| eyre::eyre!("failed to create conda prefix: {}", e))?;
let driver = InstallDriver::default();
let mut main_paths = Vec::new();
for archive in &downloaded {
let filename = archive.file_name().and_then(|n| n.to_str()).unwrap_or("?");
ctx.pr.set_message(format!("installing {filename}"));
main_paths =
Self::install_package(archive, &prefix, &driver, python_info.clone()).await?;
}
Self::make_bins_executable(&install_path)?;
self.create_symlink_bin_dir(tv, &main_paths)?;
for basename in &dep_basenames {
if let Some(pkg_info) = lockfile.get_conda_package(platform_key, basename) {
tv.conda_packages.insert(
(platform_key.to_string(), basename.clone()),
pkg_info.clone(),
);
}
}
Ok(())
}
fn make_bins_executable(install_path: &std::path::Path) -> Result<()> {
let bin_path = if cfg!(windows) {
install_path.join("Library").join("bin")
} else {
install_path.join("bin")
};
if bin_path.exists() {
for entry in std::fs::read_dir(&bin_path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
file::make_executable(&path)?;
}
}
}
Ok(())
}
fn create_symlink_bin_dir(&self, tv: &ToolVersion, main_paths: &[PathsEntry]) -> Result<()> {
let symlink_dir = tv.install_path().join(".mise-bins");
file::create_dir_all(&symlink_dir)?;
let install_path = tv.install_path();
let bin_dirs: &[&std::path::Path] = if cfg!(windows) {
&[
std::path::Path::new("Library/bin"),
std::path::Path::new("Scripts"),
std::path::Path::new("bin"),
]
} else {
&[std::path::Path::new("bin")]
};
for entry in main_paths {
if !bin_dirs
.iter()
.any(|dir| entry.relative_path.starts_with(dir))
{
continue;
}
let Some(bin_name) = entry.relative_path.file_name() else {
continue;
};
let src = install_path.join(&entry.relative_path);
let dst = symlink_dir.join(bin_name);
if src.exists() && !dst.exists() {
file::make_symlink_or_copy(&src, &dst)?;
}
}
Ok(())
}
pub async fn resolve_conda_packages(
&self,
tv: &ToolVersion,
target: &PlatformTarget,
) -> Result<BTreeMap<String, CondaPackageInfo>> {
let platform = Self::target_to_conda_platform(target);
let tool_name = self.tool_name();
let spec_str = format!("{}=={}", tool_name, tv.version);
let match_spec = MatchSpec::from_str(&spec_str, ParseStrictness::Lenient)
.map_err(|e| eyre::eyre!("invalid conda spec '{}': {}", spec_str, e))?;
let records = self.solve_packages(vec![match_spec], platform).await?;
let tool_name_norm = tool_name.to_lowercase();
let mut result = BTreeMap::new();
for record in &records {
if record.package_record.name.as_normalized() == tool_name_norm {
continue;
}
let basename = Self::record_basename(record);
result.insert(
basename,
CondaPackageInfo {
url: record.url.to_string(),
checksum: Self::format_sha256(record),
},
);
}
Ok(result)
}
}
#[async_trait]
impl Backend for CondaBackend {
fn get_type(&self) -> BackendType {
BackendType::Conda
}
fn ba(&self) -> &Arc<BackendArg> {
&self.ba
}
async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<VersionInfo>> {
let channel = self.channel()?;
let current_platform = CondaPlatform::current();
let tool_name = self.tool_name();
let gateway = Self::create_gateway();
let match_spec = MatchSpec::from_str(&tool_name, ParseStrictness::Lenient)
.map_err(|e| eyre::eyre!("invalid match spec for '{}': {}", tool_name, e))?;
let repodata: Vec<RepoData> = gateway
.query(
[channel],
[current_platform, CondaPlatform::NoArch],
[match_spec],
)
.await
.map_err(|e| eyre::eyre!("failed to list versions for '{}': {}", tool_name, e))?;
let mut version_set: std::collections::HashSet<String> = std::collections::HashSet::new();
for data in &repodata {
for record in data {
version_set.insert(record.package_record.version.to_string());
}
}
let versions = version_set
.into_iter()
.map(|version| VersionInfo {
version,
..Default::default()
})
.sorted_by_cached_key(|v| Versioning::new(&v.version))
.collect();
Ok(versions)
}
async fn list_remote_versions_with_info(
&self,
config: &Arc<Config>,
) -> Result<Vec<VersionInfo>> {
self._list_remote_versions(config).await
}
async fn install_version_(
&self,
ctx: &InstallContext,
mut tv: ToolVersion,
) -> Result<ToolVersion> {
Settings::get().ensure_experimental("conda backend")?;
let platform_key = self.get_platform_key();
let has_locked = tv
.lock_platforms
.get(&platform_key)
.and_then(|p| p.url.as_ref())
.is_some();
if has_locked {
self.install_from_locked(ctx, &mut tv, &platform_key)
.await?;
} else {
self.install_fresh(ctx, &mut tv, &platform_key).await?;
}
Ok(tv)
}
async fn resolve_lock_info(
&self,
tv: &ToolVersion,
target: &PlatformTarget,
) -> Result<PlatformInfo> {
let platform = Self::target_to_conda_platform(target);
let tool_name = self.tool_name();
let spec_str = format!("{}=={}", tool_name, tv.version);
let match_spec = match MatchSpec::from_str(&spec_str, ParseStrictness::Lenient) {
Ok(s) => s,
Err(e) => {
debug!("invalid conda spec '{}': {}", spec_str, e);
return Ok(PlatformInfo::default());
}
};
let records = match self.solve_packages(vec![match_spec], platform).await {
Ok(r) => r,
Err(e) => {
debug!(
"failed to resolve {} for {}: {}",
tool_name,
target.to_key(),
e
);
return Ok(PlatformInfo::default());
}
};
let tool_name_norm = tool_name.to_lowercase();
let mut main_record = None;
let mut dep_basenames: Vec<String> = vec![];
for record in &records {
if record.package_record.name.as_normalized() == tool_name_norm {
main_record = Some(record.clone());
} else {
dep_basenames.push(Self::record_basename(record));
}
}
match main_record {
Some(main) => Ok(PlatformInfo {
url: Some(main.url.to_string()),
checksum: Self::format_sha256(&main),
size: None,
url_api: None,
conda_deps: Some(dep_basenames),
..Default::default()
}),
None => Ok(PlatformInfo::default()),
}
}
async fn list_bin_paths(
&self,
_config: &Arc<Config>,
tv: &ToolVersion,
) -> Result<Vec<PathBuf>> {
let mise_bins = tv.install_path().join(".mise-bins");
if mise_bins.exists() {
return Ok(vec![mise_bins]);
}
let install_path = tv.install_path();
if cfg!(windows) {
Ok(vec![
install_path.join("Library").join("bin"),
install_path.join("bin"),
])
} else {
Ok(vec![install_path.join("bin")])
}
}
}