11use crate :: * ;
2- use anyhow:: Context as _;
3- use dap:: adapters:: latest_github_release;
2+ use anyhow:: { Context as _, ensure} ;
43use dap:: { DebugRequest , StartDebuggingRequestArguments , adapters:: DebugTaskDefinition } ;
54use gpui:: { AppContext , AsyncApp , SharedString } ;
65use json_dotpath:: DotPaths ;
76use language:: { LanguageName , Toolchain } ;
7+ use paths:: debug_adapters_dir;
88use serde_json:: Value ;
9+ use smol:: lock:: OnceCell ;
910use std:: net:: Ipv4Addr ;
1011use std:: {
1112 collections:: HashMap ,
@@ -17,15 +18,24 @@ use util::ResultExt;
1718
1819#[ derive( Default ) ]
1920pub ( crate ) struct PythonDebugAdapter {
20- checked : OnceLock < ( ) > ,
21+ python_venv_base : OnceCell < Result < Arc < Path > , String > > ,
2122}
2223
2324impl PythonDebugAdapter {
2425 const ADAPTER_NAME : & ' static str = "Debugpy" ;
2526 const DEBUG_ADAPTER_NAME : DebugAdapterName =
2627 DebugAdapterName ( SharedString :: new_static ( Self :: ADAPTER_NAME ) ) ;
27- const ADAPTER_PACKAGE_NAME : & ' static str = "debugpy" ;
28- const ADAPTER_PATH : & ' static str = "src/debugpy/adapter" ;
28+ const PYTHON_ADAPTER_IN_VENV : & ' static str = if cfg ! ( target_os = "windows" ) {
29+ "Scripts/python3"
30+ } else {
31+ "bin/python3"
32+ } ;
33+ const ADAPTER_PATH : & ' static str = if cfg ! ( target_os = "windows" ) {
34+ "debugpy-venv/Scripts/debugpy-adapter"
35+ } else {
36+ "debugpy-venv/bin/debugpy-adapter"
37+ } ;
38+
2939 const LANGUAGE_NAME : & ' static str = "Python" ;
3040
3141 async fn generate_debugpy_arguments (
@@ -46,25 +56,12 @@ impl PythonDebugAdapter {
4656 vec ! [ "-m" . to_string( ) , "debugpy.adapter" . to_string( ) ]
4757 } else {
4858 let adapter_path = paths:: debug_adapters_dir ( ) . join ( Self :: DEBUG_ADAPTER_NAME . as_ref ( ) ) ;
49- let file_name_prefix = format ! ( "{}_" , Self :: ADAPTER_NAME ) ;
50-
51- let debugpy_dir =
52- util:: fs:: find_file_name_in_dir ( adapter_path. as_path ( ) , |file_name| {
53- file_name. starts_with ( & file_name_prefix)
54- } )
55- . await
56- . context ( "Debugpy directory not found" ) ?;
57-
58- log:: debug!(
59- "Using GitHub-downloaded debugpy adapter from: {}" ,
60- debugpy_dir. display( )
61- ) ;
62- vec ! [
63- debugpy_dir
64- . join( Self :: ADAPTER_PATH )
65- . to_string_lossy( )
66- . to_string( ) ,
67- ]
59+ let path = adapter_path
60+ . join ( Self :: ADAPTER_PATH )
61+ . to_string_lossy ( )
62+ . into_owned ( ) ;
63+ log:: debug!( "Using pip debugpy adapter from: {path}" ) ;
64+ vec ! [ path]
6865 } ;
6966
7067 args. extend ( if let Some ( args) = user_args {
@@ -100,44 +97,67 @@ impl PythonDebugAdapter {
10097 request,
10198 } )
10299 }
103- async fn fetch_latest_adapter_version (
104- & self ,
105- delegate : & Arc < dyn DapDelegate > ,
106- ) -> Result < AdapterVersion > {
107- let github_repo = GithubRepo {
108- repo_name : Self :: ADAPTER_PACKAGE_NAME . into ( ) ,
109- repo_owner : "microsoft" . into ( ) ,
110- } ;
111-
112- fetch_latest_adapter_version_from_github ( github_repo, delegate. as_ref ( ) ) . await
113- }
114100
115- async fn install_binary (
116- adapter_name : DebugAdapterName ,
117- version : AdapterVersion ,
118- delegate : Arc < dyn DapDelegate > ,
119- ) -> Result < ( ) > {
120- let version_path = adapters:: download_adapter_from_github (
121- adapter_name,
122- version,
123- adapters:: DownloadedFileType :: GzipTar ,
124- delegate. as_ref ( ) ,
125- )
126- . await ?;
127- // only needed when you install the latest version for the first time
128- if let Some ( debugpy_dir) =
129- util:: fs:: find_file_name_in_dir ( version_path. as_path ( ) , |file_name| {
130- file_name. starts_with ( "microsoft-debugpy-" )
131- } )
101+ async fn ensure_venv ( delegate : & dyn DapDelegate ) -> Result < Arc < Path > > {
102+ let python_path = Self :: find_base_python ( delegate)
132103 . await
133- {
134- // TODO Debugger: Rename folder instead of moving all files to another folder
135- // We're doing unnecessary IO work right now
136- util:: fs:: move_folder_files_to_folder ( debugpy_dir. as_path ( ) , version_path. as_path ( ) )
104+ . context ( "Could not find Python installation for DebugPy" ) ?;
105+ let work_dir = debug_adapters_dir ( ) . join ( Self :: ADAPTER_NAME ) ;
106+ let mut path = work_dir. clone ( ) ;
107+ path. push ( "debugpy-venv" ) ;
108+ if !path. exists ( ) {
109+ util:: command:: new_smol_command ( python_path)
110+ . arg ( "-m" )
111+ . arg ( "venv" )
112+ . arg ( "debugpy-venv" )
113+ . current_dir ( work_dir)
114+ . spawn ( ) ?
115+ . output ( )
137116 . await ?;
138117 }
139118
140- Ok ( ( ) )
119+ Ok ( path. into ( ) )
120+ }
121+
122+ // Find "baseline", user python version from which we'll create our own venv.
123+ async fn find_base_python ( delegate : & dyn DapDelegate ) -> Option < PathBuf > {
124+ for path in [ "python3" , "python" ] {
125+ if let Some ( path) = delegate. which ( path. as_ref ( ) ) . await {
126+ return Some ( path) ;
127+ }
128+ }
129+ None
130+ }
131+
132+ async fn base_venv ( & self , delegate : & dyn DapDelegate ) -> Result < Arc < Path > , String > {
133+ const BINARY_DIR : & str = if cfg ! ( target_os = "windows" ) {
134+ "Scripts"
135+ } else {
136+ "bin"
137+ } ;
138+ self . python_venv_base
139+ . get_or_init ( move || async move {
140+ let venv_base = Self :: ensure_venv ( delegate)
141+ . await
142+ . map_err ( |e| format ! ( "{e}" ) ) ?;
143+ let pip_path = venv_base. join ( BINARY_DIR ) . join ( "pip3" ) ;
144+ let installation_succeeded = util:: command:: new_smol_command ( pip_path. as_path ( ) )
145+ . arg ( "install" )
146+ . arg ( "debugpy" )
147+ . arg ( "-U" )
148+ . output ( )
149+ . await
150+ . map_err ( |e| format ! ( "{e}" ) ) ?
151+ . status
152+ . success ( ) ;
153+ if !installation_succeeded {
154+ return Err ( "debugpy installation failed" . into ( ) ) ;
155+ }
156+
157+ Ok ( venv_base)
158+ } )
159+ . await
160+ . clone ( )
141161 }
142162
143163 async fn get_installed_binary (
@@ -642,18 +662,26 @@ impl DebugAdapter for PythonDebugAdapter {
642662 }
643663 }
644664 }
645-
646- if self . checked . set ( ( ) ) . is_ok ( ) {
647- delegate. output_to_console ( format ! ( "Checking latest version of {}..." , self . name( ) ) ) ;
648- if let Some ( version) = self . fetch_latest_adapter_version ( delegate) . await . log_err ( ) {
649- cx. background_spawn ( Self :: install_binary ( self . name ( ) , version, delegate. clone ( ) ) )
650- . await
651- . context ( "Failed to install debugpy" ) ?;
652- }
653- }
654-
655- self . get_installed_binary ( delegate, & config, None , user_args, toolchain, false )
665+ let toolchain = self
666+ . base_venv ( & * * delegate)
656667 . await
668+ . map_err ( |e| anyhow:: anyhow!( e) ) ?
669+ . join ( Self :: PYTHON_ADAPTER_IN_VENV ) ;
670+
671+ self . get_installed_binary (
672+ delegate,
673+ & config,
674+ None ,
675+ user_args,
676+ Some ( Toolchain {
677+ name : SharedString :: new_static ( "Zed-provided DebugPy" ) ,
678+ path : SharedString :: from ( toolchain. to_string_lossy ( ) . into_owned ( ) ) ,
679+ language_name : LanguageName :: new ( "Python" ) ,
680+ as_json : Value :: Null ,
681+ } ) ,
682+ false ,
683+ )
684+ . await
657685 }
658686
659687 fn label_for_child_session ( & self , args : & StartDebuggingRequestArguments ) -> Option < String > {
@@ -666,24 +694,6 @@ impl DebugAdapter for PythonDebugAdapter {
666694 }
667695}
668696
669- async fn fetch_latest_adapter_version_from_github (
670- github_repo : GithubRepo ,
671- delegate : & dyn DapDelegate ,
672- ) -> Result < AdapterVersion > {
673- let release = latest_github_release (
674- & format ! ( "{}/{}" , github_repo. repo_owner, github_repo. repo_name) ,
675- false ,
676- false ,
677- delegate. http_client ( ) ,
678- )
679- . await ?;
680-
681- Ok ( AdapterVersion {
682- tag_name : release. tag_name ,
683- url : release. tarball_url ,
684- } )
685- }
686-
687697#[ cfg( test) ]
688698mod tests {
689699 use super :: * ;
0 commit comments