33
44It does, in order:
55
6- - Downloads the virtualenv package to a temporary directory and add it to sys.path.
7- - Creates a virtual environment in the correct OS data dir which will be
6+ - Creates a virtual environment using venv (or virtualenv zipapp) in the correct OS data dir which will be
87 - `%APPDATA%\\ pypoetry` on Windows
98 - ~/Library/Application Support/pypoetry on MacOS
109 - `${XDG_DATA_HOME}/pypoetry` (or `~/.local/share/pypoetry` if it's not set) on UNIX systems
1110 - In `${POETRY_HOME}` if it's set.
1211 - Installs the latest or given version of Poetry inside this virtual environment.
1312 - Installs a `poetry` script in the Python user directory (or `${POETRY_HOME/bin}` if `POETRY_HOME` is set).
13+ - On failure, the error log is written to poetry-installer-error-*.log and any previously existing environment
14+ is restored.
1415"""
1516
1617import argparse
@@ -219,21 +220,6 @@ def _get_win_folder_with_ctypes(csidl_name):
219220 _get_win_folder = _get_win_folder_from_registry
220221
221222
222- @contextmanager
223- def temporary_directory (* args , ** kwargs ):
224- try :
225- from tempfile import TemporaryDirectory
226- except ImportError :
227- name = tempfile .mkdtemp (* args , ** kwargs )
228-
229- yield name
230-
231- shutil .rmtree (name )
232- else :
233- with TemporaryDirectory (* args , ** kwargs ) as name :
234- yield name
235-
236-
237223PRE_MESSAGE = """# Welcome to {poetry}!
238224
239225This will download and install the latest version of {poetry},
@@ -277,6 +263,83 @@ def temporary_directory(*args, **kwargs):
277263POST_MESSAGE_CONFIGURE_WINDOWS = """"""
278264
279265
266+ class PoetryInstallationError (RuntimeError ):
267+ def __init__ (self , return_code : int = 0 , log : Optional [str ] = None ):
268+ super (PoetryInstallationError , self ).__init__ ()
269+ self .return_code = return_code
270+ self .log = log
271+
272+
273+ class VirtualEnvironment :
274+ def __init__ (self , path : Path ) -> None :
275+ self ._path = path
276+ # str is required for compatibility with subprocess run on CPython <= 3.7 on Windows
277+ self ._python = str (
278+ self ._path .joinpath ("Scripts/python.exe" if WINDOWS else "bin/python" )
279+ )
280+
281+ @property
282+ def path (self ):
283+ return self ._path
284+
285+ @classmethod
286+ def make (cls , target : Path ) -> "VirtualEnvironment" :
287+ try :
288+ import venv
289+
290+ builder = venv .EnvBuilder (clear = True , with_pip = True , symlinks = False )
291+ builder .ensure_directories (target )
292+ builder .create (target )
293+ except ImportError :
294+ # fallback to using virtualenv package if venv is not available, eg: ubuntu
295+ python_version = f"{ sys .version_info .major } .{ sys .version_info .minor } "
296+ virtualenv_bootstrap_url = (
297+ f"https://bootstrap.pypa.io/virtualenv/{ python_version } /virtualenv.pyz"
298+ )
299+
300+ with tempfile .TemporaryDirectory (prefix = "poetry-installer" ) as temp_dir :
301+ virtualenv_pyz = Path (temp_dir ) / "virtualenv.pyz"
302+ request = Request (
303+ virtualenv_bootstrap_url , headers = {"User-Agent" : "Python Poetry" }
304+ )
305+ virtualenv_pyz .write_bytes (urlopen (request ).read ())
306+ cls .run (
307+ sys .executable , virtualenv_pyz , "--clear" , "--always-copy" , target
308+ )
309+
310+ # We add a special file so that Poetry can detect
311+ # its own virtual environment
312+ target .joinpath ("poetry_env" ).touch ()
313+
314+ env = cls (target )
315+
316+ # we do this here to ensure that outdated system default pip does not trigger older bugs
317+ env .pip ("install" , "--disable-pip-version-check" , "--upgrade" , "pip" )
318+
319+ return env
320+
321+ @staticmethod
322+ def run (* args , ** kwargs ) -> subprocess .CompletedProcess :
323+ completed_process = subprocess .run (
324+ args ,
325+ stdout = subprocess .PIPE ,
326+ stderr = subprocess .STDOUT ,
327+ ** kwargs ,
328+ )
329+ if completed_process .returncode != 0 :
330+ raise PoetryInstallationError (
331+ return_code = completed_process .returncode ,
332+ log = completed_process .stdout .decode (),
333+ )
334+ return completed_process
335+
336+ def python (self , * args , ** kwargs ) -> subprocess .CompletedProcess :
337+ return self .run (self ._python , * args , ** kwargs )
338+
339+ def pip (self , * args , ** kwargs ) -> subprocess .CompletedProcess :
340+ return self .python ("-m" , "pip" , "--isolated" , * args , ** kwargs )
341+
342+
280343class Cursor :
281344 def __init__ (self ) -> None :
282345 self ._output = sys .stdout
@@ -439,12 +502,10 @@ def _is_self_upgrade_supported(x):
439502 try :
440503 self .install (version )
441504 except subprocess .CalledProcessError as e :
442- print (
443- colorize ( "error" , f" \n An error has occurred: { e } \n { e . stdout .decode () } " )
505+ raise PoetryInstallationError (
506+ return_code = e . returncode , log = e . output .decode ()
444507 )
445508
446- return e .returncode
447-
448509 self ._write ("" )
449510 self .display_post_message (version )
450511
@@ -460,21 +521,13 @@ def install(self, version, upgrade=False):
460521 )
461522 )
462523
463- env_path = self .make_env (version )
464- self .install_poetry (version , env_path )
465- self .make_bin (version )
524+ with self .make_env (version ) as env :
525+ self .install_poetry (version , env )
526+ self .make_bin (version , env )
527+ self ._data_dir .joinpath ("VERSION" ).write_text (version )
528+ self ._install_comment (version , "Done" )
466529
467- self ._overwrite (
468- "Installing {} ({}): {}" .format (
469- colorize ("info" , "Poetry" ),
470- colorize ("b" , version ),
471- colorize ("success" , "Done" ),
472- )
473- )
474-
475- self ._data_dir .joinpath ("VERSION" ).write_text (version )
476-
477- return 0
530+ return 0
478531
479532 def uninstall (self ) -> int :
480533 if not self ._data_dir .exists ():
@@ -504,81 +557,70 @@ def uninstall(self) -> int:
504557
505558 return 0
506559
507- def make_env (self , version : str ) -> Path :
560+ def _install_comment (self , version : str , message : str ) :
508561 self ._overwrite (
509562 "Installing {} ({}): {}" .format (
510563 colorize ("info" , "Poetry" ),
511564 colorize ("b" , version ),
512- colorize ("comment" , "Creating environment" ),
565+ colorize ("comment" , message ),
513566 )
514567 )
515568
569+ @contextmanager
570+ def make_env (self , version : str ) -> VirtualEnvironment :
516571 env_path = self ._data_dir .joinpath ("venv" )
572+ env_path_saved = env_path .with_suffix (".save" )
517573
518- with temporary_directory () as tmp_dir :
519- subprocess .run (
520- [sys .executable , "-m" , "pip" , "install" , "virtualenv" , "-t" , tmp_dir ],
521- stdout = subprocess .PIPE ,
522- stderr = subprocess .STDOUT ,
523- check = True ,
524- )
525-
526- sys .path .insert (0 , tmp_dir )
527-
528- import virtualenv
574+ if env_path .exists ():
575+ self ._install_comment (version , "Saving existing environment" )
576+ if env_path_saved .exists ():
577+ shutil .rmtree (env_path_saved )
578+ shutil .move (env_path , env_path_saved )
529579
530- virtualenv .cli_run ([str (env_path ), "--clear" ])
531-
532- # We add a special file so that Poetry can detect
533- # its own virtual environment
534- env_path .joinpath ("poetry_env" ).touch ()
580+ try :
581+ self ._install_comment (version , "Creating environment" )
582+ yield VirtualEnvironment .make (env_path )
583+ except Exception as e : # noqa
584+ if env_path .exists ():
585+ self ._install_comment (
586+ version , "An error occurred. Removing partial environment."
587+ )
588+ shutil .rmtree (env_path )
535589
536- return env_path
590+ if env_path_saved .exists ():
591+ self ._install_comment (
592+ version , "Restoring previously saved environment."
593+ )
594+ shutil .move (env_path_saved , env_path )
537595
538- def make_bin (self , version : str ) -> None :
539- self ._overwrite (
540- "Installing {} ({}): {}" .format (
541- colorize ("info" , "Poetry" ),
542- colorize ("b" , version ),
543- colorize ("comment" , "Creating script" ),
544- )
545- )
596+ raise e
597+ else :
598+ if env_path_saved .exists ():
599+ shutil .rmtree (env_path_saved , ignore_errors = True )
546600
601+ def make_bin (self , version : str , env : VirtualEnvironment ) -> None :
602+ self ._install_comment (version , "Creating script" )
547603 self ._bin_dir .mkdir (parents = True , exist_ok = True )
548604
549605 script = "poetry"
550- target_script = "venv/ bin/poetry "
606+ script_bin = "bin"
551607 if WINDOWS :
552608 script = "poetry.exe"
553- target_script = "venv/Scripts/poetry.exe"
609+ script_bin = "Scripts"
610+ target_script = env .path .joinpath (script_bin , script )
554611
555612 if self ._bin_dir .joinpath (script ).exists ():
556613 self ._bin_dir .joinpath (script ).unlink ()
557614
558615 try :
559- self ._bin_dir .joinpath (script ).symlink_to (
560- self ._data_dir .joinpath (target_script )
561- )
616+ self ._bin_dir .joinpath (script ).symlink_to (target_script )
562617 except OSError :
563618 # This can happen if the user
564619 # does not have the correct permission on Windows
565- shutil .copy (
566- self ._data_dir .joinpath (target_script ), self ._bin_dir .joinpath (script )
567- )
620+ shutil .copy (target_script , self ._bin_dir .joinpath (script ))
568621
569- def install_poetry (self , version : str , env_path : Path ) -> None :
570- self ._overwrite (
571- "Installing {} ({}): {}" .format (
572- colorize ("info" , "Poetry" ),
573- colorize ("b" , version ),
574- colorize ("comment" , "Installing Poetry" ),
575- )
576- )
577-
578- if WINDOWS :
579- python = env_path .joinpath ("Scripts/python.exe" )
580- else :
581- python = env_path .joinpath ("bin/python" )
622+ def install_poetry (self , version : str , env : VirtualEnvironment ) -> None :
623+ self ._install_comment (version , "Installing Poetry" )
582624
583625 if self ._git :
584626 specification = "git+" + version
@@ -587,12 +629,7 @@ def install_poetry(self, version: str, env_path: Path) -> None:
587629 else :
588630 specification = f"poetry=={ version } "
589631
590- subprocess .run (
591- [str (python ), "-m" , "pip" , "install" , specification ],
592- stdout = subprocess .PIPE ,
593- stderr = subprocess .STDOUT ,
594- check = True ,
595- )
632+ env .pip ("install" , specification )
596633
597634 def display_pre_message (self ) -> None :
598635 kwargs = {
@@ -831,7 +868,25 @@ def main():
831868 if args .uninstall or string_to_bool (os .getenv ("POETRY_UNINSTALL" , "0" )):
832869 return installer .uninstall ()
833870
834- return installer .run ()
871+ try :
872+ return installer .run ()
873+ except PoetryInstallationError as e :
874+ installer ._write (colorize ("error" , "Poetry installation failed." )) # noqa
875+
876+ if e .log is not None :
877+ import traceback
878+
879+ _ , path = tempfile .mkstemp (
880+ suffix = ".log" ,
881+ prefix = "poetry-installer-error-" ,
882+ dir = str (Path .cwd ()),
883+ text = True ,
884+ )
885+ installer ._write (colorize ("error" , f"See { path } for error logs." )) # noqa
886+ text = f"{ e .log } \n Traceback:\n \n { '' .join (traceback .format_tb (e .__traceback__ ))} "
887+ Path (path ).write_text (text )
888+
889+ return e .return_code
835890
836891
837892if __name__ == "__main__" :
0 commit comments