Skip to content

Commit 8b83213

Browse files
committed
[WIP] Updated, implemented, and existing tests pass
1 parent fc64f28 commit 8b83213

File tree

6 files changed

+580
-398
lines changed

6 files changed

+580
-398
lines changed

src/manage/aliasutils.py

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import os
2+
3+
from .fsutils import ensure_tree, unlink
4+
from .logging import LOGGER
5+
from .pathutils import Path
6+
from .tagutils import install_matches_any
7+
8+
9+
def _if_exists(launcher, plat):
10+
suffix = "." + launcher.suffix.lstrip(".")
11+
plat_launcher = launcher.parent / f"{launcher.stem}{plat}{suffix}"
12+
if plat_launcher.is_file():
13+
return plat_launcher
14+
return launcher
15+
16+
17+
def create_alias(cmd, install, alias, target, *, script_code=None, _link=os.link):
18+
p = (cmd.global_dir / alias["name"])
19+
target = Path(target)
20+
ensure_tree(p)
21+
launcher = cmd.launcher_exe
22+
if alias.get("windowed"):
23+
launcher = cmd.launcherw_exe or launcher
24+
25+
plat = install["tag"].rpartition("-")[-1]
26+
if plat:
27+
LOGGER.debug("Checking for launcher for platform -%s", plat)
28+
launcher = _if_exists(launcher, f"-{plat}")
29+
if not launcher.is_file():
30+
LOGGER.debug("Checking for launcher for default platform %s", cmd.default_platform)
31+
launcher = _if_exists(launcher, cmd.default_platform)
32+
if not launcher.is_file():
33+
LOGGER.debug("Checking for launcher for -64")
34+
launcher = _if_exists(launcher, "-64")
35+
LOGGER.debug("Create %s linking to %s using %s", alias["name"], target, launcher)
36+
if not launcher or not launcher.is_file():
37+
if install_matches_any(install, getattr(cmd, "tags", None)):
38+
LOGGER.warn("Skipping %s alias because the launcher template was not found.", alias["name"])
39+
else:
40+
LOGGER.debug("Skipping %s alias because the launcher template was not found.", alias["name"])
41+
return
42+
43+
try:
44+
launcher_bytes = launcher.read_bytes()
45+
except OSError:
46+
warnings_shown = cmd.scratch.setdefault("aliasutils.create_alias.warnings_shown", set())
47+
if str(launcher) not in warnings_shown:
48+
LOGGER.warn("Failed to read launcher template at %s.", launcher)
49+
warnings_shown.add(str(launcher))
50+
LOGGER.debug("Failed to read %s", launcher, exc_info=True)
51+
return
52+
53+
existing_bytes = b''
54+
try:
55+
with open(p, 'rb') as f:
56+
existing_bytes = f.read(len(launcher_bytes) + 1)
57+
except FileNotFoundError:
58+
pass
59+
except OSError:
60+
LOGGER.debug("Failed to read existing alias launcher.")
61+
62+
launcher_remap = cmd.scratch.setdefault("aliasutils.create_alias.launcher_remap", {})
63+
64+
if existing_bytes == launcher_bytes:
65+
# Valid existing launcher, so save its path in case we need it later
66+
# for a hard link.
67+
launcher_remap.setdefault(launcher.name, p)
68+
else:
69+
# First try and create a hard link
70+
unlink(p)
71+
try:
72+
_link(launcher, p)
73+
LOGGER.debug("Created %s as hard link to %s", p.name, launcher.name)
74+
except OSError as ex:
75+
if ex.winerror != 17:
76+
# Report errors other than cross-drive links
77+
LOGGER.debug("Failed to create hard link for command.", exc_info=True)
78+
launcher2 = launcher_remap.get(launcher.name)
79+
if launcher2:
80+
try:
81+
_link(launcher2, p)
82+
LOGGER.debug("Created %s as hard link to %s", p.name, launcher2.name)
83+
except FileNotFoundError:
84+
raise
85+
except OSError:
86+
LOGGER.debug("Failed to create hard link to fallback launcher")
87+
launcher2 = None
88+
if not launcher2:
89+
try:
90+
p.write_bytes(launcher_bytes)
91+
LOGGER.debug("Created %s as copy of %s", p.name, launcher.name)
92+
launcher_remap[launcher.name] = p
93+
except OSError:
94+
LOGGER.error("Failed to create global command %s.", alias["name"])
95+
LOGGER.debug(exc_info=True)
96+
97+
p_target = p.with_name(p.name + ".__target__")
98+
do_update = True
99+
try:
100+
do_update = not target.match(p_target.read_text(encoding="utf-8"))
101+
except FileNotFoundError:
102+
pass
103+
except (OSError, UnicodeDecodeError):
104+
LOGGER.debug("Failed to read existing target path.", exc_info=True)
105+
106+
if do_update:
107+
p_target.write_text(str(target), encoding="utf-8")
108+
109+
p_script = p.with_name(p.name + "-script.py")
110+
if script_code:
111+
do_update = True
112+
try:
113+
do_update = p_script.read_text(encoding="utf-8") == script_code
114+
except FileNotFoundError:
115+
pass
116+
except (OSError, UnicodeDecodeError):
117+
LOGGER.debug("Failed to read existing script file.", exc_info=True)
118+
if do_update:
119+
p_script.write_text(script_code, encoding="utf-8")
120+
else:
121+
try:
122+
unlink(p_script)
123+
except OSError:
124+
LOGGER.error("Failed to clean up existing alias. Re-run with -v "
125+
"or check the install log for details.")
126+
LOGGER.info("Failed to remove %s.", p_script, exc_info=True)
127+
128+
129+
def _parse_entrypoint_line(line):
130+
name, sep, rest = line.partition("=")
131+
name = name.strip()
132+
if name and sep and rest:
133+
mod, sep, rest = rest.partition(":")
134+
mod = mod.strip()
135+
if mod and sep and rest:
136+
func, sep, extra = rest.partition("[")
137+
func = func.strip()
138+
if func:
139+
return name, mod, func
140+
return None, None, None
141+
142+
143+
def _scan(prefix, dirs):
144+
for dirname in dirs or ():
145+
root = prefix / dirname
146+
147+
# Scan d for dist-info directories with entry_points.txt
148+
dist_info = [d for d in root.listdir() if d.match("*.dist-info") and d.is_dir()]
149+
LOGGER.debug("Found %i dist-info directories in %s", len(dist_info), root)
150+
entrypoints = [f for f in [d / "entry_points.txt" for d in dist_info] if f.is_file()]
151+
LOGGER.debug("Found %i entry_points.txt files in %s", len(entrypoints), root)
152+
153+
# Filter down to [console_scripts] and [gui_scripts]
154+
for ep in entrypoints:
155+
try:
156+
f = open(ep, "r", encoding="utf-8", errors="strict")
157+
except OSError:
158+
LOGGER.debug("Failed to read %s", ep, exc_info=True)
159+
continue
160+
161+
with f:
162+
alias = None
163+
for line in f:
164+
if line.strip() == "[console_scripts]":
165+
alias = dict(windowed=0)
166+
elif line.strip() == "[gui_scripts]":
167+
alias = dict(windowed=1)
168+
elif line.lstrip().startswith("["):
169+
alias = None
170+
elif alias is not None:
171+
name, mod, func = _parse_entrypoint_line(line)
172+
if name and mod and func:
173+
yield (
174+
{**alias, "name": name},
175+
f"import sys; from {mod} import {func}; sys.exit({func}())",
176+
)
177+
178+
179+
def scan_and_create_entrypoints(cmd, install, shortcut, _create_alias=create_alias):
180+
prefix = install["prefix"]
181+
known = cmd.scratch.setdefault("entrypointutils.known", set())
182+
183+
aliases = list(install.get("alias", ()))
184+
alias_1 = [a for a in aliases if not a.get("windowed")]
185+
alias_2 = [a for a in aliases if a.get("windowed")]
186+
187+
# If no windowed targets, we'll use the non-windowed one
188+
targets = [prefix / a["target"] for a in [*alias_1[:1], *alias_2[:1], *alias_1[:1]]]
189+
if len(targets) < 2:
190+
LOGGER.debug("No suitable alias found for %s. Skipping entrypoints",
191+
install["id"])
192+
return
193+
194+
for alias, code in _scan(prefix, shortcut.get("dirs")):
195+
# Only create names once per install command
196+
n = alias["name"].casefold()
197+
if n in known:
198+
continue
199+
known.add(n)
200+
201+
# Copy the launcher template and create a standard __target__ file
202+
_create_alias(cmd, install, alias, targets[alias.get("windowed", 0)],
203+
script_code=code)
204+
205+
206+
def cleanup_entrypoints(cmd, install_shortcut_pairs):
207+
seen_names = set()
208+
for install, shortcut in install_shortcut_pairs:
209+
for alias, code in _scan(install["prefix"], shortcut.get("dirs")):
210+
seen_names.add(alias["name"].casefold())
211+
212+
# Scan existing aliases
213+
scripts = cmd.global_dir.glob("*-script.py")
214+
215+
# Excluding any in seen_names, delete unused aliases
216+
for script in scripts:
217+
name = script.name.rpartition("-")[0]
218+
if name.casefold() in seen_names:
219+
continue
220+
221+
alias = cmd.global_dir / (name + ".exe")
222+
if not alias.is_file():
223+
continue
224+
225+
try:
226+
unlink(alias)
227+
LOGGER.debug("Deleted %s", alias)
228+
except OSError:
229+
LOGGER.warn("Failed to delete %s", alias)
230+
try:
231+
unlink(script)
232+
LOGGER.debug("Deleted %s", script)
233+
except OSError:
234+
LOGGER.warn("Failed to delete %s", script)

src/manage/entrypointutils.py

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)