mise 2026.4.11

The front-end to your dev env
use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
    sync::Arc,
};

use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
use crate::http::{HTTP, HTTP_FETCH};
use crate::install_context::InstallContext;
use crate::plugins::VERSION_REGEX;
use crate::toolset::{ToolVersion, Toolset};
use crate::ui::progress_report::SingleReport;
use crate::{backend::Backend, backend::VersionInfo, config::Config};
use crate::{env, file, plugins};
use async_trait::async_trait;
use eyre::Result;
use itertools::Itertools;
use versions::Versioning;
use xx::regex;

#[derive(Debug)]
pub struct ElixirPlugin {
    ba: Arc<BackendArg>,
}

impl ElixirPlugin {
    pub fn new() -> Self {
        Self {
            ba: Arc::new(plugins::core::new_backend_arg("elixir")),
        }
    }

    fn elixir_bin(&self, tv: &ToolVersion) -> PathBuf {
        tv.install_path().join("bin").join(elixir_bin_name())
    }

    async fn test_elixir(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
        ctx.pr.set_message("elixir --version".into());
        CmdLineRunner::new(self.elixir_bin(tv))
            .with_pr(ctx.pr.as_ref())
            .envs(self.dependency_env(&ctx.config).await?)
            .arg("--version")
            .execute()
    }

    async fn download(&self, tv: &ToolVersion, pr: &dyn SingleReport) -> Result<PathBuf> {
        let version = &tv.version;
        let version = if regex!(r"^[0-9]").is_match(version) {
            &format!("v{version}")
        } else {
            version
        };
        let url = format!("https://builds.hex.pm/builds/elixir/{version}.zip");

        let filename = url.split('/').next_back().unwrap();
        let tarball_path = tv.download_path().join(filename);

        pr.set_message(format!("download {filename}"));
        if !tarball_path.exists() {
            HTTP.download_file(&url, &tarball_path, Some(pr)).await?;
        }

        Ok(tarball_path)
    }

    async fn install(
        &self,
        ctx: &InstallContext,
        tv: &ToolVersion,
        tarball_path: &Path,
    ) -> Result<()> {
        let filename = tarball_path.file_name().unwrap().to_string_lossy();
        ctx.pr.set_message(format!("extract {filename}"));
        file::remove_all(tv.install_path())?;
        file::unzip(tarball_path, &tv.install_path(), &Default::default())?;

        Ok(())
    }

    async fn verify(&self, ctx: &InstallContext, tv: &ToolVersion) -> Result<()> {
        self.test_elixir(ctx, tv).await
    }
}

#[async_trait]
impl Backend for ElixirPlugin {
    fn ba(&self) -> &Arc<BackendArg> {
        &self.ba
    }

    async fn _list_remote_versions(&self, _config: &Arc<Config>) -> Result<Vec<VersionInfo>> {
        // Format: "version hash timestamp checksum"
        // Example: "v1.17.3 abc123 2024-12-01T00:00:00Z def456"
        let versions: Vec<VersionInfo> = HTTP_FETCH
            .get_text("https://builds.hex.pm/builds/elixir/builds.txt")
            .await?
            .lines()
            .unique()
            .filter_map(|s| {
                let parts: Vec<&str> = s.split_whitespace().collect();
                if parts.len() >= 3 {
                    let version = parts[0].trim_start_matches('v');
                    let timestamp = parts[2]; // Third field is the timestamp
                    Some((version.to_string(), timestamp.to_string()))
                } else {
                    None
                }
            })
            .filter(|(v, _)| regex!(r"^[0-9]+\.[0-9]+\.[0-9]").is_match(v))
            .sorted_by_cached_key(|(s, _)| {
                (
                    Versioning::new(s.split_once('-').map(|(v, _)| v).unwrap_or(s)),
                    !VERSION_REGEX.is_match(s),
                    s.contains("-otp-"),
                    Versioning::new(s),
                    s.to_string(),
                )
            })
            .map(|(version, created_at)| VersionInfo {
                version,
                created_at: Some(created_at),
                ..Default::default()
            })
            .collect();
        Ok(versions)
    }

    async fn _idiomatic_filenames(&self) -> eyre::Result<Vec<String>> {
        Ok(vec![".exenv-version".into()])
    }

    fn get_dependencies(&self) -> Result<Vec<&str>> {
        Ok(vec!["erlang"])
    }

    async fn install_version_(
        &self,
        ctx: &InstallContext,
        mut tv: ToolVersion,
    ) -> Result<ToolVersion> {
        let tarball_path = self.download(&tv, ctx.pr.as_ref()).await?;
        ctx.pr.next_operation();
        self.verify_checksum(ctx, &mut tv, &tarball_path)?;
        ctx.pr.next_operation();
        self.install(ctx, &tv, &tarball_path).await?;
        self.verify(ctx, &tv).await?;
        Ok(tv)
    }

    async fn list_bin_paths(
        &self,
        _config: &Arc<Config>,
        tv: &ToolVersion,
    ) -> Result<Vec<PathBuf>> {
        Ok(["bin", ".mix/escripts"]
            .iter()
            .map(|p| tv.install_path().join(p))
            .collect())
    }

    async fn exec_env(
        &self,
        config: &Arc<Config>,
        _ts: &Toolset,
        tv: &ToolVersion,
    ) -> eyre::Result<BTreeMap<String, String>> {
        let mut map = BTreeMap::new();
        let mut set = |k: &str, v: PathBuf| {
            map.insert(k.to_string(), v.to_string_lossy().to_string());
        };
        let config_env = config.env().await?;
        if !env::PRISTINE_ENV.contains_key("MIX_HOME") && !config_env.contains_key("MIX_HOME") {
            set("MIX_HOME", tv.install_path().join(".mix"));
        }
        if !env::PRISTINE_ENV.contains_key("MIX_ARCHIVES")
            && !config_env.contains_key("MIX_ARCHIVES")
        {
            set(
                "MIX_ARCHIVES",
                tv.install_path().join(".mix").join("archives"),
            );
        }
        Ok(map)
    }
}

fn elixir_bin_name() -> &'static str {
    if cfg!(windows) {
        "elixir.bat"
    } else {
        "elixir"
    }
}