diff --git a/.mise.toml b/.mise.toml index 68962cff4c..2a35be4347 100644 --- a/.mise.toml +++ b/.mise.toml @@ -4,6 +4,7 @@ min_version = "2024.1.1" [env] _.file = [".env"] _.path = ["./target/debug"] +_.python.venv = "{{env.HOME}}/.cache/venv" FOO = "bar" FOO_NUM = 1 THIS_PROJECT = "{{config_root}}-{{cwd}}" @@ -13,7 +14,7 @@ THIS_PROJECT = "{{config_root}}-{{cwd}}" "cargo:eza" = "0.17.0" tiny = { version = "1", foo = "bar" } golang = { version = "prefix:1.21", foo = "bar" } -python = { version = "latest", virtualenv = "{{env.HOME}}/.cache/venv" } +python = "latest" ruby = "3.1" [plugins] diff --git a/e2e/test_python b/e2e/test_python index d108f3be81..211a6e165b 100755 --- a/e2e/test_python +++ b/e2e/test_python @@ -7,12 +7,14 @@ export MISE_EXPERIMENTAL=1 export MISE_PYTHON_DEFAULT_PACKAGES_FILE="$ROOT/e2e/.default-python-packages" cat >.e2e.mise.toml <(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, + where + D: Deserializer<'de>, { let s: Option = serde::Deserialize::deserialize(deserializer)?; @@ -397,8 +397,8 @@ where impl<'de> de::Deserialize<'de> for EnvList { fn deserialize(deserializer: D) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct EnvManVisitor; @@ -409,8 +409,8 @@ impl<'de> de::Deserialize<'de> for EnvList { } fn visit_seq(self, mut seq: S) -> std::result::Result - where - S: de::SeqAccess<'de>, + where + S: de::SeqAccess<'de>, { let mut env = vec![]; while let Some(list) = seq.next_element::()? { @@ -419,13 +419,25 @@ impl<'de> de::Deserialize<'de> for EnvList { Ok(EnvList(env)) } fn visit_map(self, mut map: M) -> std::result::Result - where - M: de::MapAccess<'de>, + where + M: de::MapAccess<'de>, { let mut env = vec![]; while let Some(key) = map.next_key::()? { match key.as_str() { "_" | "mise" => { + struct EnvDirectivePythonVenv { + path: PathBuf, + create: bool, + } + + #[derive(Deserialize, Default)] + #[serde(deny_unknown_fields)] + struct EnvDirectivePython { + #[serde(default)] + venv: Option, + } + #[derive(Deserialize)] #[serde(deny_unknown_fields)] struct EnvDirectives { @@ -435,7 +447,62 @@ impl<'de> de::Deserialize<'de> for EnvList { file: Vec, #[serde(default, deserialize_with = "deserialize_arr")] source: Vec, + #[serde(default)] + python: EnvDirectivePython, + } + + impl<'de> de::Deserialize<'de> for EnvDirectivePythonVenv { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EnvDirectivePythonVenvVisitor; + + impl<'de> Visitor<'de> for EnvDirectivePythonVenvVisitor { + type Value = EnvDirectivePythonVenv; + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("python venv directive") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + Ok(EnvDirectivePythonVenv { + path: v.into(), + create: false, + }) + } + + fn visit_map(self, mut map: M) -> Result + where + M: de::MapAccess<'de>, + { + let mut path = None; + let mut create = false; + while let Some(key) = map.next_key::()? { + match key.as_str() { + "path" => { + path = Some(map.next_value()?); + } + "create" => { + create = map.next_value()?; + } + _ => { + return Err(de::Error::unknown_field(&key, &["path", "create"])); + } + } + } + let path = path.ok_or_else(|| de::Error::missing_field("path"))?; + Ok(EnvDirectivePythonVenv { path, create }) + } + } + + const FIELDS: &[&str] = &["path", "create"]; + deserializer.deserialize_struct("PythonVenv", FIELDS, EnvDirectivePythonVenvVisitor) + } } + let directives = map.next_value::()?; // TODO: parse these in the order they're defined somehow for path in directives.path { @@ -447,6 +514,12 @@ impl<'de> de::Deserialize<'de> for EnvList { for source in directives.source { env.push(EnvDirective::Source(source)); } + if let Some(venv) = directives.python.venv { + env.push(EnvDirective::PythonVenv { + path: venv.path, + create: venv.create, + }); + } } _ => { enum Val { @@ -459,8 +532,8 @@ impl<'de> de::Deserialize<'de> for EnvList { fn deserialize( deserializer: D, ) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct ValVisitor; @@ -475,8 +548,8 @@ impl<'de> de::Deserialize<'de> for EnvList { } fn visit_bool(self, v: bool) -> Result - where - E: de::Error, + where + E: de::Error, { match v { true => Err(de::Error::custom( @@ -487,15 +560,15 @@ impl<'de> de::Deserialize<'de> for EnvList { } fn visit_i64(self, v: i64) -> Result - where - E: de::Error, + where + E: de::Error, { Ok(Val::Int(v)) } fn visit_str(self, v: &str) -> Result - where - E: de::Error, + where + E: de::Error, { Ok(Val::Str(v.to_string())) } @@ -528,8 +601,8 @@ impl<'de> de::Deserialize<'de> for EnvList { impl<'de> de::Deserialize<'de> for MiseTomlToolList { fn deserialize(deserializer: D) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct MiseTomlToolListVisitor; @@ -540,8 +613,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlToolList { } fn visit_str(self, v: &str) -> Result - where - E: de::Error, + where + E: de::Error, { let tt: ToolVersionType = v .parse() @@ -553,8 +626,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlToolList { } fn visit_map(self, map: M) -> std::result::Result - where - M: de::MapAccess<'de>, + where + M: de::MapAccess<'de>, { let mut options: BTreeMap = de::Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?; @@ -570,8 +643,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlToolList { } fn visit_seq(self, mut seq: S) -> std::result::Result - where - S: de::SeqAccess<'de>, + where + S: de::SeqAccess<'de>, { let mut tools = vec![]; while let Some(tool) = seq.next_element::()? { @@ -587,8 +660,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlToolList { impl<'de> de::Deserialize<'de> for MiseTomlTool { fn deserialize(deserializer: D) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct MiseTomlToolVisitor; @@ -599,8 +672,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlTool { } fn visit_str(self, v: &str) -> Result - where - E: de::Error, + where + E: de::Error, { let tt: ToolVersionType = v .parse() @@ -612,8 +685,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlTool { } fn visit_map(self, map: M) -> Result - where - M: de::MapAccess<'de>, + where + M: de::MapAccess<'de>, { let mut options: BTreeMap = de::Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))?; @@ -635,8 +708,8 @@ impl<'de> de::Deserialize<'de> for MiseTomlTool { impl<'de> de::Deserialize<'de> for Tasks { fn deserialize(deserializer: D) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct TasksVisitor; @@ -647,14 +720,14 @@ impl<'de> de::Deserialize<'de> for Tasks { } fn visit_map(self, mut map: M) -> std::result::Result - where - M: de::MapAccess<'de>, + where + M: de::MapAccess<'de>, { struct TaskDef(Task); impl<'de> de::Deserialize<'de> for TaskDef { fn deserialize(deserializer: D) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct TaskDefVisitor; impl<'de> Visitor<'de> for TaskDefVisitor { @@ -664,8 +737,8 @@ impl<'de> de::Deserialize<'de> for Tasks { } fn visit_str(self, v: &str) -> Result - where - E: de::Error, + where + E: de::Error, { Ok(TaskDef(Task { run: vec![v.to_string()], @@ -674,8 +747,8 @@ impl<'de> de::Deserialize<'de> for Tasks { } fn visit_seq(self, mut seq: S) -> Result - where - S: de::SeqAccess<'de>, + where + S: de::SeqAccess<'de>, { let mut run = vec![]; while let Some(s) = seq.next_element::()? { @@ -688,8 +761,8 @@ impl<'de> de::Deserialize<'de> for Tasks { } fn visit_map(self, map: M) -> Result - where - M: de::MapAccess<'de>, + where + M: de::MapAccess<'de>, { let t = de::Deserialize::deserialize( de::value::MapAccessDeserializer::new(map), @@ -716,8 +789,8 @@ impl<'de> de::Deserialize<'de> for Tasks { impl<'de> de::Deserialize<'de> for ForgeArg { fn deserialize(deserializer: D) -> std::result::Result - where - D: de::Deserializer<'de>, + where + D: de::Deserializer<'de>, { struct ForgeArgVisitor; @@ -728,8 +801,8 @@ impl<'de> de::Deserialize<'de> for ForgeArg { } fn visit_str(self, v: &str) -> Result - where - E: de::Error, + where + E: de::Error, { v.parse().map_err(de::Error::custom) } @@ -740,8 +813,8 @@ impl<'de> de::Deserialize<'de> for ForgeArg { } fn deserialize_alias<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, + where + D: Deserializer<'de>, { struct AliasMapVisitor; @@ -752,8 +825,8 @@ where } fn visit_map(self, mut map: M) -> std::result::Result - where - M: de::MapAccess<'de>, + where + M: de::MapAccess<'de>, { let mut aliases = AliasMap::new(); while let Some(plugin) = map.next_key::()? { @@ -802,7 +875,7 @@ mod tests { foo="bar" "#}, ) - .unwrap(); + .unwrap(); let cf = MiseToml::from_file(&p).unwrap(); let env = parse_env(file::read_to_string(&p).unwrap()); @@ -931,7 +1004,7 @@ mod tests { 18 = "18.0.0" "#}, ) - .unwrap(); + .unwrap(); let mut cf = MiseToml::from_file(&p).unwrap(); let node = "node".parse().unwrap(); let python = "python".parse().unwrap(); @@ -959,7 +1032,7 @@ mod tests { "3.10" = "3.10.0" "#}, ) - .unwrap(); + .unwrap(); let mut cf = MiseToml::from_file(&p).unwrap(); let node = "node".parse().unwrap(); let python = "python".parse().unwrap(); @@ -984,7 +1057,7 @@ mod tests { node = ["16.0.0", "18.0.0"] "#}, ) - .unwrap(); + .unwrap(); let mut cf = MiseToml::from_file(&p).unwrap(); let node = "node".parse().unwrap(); cf.replace_versions(&node, &["16.0.1".into(), "18.0.1".into()]) @@ -1007,7 +1080,7 @@ mod tests { node = ["16.0.0", "18.0.0"] "#}, ) - .unwrap(); + .unwrap(); let mut cf = MiseToml::from_file(&p).unwrap(); cf.remove_plugin(&"node".parse().unwrap()).unwrap(); @@ -1023,7 +1096,7 @@ mod tests { let _ = toml::from_str::(&formatdoc! {r#" invalid_key = true "#}) - .unwrap_err(); + .unwrap_err(); } #[test] diff --git a/src/config/env_directive.rs b/src/config/env_directive.rs index 98aa70dc8d..acbaf2573e 100644 --- a/src/config/env_directive.rs +++ b/src/config/env_directive.rs @@ -1,15 +1,19 @@ -use crate::config::config_file::trust_check; -use crate::config::Settings; -use crate::env_diff::{EnvDiff, EnvDiffOperation}; -use crate::file::display_path; -use crate::tera::{get_tera, BASE_CONTEXT}; -use crate::{dirs, env}; -use eyre::Context; -use indexmap::IndexMap; use std::collections::{BTreeSet, HashMap}; use std::fmt::Display; use std::path::{Path, PathBuf}; +use eyre::Context; +use indexmap::IndexMap; + +use crate::{dirs, env}; +use crate::cmd::CmdLineRunner; +use crate::config::{Config, Settings}; +use crate::config::config_file::trust_check; +use crate::env_diff::{EnvDiff, EnvDiffOperation}; +use crate::file::display_path; +use crate::tera::{BASE_CONTEXT, get_tera}; +use crate::toolset::ToolsetBuilder; + #[derive(Debug, Clone)] pub enum EnvDirective { /// simple key/value pair @@ -22,6 +26,10 @@ pub enum EnvDirective { Path(PathBuf), /// run a bash script and apply the resulting env diff Source(PathBuf), + PythonVenv { + path: PathBuf, + create: bool, + }, } impl From<(String, String)> for EnvDirective { @@ -44,6 +52,13 @@ impl Display for EnvDirective { EnvDirective::File(path) => write!(f, "dotenv {}", display_path(path)), EnvDirective::Path(path) => write!(f, "path_add {}", display_path(path)), EnvDirective::Source(path) => write!(f, "source {}", display_path(path)), + EnvDirective::PythonVenv { path, create } => { + write!(f, "python venv path={}", display_path(path))?; + if *create { + write!(f, " create")?; + } + Ok(()) + } } } } @@ -143,6 +158,41 @@ impl EnvResults { } } } + EnvDirective::PythonVenv { path, create } => { + trace!("python venv: {} create={create}", display_path(&path)); + trust_check(&source)?; + let venv = + r.parse_template(&ctx, &source, path.to_string_lossy().as_ref())?; + let venv = normalize_path(venv.into()); + if !venv.exists() { + if create { + info!("creating venv at: {}", display_path(&venv)); + // TODO: the toolset stuff doesn't feel like it's in the right place here + let config = Config::get(); + let ts = ToolsetBuilder::new().build(&config)?; + let path = ts.list_paths().into_iter().chain(env::split_paths(&env_vars["PATH"])) + .collect::>(); + CmdLineRunner::new("python3") + .args(["-m", "venv", &venv.to_string_lossy()]) + .envs(&env_vars) + .env("PATH", env::join_paths(&path)?.to_string_lossy().to_string()).execute()?; + } else { + warn!( + "no venv found at: {p}\n\n\ + To create a virtualenv manually, run:\n\ + python -m venv {p}", + p = display_path(&venv) + ); + } + } + if venv.exists() { + r.env_paths.push(venv.join("bin")); + env.insert( + "VIRTUAL_ENV".into(), + (venv.to_string_lossy().to_string(), Some(source.clone())), + ); + } + } }; } for (k, (v, source)) in env { @@ -173,9 +223,10 @@ impl EnvResults { #[cfg(test)] mod tests { - use super::*; use crate::test::replace_path; + use super::*; + #[test] fn test_env_path() { let mut env = HashMap::new(); @@ -202,7 +253,7 @@ mod tests { ), ], ) - .unwrap(); + .unwrap(); assert_debug_snapshot!( results.env_paths.into_iter().map(|p| replace_path(&p.display().to_string())).collect::>(), @r###" diff --git a/src/config/settings.rs b/src/config/settings.rs index 791ef98cda..f9309812cb 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -102,8 +102,6 @@ pub struct Settings { pub python_precompiled_os: Option, #[config(env = "MISE_PYENV_REPO", default = "https://github.com/pyenv/pyenv.git")] pub python_pyenv_repo: String, - #[config(env = "MISE_PYTHON_VENV_AUTO_CREATE", default = false)] - pub python_venv_auto_create: bool, #[config(env = "MISE_RAW", default = false)] pub raw: bool, #[config(env = "MISE_SHORTHANDS_FILE")] @@ -135,6 +133,8 @@ pub struct Settings { pub trace: bool, #[config(env = "MISE_LOG_LEVEL", default = "info")] pub log_level: String, + #[config(env = "MISE_PYTHON_VENV_AUTO_CREATE", default = false)] + pub python_venv_auto_create: bool, } #[derive(Config, Default, Debug, Clone, Serialize)] @@ -143,8 +143,8 @@ pub struct Settings { pub struct SettingsStatus { /// warn if a tool is missing #[config( - env = "MISE_STATUS_MESSAGE_MISSING_TOOLS", - default = "if_other_versions_installed" + env = "MISE_STATUS_MESSAGE_MISSING_TOOLS", + default = "if_other_versions_installed" )] pub missing_tools: SettingsStatusMissingTools, /// show env var keys when entering directories @@ -350,7 +350,7 @@ impl Settings { pub fn hidden_configs() -> &'static HashSet<&'static str> { static HIDDEN_CONFIGS: Lazy> = - Lazy::new(|| ["ci", "cd", "debug", "env_file", "trace", "log_level"].into()); + Lazy::new(|| ["ci", "cd", "debug", "env_file", "trace", "log_level", "python_venv_auto_create"].into()); &HIDDEN_CONFIGS } @@ -366,7 +366,7 @@ impl Settings { Ok(()) } - pub fn trusted_config_paths(&self) -> impl Iterator + '_ { + pub fn trusted_config_paths(&self) -> impl Iterator + '_ { self.trusted_config_paths.iter().map(file::replace_path) } diff --git a/src/plugins/core/python.rs b/src/plugins/core/python.rs index 7bdc2e52c8..2f623a7ded 100644 --- a/src/plugins/core/python.rs +++ b/src/plugins/core/python.rs @@ -249,8 +249,6 @@ impl PythonPlugin { } else { warn!( "no venv found at: {p}\n\n\ - To have mise automatically create virtualenvs, run:\n\ - mise settings set python_venv_auto_create true\n\n\ To create a virtualenv manually, run:\n\ python -m venv {p}", p = display_path(&virtualenv) diff --git a/tests/cli/install.rs b/tests/cli/install.rs index 0e59397abd..8f7d0f53c1 100644 --- a/tests/cli/install.rs +++ b/tests/cli/install.rs @@ -4,10 +4,10 @@ use predicates::prelude::*; use test_case::test_case; // From e2e/test_bun, e2e/tes_deno, e2e/test_java -#[test_case("bun", ".bun-version", "1.0.17", &["bun", "-v"], "1.0.17", false)] -#[test_case("deno", ".deno-version", "1.35.3", &["deno", "-V"], "1.35.3", false)] -#[test_case("java", ".sdkmanrc", "java=17.0.2", &["java", "-version"], "openjdk version \"17.0.2\"", true)] -#[test_case("java", ".java-version", "17.0.2", &["java", "-version"], "openjdk version \"17.0.2\"", true)] +#[test_case("bun", ".bun-version", "1.0.17", & ["bun", "-v"], "1.0.17", false)] +#[test_case("deno", ".deno-version", "1.35.3", & ["deno", "-V"], "1.35.3", false)] +#[test_case("java", ".sdkmanrc", "java=17.0.2", & ["java", "-version"], "openjdk version \"17.0.2\"", true)] +#[test_case("java", ".java-version", "17.0.2", & ["java", "-version"], "openjdk version \"17.0.2\"", true)] fn test_tool_specific_version_files( tool: &str, file_name: &str, @@ -196,12 +196,12 @@ fn test_python() -> Result<()> { fn python_config_fixture() -> File { File { path: ".mise.toml".into(), - content: toml::toml!{ - [settings] - python_venv_auto_create = true + content: toml::toml! { + [env._.python] + venv = {path="{{env.MISE_DATA_DIR}}/venv", create=true} [tools] - python = {version = "{{exec(command='echo 3.12.0')}}", virtualenv="{{env.MISE_DATA_DIR}}/venv"} + python = "{{exec(command='echo 3.12.0')}}" }.to_string(), } }