mise 2026.8.11

Dev tools, env vars, and tasks in one CLI
use eyre::Result;
use itertools::sorted;
use std::env::consts::{ARCH, OS};

use crate::{backend, config, dirs, env, file};
use crate::{config::Config, env::PYENV_ROOT};

use super::reconcile;

/// Symlinks all tool versions from an external tool into mise
///
/// For example, use this to import all pyenv installs into mise
///
/// This won't overwrite managed installs, runtime aliases, or links from other providers.
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub(super) struct SyncPython {
    /// Get tool versions from pyenv
    #[usage(long)]
    pyenv: bool,

    /// Sync tool versions with uv (2-way sync)
    #[usage(long)]
    uv: bool,
}

impl SyncPython {
    pub(super) async fn run(self) -> Result<()> {
        let python = backend::get(&"python".into()).unwrap();
        let mut providers = vec![];
        if self.pyenv {
            providers.push(self.pyenv_links()?);
        }
        if self.uv {
            providers.push(self.uv_links()?);
        }
        let mut changed = reconcile::reconcile_all(python.ba(), providers)?.into_iter();
        if self.pyenv {
            for v in changed.next().unwrap_or_default() {
                miseprintln!("Synced python@{} from pyenv", v);
            }
        }
        if self.uv {
            for v in changed.next().unwrap_or_default() {
                miseprintln!("Synced python@{v} from uv to mise");
            }
            self.sync_mise_installs_to_uv()?;
        }
        let config = Config::get().await?;
        let ts = config.get_toolset().await?;
        config::rebuild_shims_and_runtime_symlinks(
            &config,
            ts,
            &[],
            crate::lockfile::LockfileUpdateMode::Normal,
        )
        .await?;
        Ok(())
    }

    fn pyenv_links(&self) -> Result<reconcile::ProviderLinks> {
        let pyenv_versions_path = PYENV_ROOT.join("versions");

        let subdirs = file::dir_subdirs(&pyenv_versions_path)?;
        let mut links = vec![];
        for v in sorted(subdirs) {
            if v.starts_with(".") {
                continue;
            }
            links.push((v.clone(), pyenv_versions_path.join(&v)));
        }
        let ownership = reconcile::LinkOwnership::in_namespace(&pyenv_versions_path);
        Ok(reconcile::ProviderLinks::new(ownership, links))
    }

    fn uv_links(&self) -> Result<reconcile::ProviderLinks> {
        let uv_versions_path = &*env::UV_PYTHON_INSTALL_DIR;

        let subdirs = file::dir_subdirs(uv_versions_path)?;
        let mut links = vec![];
        for name in sorted(subdirs) {
            if name.starts_with(".") {
                continue;
            }
            // name is like cpython-3.13.1-macos-aarch64-none
            let Some(v) = name.split('-').nth(1).filter(|v| !v.is_empty()) else {
                debug!("skipping unrecognized uv python dir: {name}");
                continue;
            };
            links.push((v.to_string(), uv_versions_path.join(&name)));
        }
        let ownership = reconcile::LinkOwnership::in_namespace(uv_versions_path);
        Ok(reconcile::ProviderLinks::new(ownership, links))
    }

    fn sync_mise_installs_to_uv(&self) -> Result<()> {
        let uv_versions_path = &*env::UV_PYTHON_INSTALL_DIR;
        let installed_python_versions_path = dirs::INSTALLS.join("python");
        let subdirs = file::dir_subdirs(&installed_python_versions_path)?;
        for v in sorted(subdirs) {
            if v.starts_with(".") {
                continue;
            }
            let src = installed_python_versions_path.join(&v);
            if file::is_symlink_or_junction(&src) {
                continue;
            }
            // ~/.local/share/uv/python/cpython-3.10.16-macos-aarch64-none
            // ~/.local/share/uv/python/cpython-3.13.0-linux-x86_64-gnu
            let os = OS;
            let arch = if cfg!(target_arch = "x86_64") {
                "x86_64-gnu"
            } else if cfg!(target_arch = "aarch64") {
                "aarch64-none"
            } else {
                ARCH
            };
            let dst = uv_versions_path.join(format!("cpython-{v}-{os}-{arch}"));
            if !dst.exists() {
                if !uv_versions_path.exists() {
                    file::create_dir_all(uv_versions_path)?;
                }
                // TODO: uv doesn't support symlinked dirs
                // https://github.com/astral-sh/uv/blob/e65a273f1b6b7c3ab129d902e93adeda4da20636/crates/uv-python/src/managed.rs#L196
                file::clone_dir(&src, &dst)?;
                miseprintln!("Synced python@{v} from mise to uv");
            }
        }
        Ok(())
    }
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
    r#"<bold><underline>Examples:</underline></bold>

    $ <bold>pyenv install 3.11.0</bold>
    $ <bold>mise sync python --pyenv</bold>
    $ <bold>mise use -g python@3.11.0</bold> - uses pyenv-provided python
    
    $ <bold>uv python install 3.11.0</bold>
    $ <bold>mise install python@3.10.0</bold>
    $ <bold>mise sync python --uv</bold>
    $ <bold>mise x python@3.11.0 -- python -V</bold> - uses uv-provided python
    $ <bold>uv run -p 3.10.0 -- python -V</bold> - uses mise-provided python
"#
);