diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 63e9b4b787230ea877cdc92e1fdcdd6daa86dc0c..7b0fc0356a130d8980c62755e94a1fdaf2336cb0 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1001,6 +1001,15 @@ impl HeadlessProject { "failed to spawn kernel process (command: {})", envelope.payload.command ))? + } else if let Some(venv_python) = working_directory + .as_ref() + .and_then(|wd| find_venv_python(wd)) + { + let path_str = venv_python.to_string_lossy().to_string(); + spawn_kernel(&path_str, &[]).context(format!( + "failed to spawn kernel process (venv: {})", + path_str + ))? } else { spawn_kernel("python3", &[]) .or_else(|_| spawn_kernel("python", &[])) @@ -1325,3 +1334,23 @@ fn prompt_to_proto( ), } } + +fn find_venv_python(working_directory: &str) -> Option { + let wd = std::path::Path::new(working_directory); + for dir_name in &[".venv", "venv", ".env", "env"] { + let venv_dir = wd.join(dir_name); + let has_pyvenv_cfg = venv_dir.join("pyvenv.cfg").is_file(); + let has_activate = venv_dir.join("bin").join("activate").is_file(); + if has_pyvenv_cfg || has_activate { + let python = venv_dir.join("bin").join("python"); + if python.is_file() { + return Some(python); + } + let python3 = venv_dir.join("bin").join("python3"); + if python3.is_file() { + return Some(python3); + } + } + } + None +} diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs index 9f08876cd39f4b7441d8c97bd1d5344b944b09ff..737893b09e44b0f7e1b258b4445a42621248434a 100644 --- a/crates/repl/src/kernels/mod.rs +++ b/crates/repl/src/kernels/mod.rs @@ -31,6 +31,62 @@ use runtimelib::{ use ui::{Icon, IconName, SharedString}; use util::rel_path::RelPath; +pub(crate) const VENV_DIR_NAMES: &[&str] = &[".venv", "venv", ".env", "env"]; + +// Build a POSIX shell script that attempts to find and exec the best Python binary to run with the given arguments. +pub(crate) fn build_python_exec_shell_script( + python_args: &str, + cd_command: &str, + env_command: &str, +) -> String { + let venv_dirs = VENV_DIR_NAMES.join(" "); + format!( + "set -e; \ + {cd_command}\ + {env_command}\ + for venv_dir in {venv_dirs}; do \ + if [ -f \"$venv_dir/pyvenv.cfg\" ] || [ -f \"$venv_dir/bin/activate\" ]; then \ + if [ -x \"$venv_dir/bin/python\" ]; then \ + exec \"$venv_dir/bin/python\" {python_args}; \ + elif [ -x \"$venv_dir/bin/python3\" ]; then \ + exec \"$venv_dir/bin/python3\" {python_args}; \ + fi; \ + fi; \ + done; \ + if command -v python3 >/dev/null 2>&1; then \ + exec python3 {python_args}; \ + elif command -v python >/dev/null 2>&1; then \ + exec python {python_args}; \ + else \ + echo 'Error: Python not found in virtual environment or PATH' >&2; \ + exit 127; \ + fi" + ) +} + +/// Build a POSIX shell script that outputs the best Python binary. +#[cfg(target_os = "windows")] +pub(crate) fn build_python_discovery_shell_script() -> String { + let venv_dirs = VENV_DIR_NAMES.join(" "); + format!( + "for venv_dir in {venv_dirs}; do \ + if [ -f \"$venv_dir/pyvenv.cfg\" ] || [ -f \"$venv_dir/bin/activate\" ]; then \ + if [ -x \"$venv_dir/bin/python\" ]; then \ + echo \"$venv_dir/bin/python\"; exit 0; \ + elif [ -x \"$venv_dir/bin/python3\" ]; then \ + echo \"$venv_dir/bin/python3\"; exit 0; \ + fi; \ + fi; \ + done; \ + if command -v python3 >/dev/null 2>&1; then \ + echo python3; exit 0; \ + elif command -v python >/dev/null 2>&1; then \ + echo python; exit 0; \ + fi; \ + exit 1" + ) +} + pub fn start_kernel_tasks( session: Entity, iopub_socket: ClientIoPubConnection, @@ -542,49 +598,47 @@ pub fn python_env_kernel_specifications( }; if let (Some(distro), Some(internal_path)) = (distro, internal_path) { - let python_path = format!("{}/.venv/bin/python", internal_path); - let check = util::command::new_command("wsl") - .args(&["-d", distro, "test", "-f", &python_path]) + let discovery_script = build_python_discovery_shell_script(); + let script = format!( + "cd {} && {}", + shlex::try_quote(&internal_path) + .unwrap_or(std::borrow::Cow::Borrowed(&internal_path)), + discovery_script + ); + let output = util::command::new_command("wsl") + .arg("-d") + .arg(distro) + .arg("bash") + .arg("-l") + .arg("-c") + .arg(&script) .output() .await; - if check.is_ok() && check.unwrap().status.success() { - let default_kernelspec = JupyterKernelspec { - argv: vec![ - python_path.clone(), - "-m".to_string(), - "ipykernel_launcher".to_string(), - "-f".to_string(), - "{connection_file}".to_string(), - ], - display_name: format!("WSL: {} (.venv)", distro), - language: "python".to_string(), - interrupt_mode: None, - metadata: None, - env: None, - }; - - kernel_specs.push(KernelSpecification::WslRemote(WslKernelSpecification { - name: format!("WSL: {} (.venv)", distro), - kernelspec: default_kernelspec, - distro: distro.to_string(), - })); - } else { - let check_system = util::command::new_command("wsl") - .args(&["-d", distro, "command", "-v", "python3"]) - .output() - .await; + if let Ok(output) = output { + if output.status.success() { + let python_cmd = + String::from_utf8_lossy(&output.stdout).trim().to_string(); + let (python_path, display_suffix) = if python_cmd.contains('/') { + let venv_name = python_cmd.split('/').next().unwrap_or("venv"); + ( + format!("{}/{}", internal_path, python_cmd), + format!("({})", venv_name), + ) + } else { + (python_cmd, "(System)".to_string()) + }; - if check_system.is_ok() && check_system.unwrap().status.success() { + let display_name = format!("WSL: {} {}", distro, display_suffix); let default_kernelspec = JupyterKernelspec { argv: vec![ - "python3".to_string(), + python_path, "-m".to_string(), "ipykernel_launcher".to_string(), "-f".to_string(), "{connection_file}".to_string(), ], - display_name: format!("WSL: {} (System)", distro), + display_name: display_name.clone(), language: "python".to_string(), interrupt_mode: None, metadata: None, @@ -593,7 +647,7 @@ pub fn python_env_kernel_specifications( kernel_specs.push(KernelSpecification::WslRemote( WslKernelSpecification { - name: format!("WSL: {} (System)", distro), + name: display_name, kernelspec: default_kernelspec, distro: distro.to_string(), }, diff --git a/crates/repl/src/kernels/wsl_kernel.rs b/crates/repl/src/kernels/wsl_kernel.rs index be76d7ddccb7f199a368b76a1f21bf65fe6f2902..b4f6d356bf2f0176e1645b31f822d5b8fb44465f 100644 --- a/crates/repl/src/kernels/wsl_kernel.rs +++ b/crates/repl/src/kernels/wsl_kernel.rs @@ -1,5 +1,6 @@ use super::{ - KernelSession, KernelSpecification, RunningKernel, WslKernelSpecification, start_kernel_tasks, + KernelSession, KernelSpecification, RunningKernel, WslKernelSpecification, + build_python_exec_shell_script, start_kernel_tasks, }; use anyhow::{Context as _, Result}; use futures::{ @@ -228,8 +229,6 @@ impl WslRunningKernel { kernel_args.extend(resolved_argv.iter().cloned()); let shell_command = if needs_python_resolution { - // 1. Check for .venv/bin/python or .venv/bin/python3 in working directory - // 2. Fall back to system python3 or python let rest_args: Vec = resolved_argv.iter().skip(1).cloned().collect(); let arg_string = quote_posix_shell_arguments(&rest_args)?; let set_env_command = if env_assignments.is_empty() { @@ -245,34 +244,8 @@ impl WslRunningKernel { } else { String::new() }; - // TODO: find a better way to debug missing python issues in WSL - - format!( - "set -e; \ - {} \ - {} \ - echo \"Working directory: $(pwd)\" >&2; \ - if [ -x .venv/bin/python ]; then \ - echo \"Found .venv/bin/python\" >&2; \ - exec .venv/bin/python {}; \ - elif [ -x .venv/bin/python3 ]; then \ - echo \"Found .venv/bin/python3\" >&2; \ - exec .venv/bin/python3 {}; \ - elif command -v python3 >/dev/null 2>&1; then \ - echo \"Found system python3\" >&2; \ - exec python3 {}; \ - elif command -v python >/dev/null 2>&1; then \ - echo \"Found system python\" >&2; \ - exec python {}; \ - else \ - echo 'Error: Python not found in .venv or PATH' >&2; \ - echo 'Contents of current directory:' >&2; \ - ls -la >&2; \ - echo 'PATH:' \"$PATH\" >&2; \ - exit 127; \ - fi", - cd_command, set_env_command, arg_string, arg_string, arg_string, arg_string - ) + + build_python_exec_shell_script(&arg_string, &cd_command, &set_env_command) } else { let args_string = quote_posix_shell_arguments(&resolved_argv)?;