11use crate :: * ;
22use anyhow:: Context as _;
3- use dap:: adapters:: latest_github_release;
43use dap:: { DebugRequest , StartDebuggingRequestArguments , adapters:: DebugTaskDefinition } ;
5- use gpui:: { AppContext , AsyncApp , SharedString } ;
4+ use gpui:: { AsyncApp , SharedString } ;
65use json_dotpath:: DotPaths ;
7- use language:: { LanguageName , Toolchain } ;
6+ use language:: LanguageName ;
7+ use paths:: debug_adapters_dir;
88use serde_json:: Value ;
9+ use smol:: lock:: OnceCell ;
910use std:: net:: Ipv4Addr ;
1011use std:: {
1112 collections:: HashMap ,
1213 ffi:: OsStr ,
1314 path:: { Path , PathBuf } ,
14- sync:: OnceLock ,
1515} ;
16- use util:: ResultExt ;
1716
1817#[ derive( Default ) ]
1918pub ( crate ) struct PythonDebugAdapter {
20- checked : OnceLock < ( ) > ,
19+ python_venv_base : OnceCell < Result < Arc < Path > , String > > ,
2120}
2221
2322impl PythonDebugAdapter {
2423 const ADAPTER_NAME : & ' static str = "Debugpy" ;
2524 const DEBUG_ADAPTER_NAME : DebugAdapterName =
2625 DebugAdapterName ( SharedString :: new_static ( Self :: ADAPTER_NAME ) ) ;
27- const ADAPTER_PACKAGE_NAME : & ' static str = "debugpy" ;
28- const ADAPTER_PATH : & ' static str = "src/debugpy/adapter" ;
26+ const PYTHON_ADAPTER_IN_VENV : & ' static str = if cfg ! ( target_os = "windows" ) {
27+ "Scripts/python3"
28+ } else {
29+ "bin/python3"
30+ } ;
31+ const ADAPTER_PATH : & ' static str = if cfg ! ( target_os = "windows" ) {
32+ "debugpy-venv/Scripts/debugpy-adapter"
33+ } else {
34+ "debugpy-venv/bin/debugpy-adapter"
35+ } ;
36+
2937 const LANGUAGE_NAME : & ' static str = "Python" ;
3038
3139 async fn generate_debugpy_arguments (
@@ -46,25 +54,12 @@ impl PythonDebugAdapter {
4654 vec ! [ "-m" . to_string( ) , "debugpy.adapter" . to_string( ) ]
4755 } else {
4856 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- ]
57+ let path = adapter_path
58+ . join ( Self :: ADAPTER_PATH )
59+ . to_string_lossy ( )
60+ . into_owned ( ) ;
61+ log:: debug!( "Using pip debugpy adapter from: {path}" ) ;
62+ vec ! [ path]
6863 } ;
6964
7065 args. extend ( if let Some ( args) = user_args {
@@ -100,44 +95,67 @@ impl PythonDebugAdapter {
10095 request,
10196 } )
10297 }
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- } ;
11198
112- fetch_latest_adapter_version_from_github ( github_repo, delegate. as_ref ( ) ) . await
113- }
114-
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- } )
99+ async fn ensure_venv ( delegate : & dyn DapDelegate ) -> Result < Arc < Path > > {
100+ let python_path = Self :: find_base_python ( delegate)
132101 . 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 ( ) )
102+ . context ( "Could not find Python installation for DebugPy" ) ?;
103+ let work_dir = debug_adapters_dir ( ) . join ( Self :: ADAPTER_NAME ) ;
104+ let mut path = work_dir. clone ( ) ;
105+ path. push ( "debugpy-venv" ) ;
106+ if !path. exists ( ) {
107+ util:: command:: new_smol_command ( python_path)
108+ . arg ( "-m" )
109+ . arg ( "venv" )
110+ . arg ( "debugpy-venv" )
111+ . current_dir ( work_dir)
112+ . spawn ( ) ?
113+ . output ( )
137114 . await ?;
138115 }
139116
140- Ok ( ( ) )
117+ Ok ( path. into ( ) )
118+ }
119+
120+ // Find "baseline", user python version from which we'll create our own venv.
121+ async fn find_base_python ( delegate : & dyn DapDelegate ) -> Option < PathBuf > {
122+ for path in [ "python3" , "python" ] {
123+ if let Some ( path) = delegate. which ( path. as_ref ( ) ) . await {
124+ return Some ( path) ;
125+ }
126+ }
127+ None
128+ }
129+
130+ async fn base_venv ( & self , delegate : & dyn DapDelegate ) -> Result < Arc < Path > , String > {
131+ const BINARY_DIR : & str = if cfg ! ( target_os = "windows" ) {
132+ "Scripts"
133+ } else {
134+ "bin"
135+ } ;
136+ self . python_venv_base
137+ . get_or_init ( move || async move {
138+ let venv_base = Self :: ensure_venv ( delegate)
139+ . await
140+ . map_err ( |e| format ! ( "{e}" ) ) ?;
141+ let pip_path = venv_base. join ( BINARY_DIR ) . join ( "pip3" ) ;
142+ let installation_succeeded = util:: command:: new_smol_command ( pip_path. as_path ( ) )
143+ . arg ( "install" )
144+ . arg ( "debugpy" )
145+ . arg ( "-U" )
146+ . output ( )
147+ . await
148+ . map_err ( |e| format ! ( "{e}" ) ) ?
149+ . status
150+ . success ( ) ;
151+ if !installation_succeeded {
152+ return Err ( "debugpy installation failed" . into ( ) ) ;
153+ }
154+
155+ Ok ( venv_base)
156+ } )
157+ . await
158+ . clone ( )
141159 }
142160
143161 async fn get_installed_binary (
@@ -146,15 +164,15 @@ impl PythonDebugAdapter {
146164 config : & DebugTaskDefinition ,
147165 user_installed_path : Option < PathBuf > ,
148166 user_args : Option < Vec < String > > ,
149- toolchain : Option < Toolchain > ,
167+ python_from_toolchain : Option < String > ,
150168 installed_in_venv : bool ,
151169 ) -> Result < DebugAdapterBinary > {
152170 const BINARY_NAMES : [ & str ; 3 ] = [ "python3" , "python" , "py" ] ;
153171 let tcp_connection = config. tcp_connection . clone ( ) . unwrap_or_default ( ) ;
154172 let ( host, port, timeout) = crate :: configure_tcp_connection ( tcp_connection) . await ?;
155173
156- let python_path = if let Some ( toolchain) = toolchain {
157- Some ( toolchain. path . to_string ( ) )
174+ let python_path = if let Some ( toolchain) = python_from_toolchain {
175+ Some ( toolchain)
158176 } else {
159177 let mut name = None ;
160178
@@ -635,25 +653,28 @@ impl DebugAdapter for PythonDebugAdapter {
635653 & config,
636654 None ,
637655 user_args,
638- Some ( toolchain. clone ( ) ) ,
656+ Some ( toolchain. path . to_string ( ) ) ,
639657 true ,
640658 )
641659 . await ;
642660 }
643661 }
644662 }
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 )
663+ let toolchain = self
664+ . base_venv ( & * * delegate)
656665 . await
666+ . map_err ( |e| anyhow:: anyhow!( e) ) ?
667+ . join ( Self :: PYTHON_ADAPTER_IN_VENV ) ;
668+
669+ self . get_installed_binary (
670+ delegate,
671+ & config,
672+ None ,
673+ user_args,
674+ Some ( toolchain. to_string_lossy ( ) . into_owned ( ) ) ,
675+ false ,
676+ )
677+ . await
657678 }
658679
659680 fn label_for_child_session ( & self , args : & StartDebuggingRequestArguments ) -> Option < String > {
@@ -666,24 +687,6 @@ impl DebugAdapter for PythonDebugAdapter {
666687 }
667688}
668689
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-
687690#[ cfg( test) ]
688691mod tests {
689692 use super :: * ;
0 commit comments