33from .exceptions import FilesInUseError , NoLauncherTemplateError
44from .fsutils import atomic_unlink , ensure_tree , unlink
55from .logging import LOGGER
6- from .pathutils import Path
6+ from .pathutils import Path , relative_to
77from .tagutils import install_matches_any
88
99_EXE = ".exe" .casefold ()
@@ -105,16 +105,19 @@ def _create_alias(
105105 if windowed :
106106 launcher = cmd .launcherw_exe or launcher
107107
108+ chosen_by = "default"
108109 if plat :
109- LOGGER .debug ("Checking for launcher for platform -%s" , plat )
110110 launcher = _if_exists (launcher , f"-{ plat } " )
111+ chosen_by = "platform tag"
111112 if not launcher .is_file ():
112- LOGGER .debug ("Checking for launcher for default platform %s" , cmd .default_platform )
113113 launcher = _if_exists (launcher , cmd .default_platform )
114+ chosen_by = "default platform"
114115 if not launcher .is_file ():
115- LOGGER .debug ("Checking for launcher for -64" )
116116 launcher = _if_exists (launcher , "-64" )
117- LOGGER .debug ("Create %s linking to %s using %s" , name , target , launcher )
117+ chosen_by = "fallback default"
118+ LOGGER .debug ("Create %s for %s using %s, chosen by %s" , name ,
119+ relative_to (target , getattr (cmd , "install_dir" , None )),
120+ launcher , chosen_by )
118121 if not launcher or not launcher .is_file ():
119122 raise NoLauncherTemplateError ()
120123
@@ -128,18 +131,62 @@ def _create_alias(
128131 LOGGER .debug ("Failed to read %s" , launcher , exc_info = True )
129132 return
130133
134+ force = getattr (cmd , "force" , False )
131135 existing_bytes = b''
132- try :
133- with open (p , 'rb' ) as f :
134- existing_bytes = f .read (len (launcher_bytes ) + 1 )
135- except FileNotFoundError :
136- pass
137- except OSError :
138- LOGGER .debug ("Failed to read existing alias launcher." )
136+ if force :
137+ # Only expect InstallCommand to have .force
138+ unlink (p )
139+ else :
140+ try :
141+ with open (p , 'rb' ) as f :
142+ existing_bytes = f .read (len (launcher_bytes ) + 1 )
143+ except FileNotFoundError :
144+ pass
145+ except OSError :
146+ LOGGER .debug ("Failed to read existing alias launcher." )
139147
140148 launcher_remap = cmd .scratch .setdefault ("aliasutils.create_alias.launcher_remap" , {})
141- if not allow_link or not _link :
142- # If links are disallowed, always replace the target with a copy.
149+
150+ if existing_bytes != launcher_bytes and allow_link and _link :
151+ # Try to find an existing launcher we can hard-link
152+ launcher2 = launcher_remap .get (launcher .name )
153+ if (not launcher2 or not launcher2 .is_file ()) and not force :
154+ # None known, so search existing files. Or, user is forcing us, so
155+ # we only want to use an existing launcher if we've cached it this
156+ # session.
157+ try :
158+ LOGGER .debug ("Searching %s for suitable launcher to link" , cmd .global_dir )
159+ for p2 in cmd .global_dir .glob ("*.exe" ):
160+ try :
161+ with open (p2 , 'rb' ) as f :
162+ existing_bytes2 = f .read (len (launcher_bytes ) + 1 )
163+ except OSError :
164+ LOGGER .debug ("Failed to check %s contents" , p2 , exc_info = True )
165+ else :
166+ if existing_bytes2 == launcher_bytes :
167+ launcher2 = p2
168+ break
169+ else :
170+ LOGGER .debug ("No existing launcher available" )
171+ except Exception :
172+ LOGGER .debug ("Failed to find existing launcher" , exc_info = True )
173+
174+ if launcher2 and launcher2 .is_file ():
175+ # We know that the target either doesn't exist or needs replacing
176+ unlink (p )
177+ try :
178+ _link (launcher2 , p )
179+ existing_bytes = launcher_bytes
180+ launcher_remap [launcher .name ] = launcher2
181+ LOGGER .debug ("Created %s as hard link to %s" , p .name , launcher2 .name )
182+ except FileNotFoundError :
183+ raise
184+ except OSError :
185+ LOGGER .debug ("Failed to create hard link to %s" , launcher2 .name )
186+ launcher2 = None
187+
188+ # Recheck - existing_bytes will have been updated if we successfully linked
189+ if existing_bytes != launcher_bytes :
143190 unlink (p )
144191 try :
145192 p .write_bytes (launcher_bytes )
@@ -148,43 +195,6 @@ def _create_alias(
148195 except OSError :
149196 LOGGER .error ("Failed to create global command %s." , name )
150197 LOGGER .debug ("TRACEBACK" , exc_info = True )
151- elif existing_bytes == launcher_bytes :
152- # Valid existing launcher, so save its path in case we need it later
153- # for a hard link.
154- launcher_remap .setdefault (launcher .name , p )
155- else :
156- # Links are allowed and we need to create one, so try to make a link,
157- # falling back to a link to another existing alias (that we've checked
158- # already during this run), and then falling back to a copy.
159- # This handles the case where our links are on a different volume to the
160- # install (so hard links don't work), but limits us to only a single
161- # copy (each) of the redirector(s), thus saving space.
162- unlink (p )
163- try :
164- _link (launcher , p )
165- LOGGER .debug ("Created %s as hard link to %s" , p .name , launcher .name )
166- except OSError as ex :
167- if ex .winerror != 17 :
168- # Report errors other than cross-drive links
169- LOGGER .debug ("Failed to create hard link for command." , exc_info = True )
170- launcher2 = launcher_remap .get (launcher .name )
171- if launcher2 :
172- try :
173- _link (launcher2 , p )
174- LOGGER .debug ("Created %s as hard link to %s" , p .name , launcher2 .name )
175- except FileNotFoundError :
176- raise
177- except OSError :
178- LOGGER .debug ("Failed to create hard link to fallback launcher" )
179- launcher2 = None
180- if not launcher2 :
181- try :
182- p .write_bytes (launcher_bytes )
183- LOGGER .debug ("Created %s as copy of %s" , p .name , launcher .name )
184- launcher_remap [launcher .name ] = p
185- except OSError :
186- LOGGER .error ("Failed to create global command %s." , name )
187- LOGGER .debug ("TRACEBACK" , exc_info = True )
188198
189199 p_target = p .with_name (p .name + ".__target__" )
190200 do_update = True
@@ -252,13 +262,14 @@ def _readlines(path):
252262 return
253263
254264
255- def _scan_one (install , root ):
265+ def _scan_one (cmd , install , root ):
256266 # Scan d for dist-info directories with entry_points.txt
257267 dist_info = [d for d in root .glob ("*.dist-info" ) if d .is_dir ()]
258268 entrypoints = [f for f in [d / "entry_points.txt" for d in dist_info ] if f .is_file ()]
259269 if len (entrypoints ):
260270 LOGGER .debug ("Found %i entry_points.txt files in %i dist-info in %s" ,
261- len (entrypoints ), len (dist_info ), root )
271+ len (entrypoints ), len (dist_info ),
272+ relative_to (root , getattr (cmd , "install_dir" , None )))
262273
263274 # Filter down to [console_scripts] and [gui_scripts]
264275 for ep in entrypoints :
@@ -277,10 +288,10 @@ def _scan_one(install, root):
277288 mod = mod , func = func , ** alias )
278289
279290
280- def _scan (install , prefix , dirs ):
291+ def _scan (cmd , install , prefix , dirs ):
281292 for dirname in dirs or ():
282293 root = prefix / dirname
283- yield from _scan_one (install , root )
294+ yield from _scan_one (cmd , install , root )
284295
285296
286297def calculate_aliases (cmd , install , * , _scan = _scan ):
@@ -322,7 +333,7 @@ def calculate_aliases(cmd, install, *, _scan=_scan):
322333 site_dirs = s .get ("dirs" , ())
323334 break
324335
325- for ai in _scan (install , prefix , site_dirs ):
336+ for ai in _scan (cmd , install , prefix , site_dirs ):
326337 if ai .windowed and default_alias_w :
327338 yield ai .replace (target = default_alias_w .target )
328339 elif not ai .windowed and default_alias :
0 commit comments