11import os
22
3- from .exceptions import FilesInUseError
3+ from .exceptions import FilesInUseError , NoLauncherTemplateError
44from .fsutils import atomic_unlink , ensure_tree , unlink
55from .logging import LOGGER
66from .pathutils import Path
77from .tagutils import install_matches_any
88
9+ _EXE = ".exe" .casefold ()
10+
11+ DEFAULT_SITE_DIRS = ["Lib\\ site-packages" , "Scripts" ]
12+
913SCRIPT_CODE = """import sys
1014
1115# Clear sys.path[0] if it contains this script.
3842sys.exit({func}())
3943"""
4044
45+
46+ class AliasInfo :
47+ def __init__ (self , ** kwargs ):
48+ self .install = kwargs .get ("install" )
49+ self .name = kwargs .get ("name" )
50+ self .windowed = kwargs .get ("windowed" , 0 )
51+ self .target = kwargs .get ("target" )
52+ self .mod = kwargs .get ("mod" )
53+ self .func = kwargs .get ("func" )
54+
55+ def replace (self , ** kwargs ):
56+ return AliasInfo (** {
57+ "install" : self .install ,
58+ "name" : self .name ,
59+ "windowed" : self .windowed ,
60+ "target" : self .target ,
61+ "mod" : self .mod ,
62+ "func" : self .func ,
63+ ** kwargs ,
64+ })
65+
66+ @property
67+ def script_code (self ):
68+ if self .mod and self .func :
69+ return SCRIPT_CODE .format (mod = self .mod , func = self .func )
70+
71+
4172def _if_exists (launcher , plat ):
4273 suffix = "." + launcher .suffix .lstrip ("." )
4374 plat_launcher = launcher .parent / f"{ launcher .stem } { plat } { suffix } "
@@ -46,24 +77,17 @@ def _if_exists(launcher, plat):
4677 return launcher
4778
4879
49- def create_alias (cmd , install , alias , target , aliases_written , * , script_code = None , _link = os .link ):
50- p = cmd .global_dir / alias [ " name" ]
80+ def _create_alias (cmd , * , name , target , plat = None , windowed = 0 , script_code = None , _link = os .link ):
81+ p = cmd .global_dir / name
5182 if not p .match ("*.exe" ):
5283 p = p .with_name (p .name + ".exe" )
5384 if not isinstance (target , Path ):
5485 target = Path (target )
5586 ensure_tree (p )
5687 launcher = cmd .launcher_exe
57- if alias . get ( " windowed" ) :
88+ if windowed :
5889 launcher = cmd .launcherw_exe or launcher
5990
60- n = p .stem .casefold ()
61- if n in aliases_written :
62- # We've already written this alias in this session, so skip it.
63- return
64- aliases_written .add (n )
65-
66- plat = install ["tag" ].rpartition ("-" )[- 1 ]
6791 if plat :
6892 LOGGER .debug ("Checking for launcher for platform -%s" , plat )
6993 launcher = _if_exists (launcher , f"-{ plat } " )
@@ -73,13 +97,9 @@ def create_alias(cmd, install, alias, target, aliases_written, *, script_code=No
7397 if not launcher .is_file ():
7498 LOGGER .debug ("Checking for launcher for -64" )
7599 launcher = _if_exists (launcher , "-64" )
76- LOGGER .debug ("Create %s linking to %s using %s" , alias [ " name" ] , target , launcher )
100+ LOGGER .debug ("Create %s linking to %s using %s" , name , target , launcher )
77101 if not launcher or not launcher .is_file ():
78- if install_matches_any (install , getattr (cmd , "tags" , None )):
79- LOGGER .warn ("Skipping %s alias because the launcher template was not found." , alias ["name" ])
80- else :
81- LOGGER .debug ("Skipping %s alias because the launcher template was not found." , alias ["name" ])
82- return
102+ raise NoLauncherTemplateError ()
83103
84104 try :
85105 launcher_bytes = launcher .read_bytes ()
@@ -131,7 +151,7 @@ def create_alias(cmd, install, alias, target, aliases_written, *, script_code=No
131151 LOGGER .debug ("Created %s as copy of %s" , p .name , launcher .name )
132152 launcher_remap [launcher .name ] = p
133153 except OSError :
134- LOGGER .error ("Failed to create global command %s." , alias [ " name" ] )
154+ LOGGER .error ("Failed to create global command %s." , name )
135155 LOGGER .debug ("TRACEBACK" , exc_info = True )
136156
137157 p_target = p .with_name (p .name + ".__target__" )
@@ -199,12 +219,13 @@ def _readlines(path):
199219 return
200220
201221
202- def _scan_one (root ):
222+ def _scan_one (install , root ):
203223 # Scan d for dist-info directories with entry_points.txt
204224 dist_info = [d for d in root .glob ("*.dist-info" ) if d .is_dir ()]
205- LOGGER .debug ("Found %i dist-info directories in %s" , len (dist_info ), root )
206225 entrypoints = [f for f in [d / "entry_points.txt" for d in dist_info ] if f .is_file ()]
207- LOGGER .debug ("Found %i entry_points.txt files in %s" , len (entrypoints ), root )
226+ if len (entrypoints ):
227+ LOGGER .debug ("Found %i entry_points.txt files in %i dist-info in %s" ,
228+ len (entrypoints ), len (dist_info ), root )
208229
209230 # Filter down to [console_scripts] and [gui_scripts]
210231 for ep in entrypoints :
@@ -219,64 +240,117 @@ def _scan_one(root):
219240 elif alias is not None :
220241 name , mod , func = _parse_entrypoint_line (line )
221242 if name and mod and func :
222- yield (
223- {** alias , "name" : name },
224- SCRIPT_CODE .format (mod = mod , func = func ),
225- )
243+ yield AliasInfo (install = install , name = name ,
244+ mod = mod , func = func , ** alias )
226245
227246
228- def _scan (prefix , dirs ):
247+ def _scan (install , prefix , dirs ):
229248 for dirname in dirs or ():
230249 root = prefix / dirname
231- yield from _scan_one (root )
250+ yield from _scan_one (install , root )
232251
233252
234- def scan_and_create_entrypoints (cmd , install , shortcut , aliases_written , * , _create_alias = create_alias , _scan = _scan ):
235- prefix = install ["prefix" ]
253+ def calculate_aliases (cmd , install , * , _scan = _scan ):
254+ LOGGER . debug ( "Calculating aliases for %s" , install ["id" ])
236255
237- aliases = list (install .get ("alias" , ()))
238- alias_1 = [a for a in aliases if not a .get ("windowed" )]
239- # If no windowed targets, we'll use the non-windowed one
240- alias_2 = [a for a in aliases if a .get ("windowed" )] or alias_1
256+ prefix = install ["prefix" ]
241257
242- targets = [
243- (prefix / alias_1 [0 ]["target" ]) if alias_1 else None ,
244- (prefix / alias_2 [0 ]["target" ]) if alias_2 else None ,
245- ]
258+ default_alias = None
259+ default_alias_w = None
246260
247- if not any (targets ):
248- LOGGER .debug ("No suitable alias found for %s. Skipping entrypoints" ,
249- install ["id" ])
261+ for a in install .get ("alias" , ()):
262+ target = prefix / a ["target" ]
263+ if not target .is_file ():
264+ LOGGER .warn ("Skipping alias '%s' because target '%s' does not exist" ,
265+ a ["name" ], a ["target" ])
266+ continue
267+ ai = AliasInfo (install = install , ** a )
268+ yield ai
269+ if a .get ("windowed" ) and not default_alias_w :
270+ default_alias_w = ai
271+ if not default_alias :
272+ default_alias = ai
273+
274+ if not default_alias_w :
275+ default_alias_w = default_alias
276+
277+ if install .get ("default" ):
278+ if default_alias :
279+ yield default_alias .replace (name = "python" )
280+ if default_alias_w :
281+ yield default_alias_w .replace (name = "pythonw" , windowed = 1 )
282+
283+ site_dirs = DEFAULT_SITE_DIRS
284+ for s in install .get ("shortcuts" , ()):
285+ if s .get ("kind" ) == "site-dirs" :
286+ site_dirs = s .get ("dirs" , ())
287+ break
288+
289+ for ai in _scan (install , prefix , site_dirs ):
290+ if ai .windowed and default_alias_w :
291+ yield ai .replace (target = default_alias_w .target )
292+ elif not ai .windowed and default_alias :
293+ yield ai .replace (target = default_alias .target )
294+
295+
296+ def create_aliases (cmd , aliases , * , _create_alias = _create_alias ):
297+ if not cmd .global_dir :
250298 return
251299
252- for alias , code in _scan (prefix , shortcut .get ("dirs" )):
253- # Copy the launcher template and create a standard __target__ file
254- target = targets [1 if alias .get ("windowed" , 0 ) else 0 ]
255- if not target :
256- LOGGER .debug ("No suitable alias found for %s. Skipping this " +
257- "entrypoint" , alias ["name" ])
300+ written = set ()
301+
302+ LOGGER .debug ("Creating aliases" )
303+
304+ for alias in aliases :
305+ if not alias .name :
306+ LOGGER .debug ("Invalid alias info provided with no name." )
307+ continue
308+
309+ n = alias .name .casefold ().removesuffix (_EXE )
310+ if n in written :
311+ # We've already written this alias, so skip it.
258312 continue
259- _create_alias ( cmd , install , alias , target , aliases_written , script_code = code )
313+ written . add ( n )
260314
315+ if not alias .target :
316+ LOGGER .debug ("No suitable alias found for %s. Skipping" , alias .name )
317+ continue
261318
262- def cleanup_alias (cmd , site_dirs_written , * , _unlink_many = atomic_unlink , _scan = _scan ):
319+ target = alias .install ["prefix" ] / alias .target
320+ try :
321+ _create_alias (
322+ cmd ,
323+ install = alias .install ,
324+ name = alias .name ,
325+ plat = alias .install .get ("tag" , "" ).rpartition ("-" )[2 ],
326+ target = target ,
327+ script_code = alias .script_code ,
328+ windowed = alias .windowed ,
329+ )
330+ except NoLauncherTemplateError :
331+ if install_matches_any (alias .install , getattr (cmd , "tags" , None )):
332+ LOGGER .warn ("Skipping %s alias because "
333+ "the launcher template was not found." , alias .name )
334+ else :
335+ LOGGER .debug ("Skipping %s alias because "
336+ "the launcher template was not found." , alias .name )
337+
338+
339+
340+ def cleanup_aliases (cmd , * , preserve , _unlink_many = atomic_unlink ):
263341 if not cmd .global_dir or not cmd .global_dir .is_dir ():
264342 return
265343
344+ LOGGER .debug ("Cleaning up aliases" )
266345 expected = set ()
267- for i in cmd .get_installs ():
268- expected .update (a .get ("name" , "" ).casefold () for a in i .get ("alias" , ()))
269-
270- if expected :
271- expected .add ("python" .casefold ())
272- expected .add ("pythonw" .casefold ())
273-
274- for i , s in site_dirs_written or ():
275- for alias , code in _scan (i ["prefix" ], s .get ("dirs" )):
276- expected .add (alias .get ("name" , "" ).casefold ())
346+ for alias in preserve :
347+ if alias .name :
348+ n = alias .name .casefold ().removesuffix (_EXE ) + _EXE
349+ expected .add (n )
277350
351+ LOGGER .debug ("Retaining %d aliases" , len (expected ))
278352 for alias in cmd .global_dir .glob ("*.exe" ):
279- if alias .stem . casefold () in expected or alias . name .casefold () in expected :
353+ if alias .name .casefold () in expected :
280354 continue
281355 target = alias .with_name (alias .name + ".__target__" )
282356 script = alias .with_name (alias .name + ".__script__.py" )
0 commit comments