Skip to content

Commit 2e47008

Browse files
authored
Add install.hard_link_entrypoints config option. (#267)
Fixes #266
1 parent f94c696 commit 2e47008

File tree

6 files changed

+58
-8
lines changed

6 files changed

+58
-8
lines changed

src/manage/aliasutils.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,16 @@ def _if_exists(launcher, plat):
8585
return launcher
8686

8787

88-
def _create_alias(cmd, *, name, target, plat=None, windowed=0, script_code=None, _link=os.link):
88+
def _create_alias(
89+
cmd,
90+
*,
91+
name,
92+
target,
93+
plat=None,
94+
windowed=0,
95+
script_code=None,
96+
allow_link=True,
97+
_link=os.link):
8998
p = cmd.global_dir / name
9099
if not p.match("*.exe"):
91100
p = p.with_name(p.name + ".exe")
@@ -129,12 +138,27 @@ def _create_alias(cmd, *, name, target, plat=None, windowed=0, script_code=None,
129138
LOGGER.debug("Failed to read existing alias launcher.")
130139

131140
launcher_remap = cmd.scratch.setdefault("aliasutils.create_alias.launcher_remap", {})
132-
if existing_bytes == launcher_bytes:
141+
if not allow_link or not _link:
142+
# If links are disallowed, always replace the target with a copy.
143+
unlink(p)
144+
try:
145+
p.write_bytes(launcher_bytes)
146+
LOGGER.debug("Created %s as copy of %s", p.name, launcher.name)
147+
launcher_remap[launcher.name] = p
148+
except OSError:
149+
LOGGER.error("Failed to create global command %s.", name)
150+
LOGGER.debug("TRACEBACK", exc_info=True)
151+
elif existing_bytes == launcher_bytes:
133152
# Valid existing launcher, so save its path in case we need it later
134153
# for a hard link.
135154
launcher_remap.setdefault(launcher.name, p)
136155
else:
137-
# First try and create a hard link
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.
138162
unlink(p)
139163
try:
140164
_link(launcher, p)
@@ -305,7 +329,7 @@ def calculate_aliases(cmd, install, *, _scan=_scan):
305329
yield ai.replace(target=default_alias.target)
306330

307331

308-
def create_aliases(cmd, aliases, *, _create_alias=_create_alias):
332+
def create_aliases(cmd, aliases, *, allow_link=True, _create_alias=_create_alias):
309333
if not cmd.global_dir:
310334
return
311335

@@ -337,6 +361,7 @@ def create_aliases(cmd, aliases, *, _create_alias=_create_alias):
337361
target=target,
338362
script_code=alias.script_code,
339363
windowed=alias.windowed,
364+
allow_link=allow_link,
340365
)
341366
except NoLauncherTemplateError:
342367
if install_matches_any(alias.install, getattr(cmd, "tags", None)):

src/manage/commands.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ def execute(self):
258258
"default_install_tag": (str, None),
259259
"preserve_site_on_upgrade": (config_bool, None),
260260
"enable_entrypoints": (config_bool, None),
261+
"hard_link_entrypoints": (config_bool, None),
261262
},
262263

263264
"first_run": {
@@ -823,6 +824,7 @@ class InstallCommand(BaseCommand):
823824
default_install_tag = None
824825
preserve_site_on_upgrade = True
825826
enable_entrypoints = True
827+
hard_link_entrypoints = True
826828

827829
def __init__(self, args, root=None):
828830
super().__init__(args, root)

src/manage/install_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ def update_all_shortcuts(cmd, *, _aliasutils=None):
294294
except LookupError:
295295
LOGGER.warn("Failed to process aliases for %s.", i.get("display-name", i["id"]))
296296
LOGGER.debug("TRACEBACK", exc_info=True)
297-
_aliasutils.create_aliases(cmd, aliases)
297+
_aliasutils.create_aliases(cmd, aliases, allow_link=getattr(cmd, "hard_link_entrypoints", True))
298298
_aliasutils.cleanup_aliases(cmd, preserve=aliases)
299299

300300
for i in installs:

src/pymanager.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"install": {
33
"source": "%PYTHON_MANAGER_SOURCE_URL%",
44
"fallback_source": "./bundled/fallback-index.json",
5-
"default_install_tag": "3"
5+
"default_install_tag": "3",
6+
"hard_link_entrypoints": false
67
},
78
"list": {
89
"format": "%PYTHON_MANAGER_LIST_FORMAT%"

tests/test_alias.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,28 @@ def fake_link(x, y):
224224
)
225225

226226

227+
def test_write_alias_launcher_no_linking(fake_config, assert_log, tmp_path):
228+
fake_config.scratch = {
229+
"aliasutils.create_alias.launcher_remap": {"launcher.txt": tmp_path / "actual_launcher.txt"},
230+
}
231+
fake_config.launcher_exe = tmp_path / "launcher.txt"
232+
fake_config.launcher_exe.write_bytes(b'Arbitrary contents')
233+
(tmp_path / "actual_launcher.txt").write_bytes(b'Arbitrary contents')
234+
fake_config.default_platform = '-32'
235+
fake_config.global_dir = tmp_path / "bin"
236+
AU._create_alias(
237+
fake_config,
238+
name="test.exe",
239+
target=tmp_path / "target.exe",
240+
_link=None
241+
)
242+
assert_log(
243+
"Create %s linking to %s",
244+
("Created %s as copy of %s", ("test.exe", "launcher.txt")),
245+
assert_log.end_of_log(),
246+
)
247+
248+
227249
def test_parse_entrypoint_line():
228250
for line, expect in [
229251
("", (None, None, None)),
@@ -305,7 +327,7 @@ def test_create_aliases(fake_config, tmp_path):
305327

306328
created = []
307329
# Full arguments copied from source to ensure callers only pass valid args
308-
def _on_create(cmd, *, name, target, plat=None, windowed=0, script_code=None):
330+
def _on_create(cmd, *, name, target, plat=None, windowed=0, script_code=None, allow_link=True):
309331
created.append((name, windowed, script_code))
310332

311333
aliases = [

tests/test_install_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ class AliasUtils:
199199
calculate_aliases = staticmethod(AU.calculate_aliases)
200200

201201
@staticmethod
202-
def create_aliases(cmd, aliases):
202+
def create_aliases(cmd, aliases, *, allow_link=True):
203203
created.extend(aliases)
204204

205205
@staticmethod

0 commit comments

Comments
 (0)