Add basic PyEnv and pixi support for python environments (#37156)

cc https://github.com/zed-industries/zed/issues/29807

Release Notes:

- Fixed terminals and tasks not respecting python pyenv and pixi
environments
This commit is contained in:
Lukas Wirth
2025-08-29 10:19:27 +00:00
committed by GitHub
parent 52da72d80a
commit 7403a4ba17
8 changed files with 155 additions and 100 deletions
+7 -1
View File
@@ -14,6 +14,7 @@ use collections::HashMap;
use fs::Fs; use fs::Fs;
use gpui::{AsyncApp, SharedString}; use gpui::{AsyncApp, SharedString};
use settings::WorktreeId; use settings::WorktreeId;
use task::ShellKind;
use crate::{LanguageName, ManifestName}; use crate::{LanguageName, ManifestName};
@@ -68,7 +69,12 @@ pub trait ToolchainLister: Send + Sync {
fn term(&self) -> SharedString; fn term(&self) -> SharedString;
/// Returns the name of the manifest file for this toolchain. /// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName; fn manifest_name(&self) -> ManifestName;
async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option<String>; async fn activation_script(
&self,
toolchain: &Toolchain,
shell: ShellKind,
fs: &dyn Fs,
) -> Vec<String>;
} }
#[async_trait(?Send)] #[async_trait(?Send)]
+58 -13
View File
@@ -34,7 +34,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use task::{TaskTemplate, TaskTemplates, VariableName}; use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
use util::ResultExt; use util::ResultExt;
pub(crate) struct PyprojectTomlManifestProvider; pub(crate) struct PyprojectTomlManifestProvider;
@@ -894,20 +894,65 @@ impl ToolchainLister for PythonToolchainProvider {
fn term(&self) -> SharedString { fn term(&self) -> SharedString {
self.term.clone() self.term.clone()
} }
async fn activation_script(&self, toolchain: &Toolchain, fs: &dyn Fs) -> Option<String> { async fn activation_script(
let toolchain = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>( &self,
toolchain: &Toolchain,
shell: ShellKind,
fs: &dyn Fs,
) -> Vec<String> {
let Ok(toolchain) = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>(
toolchain.as_json.clone(), toolchain.as_json.clone(),
) ) else {
.ok()?; return vec![];
let mut activation_script = None; };
if let Some(prefix) = &toolchain.prefix { let mut activation_script = vec![];
#[cfg(not(target_os = "windows"))]
let path = prefix.join(BINARY_DIR).join("activate"); match toolchain.kind {
#[cfg(target_os = "windows")] Some(PythonEnvironmentKind::Pixi) => {
let path = prefix.join(BINARY_DIR).join("activate.ps1"); let env = toolchain.name.as_deref().unwrap_or("default");
if fs.is_file(&path).await { activation_script.push(format!("pixi shell -e {env}"))
activation_script = Some(format!(". {}", path.display()));
} }
Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
if let Some(prefix) = &toolchain.prefix {
let activate_keyword = match shell {
ShellKind::Cmd => ".",
ShellKind::Nushell => "overlay use",
ShellKind::Powershell => ".",
ShellKind::Fish => "source",
ShellKind::Csh => "source",
ShellKind::Posix => "source",
};
let activate_script_name = match shell {
ShellKind::Posix => "activate",
ShellKind::Csh => "activate.csh",
ShellKind::Fish => "activate.fish",
ShellKind::Nushell => "activate.nu",
ShellKind::Powershell => "activate.ps1",
ShellKind::Cmd => "activate.bat",
};
let path = prefix.join(BINARY_DIR).join(activate_script_name);
if fs.is_file(&path).await {
activation_script.push(format!("{activate_keyword} {}", path.display()));
}
}
}
Some(PythonEnvironmentKind::Pyenv) => {
let Some(manager) = toolchain.manager else {
return vec![];
};
let version = toolchain.version.as_deref().unwrap_or("system");
let pyenv = manager.executable;
let pyenv = pyenv.display();
activation_script.extend(match shell {
ShellKind::Fish => Some(format!("{pyenv} shell - fish {version}")),
ShellKind::Posix => Some(format!("{pyenv} shell - sh {version}")),
ShellKind::Nushell => Some(format!("{pyenv} shell - nu {version}")),
ShellKind::Powershell => None,
ShellKind::Csh => None,
ShellKind::Cmd => None,
})
}
_ => {}
} }
activation_script activation_script
} }
-1
View File
@@ -276,7 +276,6 @@ impl DapStore {
&binary.arguments, &binary.arguments,
&binary.envs, &binary.envs,
binary.cwd.map(|path| path.display().to_string()), binary.cwd.map(|path| path.display().to_string()),
None,
port_forwarding, port_forwarding,
) )
})??; })??;
+3 -3
View File
@@ -40,7 +40,7 @@ use serde_json::json;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::os; use std::os;
use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll}; use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
use task::{ResolvedTask, TaskContext}; use task::{ResolvedTask, ShellKind, TaskContext};
use unindent::Unindent as _; use unindent::Unindent as _;
use util::{ use util::{
TryFutureExt as _, assert_set_eq, maybe, path, TryFutureExt as _, assert_set_eq, maybe, path,
@@ -9222,8 +9222,8 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
fn manifest_name(&self) -> ManifestName { fn manifest_name(&self) -> ManifestName {
SharedString::new_static("pyproject.toml").into() SharedString::new_static("pyproject.toml").into()
} }
async fn activation_script(&self, _: &Toolchain, _: &dyn Fs) -> Option<String> { async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec<String> {
None vec![]
} }
} }
Arc::new( Arc::new(
+69 -56
View File
@@ -1,7 +1,8 @@
use anyhow::Result; use anyhow::Result;
use collections::HashMap; use collections::HashMap;
use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
use itertools::Itertools;
use itertools::Itertools as _;
use language::LanguageName; use language::LanguageName;
use remote::RemoteClient; use remote::RemoteClient;
use settings::{Settings, SettingsLocation}; use settings::{Settings, SettingsLocation};
@@ -11,7 +12,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
}; };
use task::{Shell, ShellBuilder, SpawnInTerminal}; use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
use terminal::{ use terminal::{
TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings, TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::TerminalSettings,
}; };
@@ -131,33 +132,62 @@ impl Project {
cx.spawn(async move |project, cx| { cx.spawn(async move |project, cx| {
let activation_script = maybe!(async { let activation_script = maybe!(async {
let toolchain = toolchain?.await?; let toolchain = toolchain?.await?;
lang_registry Some(
.language_for_name(&toolchain.language_name.0) lang_registry
.await .language_for_name(&toolchain.language_name.0)
.ok()? .await
.toolchain_lister()? .ok()?
.activation_script(&toolchain, fs.as_ref()) .toolchain_lister()?
.await .activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
.await,
)
}) })
.await; .await
.unwrap_or_default();
project.update(cx, move |this, cx| { project.update(cx, move |this, cx| {
let shell = { let shell = {
env.extend(spawn_task.env); env.extend(spawn_task.env);
match remote_client { match remote_client {
Some(remote_client) => create_remote_shell( Some(remote_client) => match activation_script.clone() {
spawn_task activation_script if !activation_script.is_empty() => {
.command let activation_script = activation_script.join("; ");
.as_ref() let to_run = if let Some(command) = spawn_task.command {
.map(|command| (command, &spawn_task.args)), let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
&mut env, let args = spawn_task
path, .args
remote_client, .iter()
activation_script.clone(), .filter_map(|arg| shlex::try_quote(arg).ok());
cx, command.into_iter().chain(args).join(" ")
)?, } else {
format!("exec {shell} -l")
};
let args = vec![
"-c".to_owned(),
format!("{activation_script}; {to_run}",),
];
create_remote_shell(
Some((&shell, &args)),
&mut env,
path,
remote_client,
cx,
)?
}
_ => create_remote_shell(
spawn_task
.command
.as_ref()
.map(|command| (command, &spawn_task.args)),
&mut env,
path,
remote_client,
cx,
)?,
},
None => match activation_script.clone() { None => match activation_script.clone() {
Some(activation_script) => { activation_script if !activation_script.is_empty() => {
let activation_script = activation_script.join("; ");
let to_run = if let Some(command) = spawn_task.command { let to_run = if let Some(command) = spawn_task.command {
let command: Option<Cow<str>> = shlex::try_quote(&command).ok(); let command: Option<Cow<str>> = shlex::try_quote(&command).ok();
let args = spawn_task let args = spawn_task
@@ -169,7 +199,7 @@ impl Project {
format!("exec {shell} -l") format!("exec {shell} -l")
}; };
Shell::WithArguments { Shell::WithArguments {
program: get_default_system_shell(), program: shell,
args: vec![ args: vec![
"-c".to_owned(), "-c".to_owned(),
format!("{activation_script}; {to_run}",), format!("{activation_script}; {to_run}",),
@@ -177,7 +207,7 @@ impl Project {
title_override: None, title_override: None,
} }
} }
None => { _ => {
if let Some(program) = spawn_task.command { if let Some(program) = spawn_task.command {
Shell::WithArguments { Shell::WithArguments {
program, program,
@@ -302,31 +332,21 @@ impl Project {
.await .await
.ok(); .ok();
let lister = language?.toolchain_lister(); let lister = language?.toolchain_lister();
lister?.activation_script(&toolchain, fs.as_ref()).await Some(
lister?
.activation_script(&toolchain, ShellKind::new(&shell), fs.as_ref())
.await,
)
}) })
.await; .await
.unwrap_or_default();
project.update(cx, move |this, cx| { project.update(cx, move |this, cx| {
let shell = { let shell = {
match remote_client { match remote_client {
Some(remote_client) => create_remote_shell( Some(remote_client) => {
None, create_remote_shell(None, &mut env, path, remote_client, cx)?
&mut env, }
path, None => settings.shell,
remote_client,
activation_script.clone(),
cx,
)?,
None => match activation_script.clone() {
Some(activation_script) => Shell::WithArguments {
program: get_default_system_shell(),
args: vec![
"-c".to_owned(),
format!("{activation_script}; exec {shell} -l",),
],
title_override: Some(shell.into()),
},
None => settings.shell,
},
} }
}; };
TerminalBuilder::new( TerminalBuilder::new(
@@ -437,15 +457,10 @@ impl Project {
match remote_client { match remote_client {
Some(remote_client) => { Some(remote_client) => {
let command_template = remote_client.read(cx).build_command( let command_template =
Some(command), remote_client
&args, .read(cx)
&env, .build_command(Some(command), &args, &env, None, None)?;
None,
// todo
None,
None,
)?;
let mut command = std::process::Command::new(command_template.program); let mut command = std::process::Command::new(command_template.program);
command.args(command_template.args); command.args(command_template.args);
command.envs(command_template.env); command.envs(command_template.env);
@@ -473,7 +488,6 @@ fn create_remote_shell(
env: &mut HashMap<String, String>, env: &mut HashMap<String, String>,
working_directory: Option<Arc<Path>>, working_directory: Option<Arc<Path>>,
remote_client: Entity<RemoteClient>, remote_client: Entity<RemoteClient>,
activation_script: Option<String>,
cx: &mut App, cx: &mut App,
) -> Result<Shell> { ) -> Result<Shell> {
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
@@ -493,7 +507,6 @@ fn create_remote_shell(
args.as_slice(), args.as_slice(),
env, env,
working_directory.map(|path| path.display().to_string()), working_directory.map(|path| path.display().to_string()),
activation_script,
None, None,
)?; )?;
*env = command.env; *env = command.env;
+1 -11
View File
@@ -757,7 +757,6 @@ impl RemoteClient {
args: &[String], args: &[String],
env: &HashMap<String, String>, env: &HashMap<String, String>,
working_dir: Option<String>, working_dir: Option<String>,
activation_script: Option<String>,
port_forward: Option<(u16, String, u16)>, port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate> { ) -> Result<CommandTemplate> {
let Some(connection) = self let Some(connection) = self
@@ -767,14 +766,7 @@ impl RemoteClient {
else { else {
return Err(anyhow!("no connection")); return Err(anyhow!("no connection"));
}; };
connection.build_command( connection.build_command(program, args, env, working_dir, port_forward)
program,
args,
env,
working_dir,
activation_script,
port_forward,
)
} }
pub fn upload_directory( pub fn upload_directory(
@@ -1006,7 +998,6 @@ pub(crate) trait RemoteConnection: Send + Sync {
args: &[String], args: &[String],
env: &HashMap<String, String>, env: &HashMap<String, String>,
working_dir: Option<String>, working_dir: Option<String>,
activation_script: Option<String>,
port_forward: Option<(u16, String, u16)>, port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>; ) -> Result<CommandTemplate>;
fn connection_options(&self) -> SshConnectionOptions; fn connection_options(&self) -> SshConnectionOptions;
@@ -1373,7 +1364,6 @@ mod fake {
args: &[String], args: &[String],
env: &HashMap<String, String>, env: &HashMap<String, String>,
_: Option<String>, _: Option<String>,
_: Option<String>,
_: Option<(u16, String, u16)>, _: Option<(u16, String, u16)>,
) -> Result<CommandTemplate> { ) -> Result<CommandTemplate> {
let ssh_program = program.unwrap_or_else(|| "sh".to_string()); let ssh_program = program.unwrap_or_else(|| "sh".to_string());
+2 -10
View File
@@ -30,10 +30,7 @@ use std::{
time::Instant, time::Instant,
}; };
use tempfile::TempDir; use tempfile::TempDir;
use util::{ use util::paths::{PathStyle, RemotePathBuf};
get_default_system_shell,
paths::{PathStyle, RemotePathBuf},
};
pub(crate) struct SshRemoteConnection { pub(crate) struct SshRemoteConnection {
socket: SshSocket, socket: SshSocket,
@@ -116,7 +113,6 @@ impl RemoteConnection for SshRemoteConnection {
input_args: &[String], input_args: &[String],
input_env: &HashMap<String, String>, input_env: &HashMap<String, String>,
working_dir: Option<String>, working_dir: Option<String>,
activation_script: Option<String>,
port_forward: Option<(u16, String, u16)>, port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate> { ) -> Result<CommandTemplate> {
use std::fmt::Write as _; use std::fmt::Write as _;
@@ -138,9 +134,6 @@ impl RemoteConnection for SshRemoteConnection {
} else { } else {
write!(&mut script, "cd; ").unwrap(); write!(&mut script, "cd; ").unwrap();
}; };
if let Some(activation_script) = activation_script {
write!(&mut script, " {activation_script};").unwrap();
}
for (k, v) in input_env.iter() { for (k, v) in input_env.iter() {
if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
@@ -162,8 +155,7 @@ impl RemoteConnection for SshRemoteConnection {
write!(&mut script, "exec {shell} -l").unwrap(); write!(&mut script, "exec {shell} -l").unwrap();
}; };
let sys_shell = get_default_system_shell(); let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&script).unwrap());
let shell_invocation = format!("{sys_shell} -c {}", shlex::try_quote(&script).unwrap());
let mut args = Vec::new(); let mut args = Vec::new();
args.extend(self.socket.ssh_args()); args.extend(self.socket.ssh_args());
+15 -5
View File
@@ -354,7 +354,7 @@ impl TerminalBuilder {
window_id: u64, window_id: u64,
completion_tx: Option<Sender<Option<ExitStatus>>>, completion_tx: Option<Sender<Option<ExitStatus>>>,
cx: &App, cx: &App,
activation_script: Option<String>, activation_script: Vec<String>,
) -> Result<TerminalBuilder> { ) -> Result<TerminalBuilder> {
// If the parent environment doesn't have a locale set // If the parent environment doesn't have a locale set
// (As is the case when launched from a .app on MacOS), // (As is the case when launched from a .app on MacOS),
@@ -493,7 +493,9 @@ impl TerminalBuilder {
let pty_tx = event_loop.channel(); let pty_tx = event_loop.channel();
let _io_thread = event_loop.spawn(); // DANGER let _io_thread = event_loop.spawn(); // DANGER
let terminal = Terminal { let no_task = task.is_none();
let mut terminal = Terminal {
task, task,
pty_tx: Notifier(pty_tx), pty_tx: Notifier(pty_tx),
completion_tx, completion_tx,
@@ -518,7 +520,7 @@ impl TerminalBuilder {
last_hyperlink_search_position: None, last_hyperlink_search_position: None,
#[cfg(windows)] #[cfg(windows)]
shell_program, shell_program,
activation_script, activation_script: activation_script.clone(),
template: CopyTemplate { template: CopyTemplate {
shell, shell,
env, env,
@@ -529,6 +531,14 @@ impl TerminalBuilder {
}, },
}; };
if !activation_script.is_empty() && no_task {
for activation_script in activation_script {
terminal.input(activation_script.into_bytes());
terminal.write_to_pty(b"\n");
}
terminal.clear();
}
Ok(TerminalBuilder { Ok(TerminalBuilder {
terminal, terminal,
events_rx, events_rx,
@@ -712,7 +722,7 @@ pub struct Terminal {
#[cfg(windows)] #[cfg(windows)]
shell_program: Option<String>, shell_program: Option<String>,
template: CopyTemplate, template: CopyTemplate,
activation_script: Option<String>, activation_script: Vec<String>,
} }
struct CopyTemplate { struct CopyTemplate {
@@ -2218,7 +2228,7 @@ mod tests {
0, 0,
Some(completion_tx), Some(completion_tx),
cx, cx,
None, vec![],
) )
.unwrap() .unwrap()
.subscribe(cx) .subscribe(cx)