diff --git a/src/manage/aliasutils.py b/src/manage/aliasutils.py index cdf9d7c..dcf42ab 100644 --- a/src/manage/aliasutils.py +++ b/src/manage/aliasutils.py @@ -85,7 +85,16 @@ def _if_exists(launcher, plat): return launcher -def _create_alias(cmd, *, name, target, plat=None, windowed=0, script_code=None, _link=os.link): +def _create_alias( + cmd, + *, + name, + target, + plat=None, + windowed=0, + script_code=None, + allow_link=True, + _link=os.link): p = cmd.global_dir / name if not p.match("*.exe"): p = p.with_name(p.name + ".exe") @@ -129,12 +138,27 @@ def _create_alias(cmd, *, name, target, plat=None, windowed=0, script_code=None, LOGGER.debug("Failed to read existing alias launcher.") launcher_remap = cmd.scratch.setdefault("aliasutils.create_alias.launcher_remap", {}) - if existing_bytes == launcher_bytes: + if not allow_link or not _link: + # If links are disallowed, always replace the target with a copy. + unlink(p) + try: + p.write_bytes(launcher_bytes) + LOGGER.debug("Created %s as copy of %s", p.name, launcher.name) + launcher_remap[launcher.name] = p + except OSError: + LOGGER.error("Failed to create global command %s.", name) + LOGGER.debug("TRACEBACK", exc_info=True) + elif existing_bytes == launcher_bytes: # Valid existing launcher, so save its path in case we need it later # for a hard link. launcher_remap.setdefault(launcher.name, p) else: - # First try and create a hard link + # Links are allowed and we need to create one, so try to make a link, + # falling back to a link to another existing alias (that we've checked + # already during this run), and then falling back to a copy. + # This handles the case where our links are on a different volume to the + # install (so hard links don't work), but limits us to only a single + # copy (each) of the redirector(s), thus saving space. unlink(p) try: _link(launcher, p) @@ -305,7 +329,7 @@ def calculate_aliases(cmd, install, *, _scan=_scan): yield ai.replace(target=default_alias.target) -def create_aliases(cmd, aliases, *, _create_alias=_create_alias): +def create_aliases(cmd, aliases, *, allow_link=True, _create_alias=_create_alias): if not cmd.global_dir: return @@ -337,6 +361,7 @@ def create_aliases(cmd, aliases, *, _create_alias=_create_alias): target=target, script_code=alias.script_code, windowed=alias.windowed, + allow_link=allow_link, ) except NoLauncherTemplateError: if install_matches_any(alias.install, getattr(cmd, "tags", None)): diff --git a/src/manage/commands.py b/src/manage/commands.py index b0cda96..62b2ab1 100644 --- a/src/manage/commands.py +++ b/src/manage/commands.py @@ -258,6 +258,7 @@ def execute(self): "default_install_tag": (str, None), "preserve_site_on_upgrade": (config_bool, None), "enable_entrypoints": (config_bool, None), + "hard_link_entrypoints": (config_bool, None), }, "first_run": { @@ -823,6 +824,7 @@ class InstallCommand(BaseCommand): default_install_tag = None preserve_site_on_upgrade = True enable_entrypoints = True + hard_link_entrypoints = True def __init__(self, args, root=None): super().__init__(args, root) diff --git a/src/manage/install_command.py b/src/manage/install_command.py index b4ec740..ae3db3b 100644 --- a/src/manage/install_command.py +++ b/src/manage/install_command.py @@ -294,7 +294,7 @@ def update_all_shortcuts(cmd, *, _aliasutils=None): except LookupError: LOGGER.warn("Failed to process aliases for %s.", i.get("display-name", i["id"])) LOGGER.debug("TRACEBACK", exc_info=True) - _aliasutils.create_aliases(cmd, aliases) + _aliasutils.create_aliases(cmd, aliases, allow_link=getattr(cmd, "hard_link_entrypoints", True)) _aliasutils.cleanup_aliases(cmd, preserve=aliases) for i in installs: diff --git a/src/pymanager.json b/src/pymanager.json index 0f6fbb5..e46a592 100644 --- a/src/pymanager.json +++ b/src/pymanager.json @@ -2,7 +2,8 @@ "install": { "source": "%PYTHON_MANAGER_SOURCE_URL%", "fallback_source": "./bundled/fallback-index.json", - "default_install_tag": "3" + "default_install_tag": "3", + "hard_link_entrypoints": false }, "list": { "format": "%PYTHON_MANAGER_LIST_FORMAT%" diff --git a/tests/test_alias.py b/tests/test_alias.py index ce46f9b..672237e 100644 --- a/tests/test_alias.py +++ b/tests/test_alias.py @@ -305,7 +305,7 @@ def test_create_aliases(fake_config, tmp_path): created = [] # Full arguments copied from source to ensure callers only pass valid args - def _on_create(cmd, *, name, target, plat=None, windowed=0, script_code=None): + def _on_create(cmd, *, name, target, plat=None, windowed=0, script_code=None, allow_link=True): created.append((name, windowed, script_code)) aliases = [ diff --git a/tests/test_install_command.py b/tests/test_install_command.py index a90d72b..9c76ae1 100644 --- a/tests/test_install_command.py +++ b/tests/test_install_command.py @@ -199,7 +199,7 @@ class AliasUtils: calculate_aliases = staticmethod(AU.calculate_aliases) @staticmethod - def create_aliases(cmd, aliases): + def create_aliases(cmd, aliases, *, allow_link=True): created.extend(aliases) @staticmethod