use crate::Result;
use crate::cli::args::BackendArg;
use crate::cmd::CmdLineRunner;
use crate::config::config_file::trust_check;
use crate::config::env_directive::EnvResults;
use crate::config::{Config, Settings};
use crate::env_diff::EnvMap;
use crate::file::{display_path, which_no_shims};
use crate::lock_file::LockFile;
use crate::toolset::Toolset;
use crate::{backend, plugins};
use indexmap::IndexMap;
use std::collections::{HashMap, HashSet};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Clone, Debug)]
pub struct Venv {
pub venv_path: PathBuf,
pub env: HashMap<String, String>,
}
pub(crate) fn load_venv(
venv_root: &Path,
extra_env: impl IntoIterator<Item = (String, String)>,
) -> Venv {
#[cfg(windows)]
let venv_bin_dir = "Scripts";
#[cfg(not(windows))]
let venv_bin_dir = "bin";
let mut env = HashMap::new();
env.extend(extra_env);
env.insert(
"VIRTUAL_ENV".to_string(),
venv_root.to_string_lossy().to_string(),
);
Venv {
venv_path: venv_root.join(venv_bin_dir),
env,
}
}
fn build_uv_venv_command<'a>(
uv_bin: PathBuf,
venv: &'a Path,
python_path: Option<&'a str>,
python: Option<&'a str>,
uv_create_args: Option<Vec<String>>,
) -> CmdLineRunner<'a> {
info!("creating venv with uv at: {}", display_path(venv));
let extra = uv_create_args
.or(Settings::get().python.uv_venv_create_args.clone())
.unwrap_or_default();
let mut cmd = CmdLineRunner::new(uv_bin).args(["venv", &venv.to_string_lossy()]);
cmd = match (python_path, python) {
(Some(python_path), _) => cmd.args(["--python", python_path]),
(_, Some(python)) => cmd.args(["--python", python]),
_ => cmd,
};
cmd.args(extra)
}
fn build_stdlib_venv_command<'a>(
venv: &'a Path,
python_path: Option<&'a str>,
python: Option<&'a str>,
python_create_args: Option<Vec<String>>,
) -> CmdLineRunner<'a> {
info!("creating venv with stdlib at: {}", display_path(venv));
let extra = python_create_args
.or(Settings::get().python.venv_create_args.clone())
.unwrap_or_default();
let bin = match (python_path, python) {
(Some(python_path), _) => python_path.to_string(),
(_, Some(python)) => format!("python{python}"),
_ => "python3".to_string(),
};
CmdLineRunner::new(bin)
.args(["-m", "venv", &venv.to_string_lossy()])
.args(extra)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn create_python_venv(
config: &Arc<Config>,
ts: &Toolset,
venv: &Path,
env_vars: EnvMap,
python: Option<&str>,
uv_create_args: Option<Vec<String>>,
python_create_args: Option<Vec<String>>,
require_uv: bool,
) -> Result<bool> {
let ba = BackendArg::from("python");
let tv = ts.versions.get(&ba).and_then(|tv| {
if let Some(v) = python {
tv.versions.iter().find(|t| t.version.starts_with(v))
} else {
tv.versions.first()
}
});
let python_path = tv.map(|tv| {
plugins::core::python::python_path(tv)
.to_string_lossy()
.to_string()
});
let installed = if let Some(tv) = tv {
let backend = backend::get(&ba).unwrap();
backend.is_version_installed(config, tv, false)
} else {
true
};
if !installed {
warn_once!(
"no venv found at: {p}\n\n\
mise will automatically create the venv once all requested python versions are installed.\n\
To install the missing python versions and create the venv, please run:\n\
`mise install`",
p = display_path(venv)
);
return Ok(false);
}
let uv_bin = ts
.which_bin(config, "uv")
.await
.or_else(|| which_no_shims("uv"));
if require_uv && uv_bin.is_none() {
warn_once!(
"uv is required to create the venv at {p} but is not installed",
p = display_path(venv)
);
return Ok(false);
}
let use_uv = require_uv || (!Settings::get().python.venv_stdlib && uv_bin.is_some());
let cmd = if use_uv {
build_uv_venv_command(
uv_bin.unwrap(),
venv,
python_path.as_deref(),
python,
uv_create_args,
)
} else {
build_stdlib_venv_command(venv, python_path.as_deref(), python, python_create_args)
}
.envs(env_vars);
cmd.execute()?;
crate::prepare::mark_output_stale(venv.to_path_buf());
Ok(true)
}
impl EnvResults {
#[allow(clippy::too_many_arguments)]
pub async fn venv(
config: &Arc<Config>,
ctx: &mut tera::Context,
tera: &mut tera::Tera,
env: &mut IndexMap<String, (String, Option<PathBuf>)>,
r: &mut EnvResults,
normalize_path: fn(&Path, PathBuf) -> PathBuf,
source: &Path,
config_root: &Path,
env_vars: EnvMap,
path: String,
create: bool,
python: Option<String>,
uv_create_args: Option<Vec<String>>,
python_create_args: Option<Vec<String>>,
) -> Result<()> {
trace!("python venv: {} create={create}", display_path(&path));
trust_check(source)?;
let venv = r.parse_template(ctx, tera, source, &path)?;
let venv = normalize_path(config_root, venv.into());
let venv_lock = LockFile::new(&venv).lock()?;
if !venv.exists() && create {
let trs = config.get_tool_request_set().await?;
let mut filter = HashSet::new();
filter.insert("python".to_string());
filter.insert("uv".to_string());
let filtered_trs = trs.filter_by_tool(filter);
let mut ts: Toolset = filtered_trs.into();
let _ = ts.resolve(config).await;
create_python_venv(
config,
&ts,
&venv,
env_vars,
python.as_deref(),
uv_create_args,
python_create_args,
false,
)
.await?;
}
drop(venv_lock);
if venv.exists() {
let Venv {
venv_path,
env: venv_env,
} = load_venv(&venv, HashMap::new());
r.env_paths.insert(0, venv_path);
for (k, v) in venv_env {
env.insert(k, (v, Some(source.to_path_buf())));
}
} else if !create {
warn_once!(
"no venv found at: {p}
To create a virtualenv manually, run:
python -m venv {p}",
p = display_path(&venv)
);
}
Ok(())
}
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::*;
use crate::config::env_directive::{
EnvDirective, EnvDirectiveOptions, EnvResolveOptions, ToolsFilter,
};
use crate::tera::BASE_CONTEXT;
use crate::test::replace_path;
use insta::assert_debug_snapshot;
#[tokio::test]
async fn test_venv_path() {
let env = EnvMap::new();
let config = Config::get().await.unwrap();
let results = EnvResults::resolve(
&config,
BASE_CONTEXT.clone(),
&env,
vec![
(
EnvDirective::PythonVenv {
path: "/".into(),
create: false,
python: None,
uv_create_args: None,
python_create_args: None,
options: EnvDirectiveOptions {
tools: true,
redact: Some(false),
required: crate::config::env_directive::RequiredValue::False,
},
},
Default::default(),
),
(
EnvDirective::PythonVenv {
path: "./".into(),
create: false,
python: None,
uv_create_args: None,
python_create_args: None,
options: EnvDirectiveOptions {
tools: true,
redact: Some(false),
required: crate::config::env_directive::RequiredValue::False,
},
},
Default::default(),
),
],
EnvResolveOptions {
vars: false,
tools: ToolsFilter::ToolsOnly,
warn_on_missing_required: false,
},
)
.await
.unwrap();
assert_debug_snapshot!(
results.env_paths.into_iter().map(|p| replace_path(&p.display().to_string())).collect::<Vec<_>>(),
@r#"
[
"~/bin",
]
"#
);
}
}