Refine command resolution and symlink creation (see ChatGPT conversation: https://chatgpt.com/share/6936be2d-952c-800f-a1cd-7ce5438014ff)
This commit is contained in:
@@ -1,62 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from pkgmgr.get_repo_identifier import get_repo_identifier
|
||||
from pkgmgr.get_repo_dir import get_repo_dir
|
||||
|
||||
def create_ink(repo, repositories_base_dir, bin_dir, all_repos, quiet=False, preview=False):
|
||||
|
||||
def create_ink(repo, repositories_base_dir, bin_dir, all_repos,
|
||||
quiet=False, preview=False):
|
||||
"""
|
||||
Creates a symbolic link for the repository's command.
|
||||
|
||||
Instead of creating an executable wrapper, this function creates a symlink
|
||||
that points to the command file within the repository (e.g., main.sh or main.py).
|
||||
It also ensures that the command file has executable permissions.
|
||||
Create a symlink for the repository's command.
|
||||
|
||||
IMPORTANT:
|
||||
This function is intentionally kept *simple*. All decision logic for
|
||||
choosing the command lives inside resolve_command_for_repo().
|
||||
|
||||
Behavior:
|
||||
- If repo["command"] is defined → create a symlink to it.
|
||||
- If repo["command"] is missing or None → do NOT create a link.
|
||||
"""
|
||||
|
||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||
|
||||
command = repo.get("command")
|
||||
if not command:
|
||||
# Automatically detect main.sh or main.py:
|
||||
main_sh = os.path.join(repo_dir, "main.sh")
|
||||
main_py = os.path.join(repo_dir, "main.py")
|
||||
if os.path.exists(main_sh):
|
||||
command = main_sh
|
||||
elif os.path.exists(main_py):
|
||||
command = main_py
|
||||
else:
|
||||
if not quiet:
|
||||
print(f"No command defined and neither main.sh nor main.py found in {repo_dir}. Skipping link creation.")
|
||||
return
|
||||
|
||||
# Ensure the command file is executable.
|
||||
if not preview:
|
||||
try:
|
||||
os.chmod(command, 0o755)
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print(f"Failed to set executable permissions for {command}: {e}")
|
||||
if not quiet:
|
||||
print(f"No command resolved for '{repo_identifier}'. Skipping link.")
|
||||
return
|
||||
|
||||
link_path = os.path.join(bin_dir, repo_identifier)
|
||||
if preview:
|
||||
print(f"[Preview] Would create symlink '{link_path}' pointing to '{command}'.")
|
||||
else:
|
||||
os.makedirs(bin_dir, exist_ok=True)
|
||||
if os.path.exists(link_path) or os.path.islink(link_path):
|
||||
os.remove(link_path)
|
||||
os.symlink(command, link_path)
|
||||
if not quiet:
|
||||
print(f"Symlink for {repo_identifier} created at {link_path}.")
|
||||
|
||||
alias_name = repo.get("alias")
|
||||
if alias_name:
|
||||
if alias_name == repo_identifier:
|
||||
print(f"Skipped alias link creation. Alias '{alias_name}' and repository identifier '{repo_identifier}' are the same.")
|
||||
else:
|
||||
alias_link_path = os.path.join(bin_dir, alias_name)
|
||||
try:
|
||||
if os.path.exists(alias_link_path) or os.path.islink(alias_link_path):
|
||||
os.remove(alias_link_path)
|
||||
os.symlink(link_path, alias_link_path)
|
||||
if not quiet:
|
||||
print(f"Alias '{alias_name}' has been set to point to {repo_identifier}.")
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print(f"Error creating alias '{alias_name}': {e}")
|
||||
if preview:
|
||||
print(f"[Preview] Would link {link_path} → {command}")
|
||||
return
|
||||
|
||||
# Mark local repo scripts as executable if needed
|
||||
try:
|
||||
if os.path.realpath(command).startswith(os.path.realpath(repo_dir)):
|
||||
os.chmod(command, 0o755)
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print(f"Failed to set permissions on '{command}': {e}")
|
||||
|
||||
# Create bin directory
|
||||
os.makedirs(bin_dir, exist_ok=True)
|
||||
|
||||
# Remove existing
|
||||
if os.path.exists(link_path) or os.path.islink(link_path):
|
||||
os.remove(link_path)
|
||||
|
||||
# Create the link
|
||||
os.symlink(command, link_path)
|
||||
|
||||
if not quiet:
|
||||
print(f"Symlink created: {link_path} → {command}")
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Optional alias support (same as before)
|
||||
# ------------------------------------------------------------
|
||||
alias_name = repo.get("alias")
|
||||
if alias_name:
|
||||
alias_link_path = os.path.join(bin_dir, alias_name)
|
||||
|
||||
if alias_name == repo_identifier:
|
||||
if not quiet:
|
||||
print(f"Alias '{alias_name}' equals identifier. Skipping alias creation.")
|
||||
return
|
||||
|
||||
try:
|
||||
if os.path.exists(alias_link_path) or os.path.islink(alias_link_path):
|
||||
os.remove(alias_link_path)
|
||||
os.symlink(link_path, alias_link_path)
|
||||
if not quiet:
|
||||
print(f"Alias '{alias_name}' created → {repo_identifier}")
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print(f"Error creating alias '{alias_name}': {e}")
|
||||
|
||||
@@ -8,7 +8,8 @@ This module orchestrates the installation of repositories by:
|
||||
|
||||
1. Ensuring the repository directory exists (cloning if necessary).
|
||||
2. Verifying the repository according to the configured policies.
|
||||
3. Creating executable links using create_ink().
|
||||
3. Creating executable links using create_ink(), after resolving the
|
||||
appropriate command via resolve_command_for_repo().
|
||||
4. Running a sequence of modular installer components that handle
|
||||
specific technologies or manifests (PKGBUILD, Nix flakes, Python
|
||||
via pyproject.toml, Makefile, OS-specific package metadata).
|
||||
@@ -25,8 +26,8 @@ from pkgmgr.get_repo_dir import get_repo_dir
|
||||
from pkgmgr.create_ink import create_ink
|
||||
from pkgmgr.verify import verify_repository
|
||||
from pkgmgr.clone_repos import clone_repos
|
||||
|
||||
from pkgmgr.context import RepoContext
|
||||
from pkgmgr.resolve_command import resolve_command_for_repo
|
||||
|
||||
# Installer implementations
|
||||
from pkgmgr.installers.os_packages import (
|
||||
@@ -204,7 +205,24 @@ def install_repos(
|
||||
update_dependencies=update_dependencies,
|
||||
)
|
||||
|
||||
# Create the symlink using create_ink before running installers.
|
||||
# ------------------------------------------------------------
|
||||
# Resolve the command for this repository before creating the link.
|
||||
# If no command is resolved, no link will be created.
|
||||
# ------------------------------------------------------------
|
||||
resolved_command = resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier=identifier,
|
||||
repo_dir=repo_dir,
|
||||
)
|
||||
|
||||
if resolved_command:
|
||||
repo["command"] = resolved_command
|
||||
else:
|
||||
repo.pop("command", None)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Create the symlink using create_ink (if a command is set).
|
||||
# ------------------------------------------------------------
|
||||
create_ink(
|
||||
repo,
|
||||
repositories_base_dir,
|
||||
@@ -239,7 +257,7 @@ def install_repos(
|
||||
continue
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Debug + aussagekräftiger Fehler bei Installer-Fail
|
||||
# Debug output + clear error if an installer fails
|
||||
# ------------------------------------------------------------
|
||||
if not quiet:
|
||||
print(
|
||||
@@ -268,10 +286,9 @@ def install_repos(
|
||||
f" pkgmgr install {identifier} --clone-mode shallow --no-verification"
|
||||
)
|
||||
|
||||
# Re-raise, damit CLI/Test sauber fehlschlägt,
|
||||
# aber nun mit deutlich mehr Kontext.
|
||||
# Re-raise so that CLI/tests fail clearly,
|
||||
# but now with much more context.
|
||||
raise
|
||||
|
||||
# Nur wenn der Installer erfolgreich war, Capabilities mergen
|
||||
# Only merge capabilities if the installer succeeded
|
||||
provided_capabilities.update(caps)
|
||||
|
||||
|
||||
113
pkgmgr/resolve_command.py
Normal file
113
pkgmgr/resolve_command.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Command resolver for repositories.
|
||||
|
||||
This module determines the correct command to expose via symlink.
|
||||
It implements the following priority:
|
||||
|
||||
1. Explicit command in repo config → command
|
||||
2. System package manager binary (/usr/...) → NO LINK (respect OS)
|
||||
3. Nix profile binary (~/.nix-profile/bin/<id>) → command
|
||||
4. Python / non-system console script on PATH → command
|
||||
5. Fallback: repository's main.sh or main.py → command
|
||||
6. If nothing is available → raise error
|
||||
|
||||
The actual symlink creation is handled by create_ink(). This resolver
|
||||
only decides *what* should be used as the entrypoint, or whether no
|
||||
link should be created at all.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def resolve_command_for_repo(repo, repo_identifier: str, repo_dir: str) -> Optional[str]:
|
||||
"""
|
||||
Determine the command for this repository.
|
||||
|
||||
Returns:
|
||||
str → path to the command (a symlink should be created)
|
||||
None → do NOT create a link (e.g. system package already provides it)
|
||||
|
||||
On total failure (no suitable command found at any layer), this function
|
||||
raises SystemExit with a descriptive error message.
|
||||
"""
|
||||
# ------------------------------------------------------------
|
||||
# 1. Explicit command defined by repository config
|
||||
# ------------------------------------------------------------
|
||||
explicit = repo.get("command")
|
||||
if explicit:
|
||||
return explicit
|
||||
|
||||
home = os.path.expanduser("~")
|
||||
|
||||
def is_executable(path: str) -> bool:
|
||||
return os.path.exists(path) and os.access(path, os.X_OK)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 2. System package manager binary via PATH
|
||||
#
|
||||
# If the binary lives under /usr/, we treat it as a system-managed
|
||||
# package (e.g. installed via pacman/apt/yum). In that case, pkgmgr
|
||||
# does NOT create a link at all and defers entirely to the OS.
|
||||
# ------------------------------------------------------------
|
||||
path_candidate = shutil.which(repo_identifier)
|
||||
system_binary: Optional[str] = None
|
||||
non_system_binary: Optional[str] = None
|
||||
|
||||
if path_candidate:
|
||||
if path_candidate.startswith("/usr/"):
|
||||
system_binary = path_candidate
|
||||
else:
|
||||
non_system_binary = path_candidate
|
||||
|
||||
if system_binary:
|
||||
# Respect system package manager: do not create a link.
|
||||
if repo.get("debug", False):
|
||||
print(
|
||||
f"[pkgmgr] System binary for '{repo_identifier}' found at "
|
||||
f"{system_binary}; no symlink will be created."
|
||||
)
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 3. Nix profile binary (~/.nix-profile/bin/<identifier>)
|
||||
# ------------------------------------------------------------
|
||||
nix_candidate = os.path.join(home, ".nix-profile", "bin", repo_identifier)
|
||||
if is_executable(nix_candidate):
|
||||
return nix_candidate
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 4. Python / non-system console script on PATH
|
||||
#
|
||||
# Here we reuse the non-system PATH candidate (e.g. from a venv or
|
||||
# a user-local install like ~/.local/bin). This is treated as a
|
||||
# valid command target.
|
||||
# ------------------------------------------------------------
|
||||
if non_system_binary and is_executable(non_system_binary):
|
||||
return non_system_binary
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 5. Fallback: main.sh / main.py inside the repository
|
||||
# ------------------------------------------------------------
|
||||
main_sh = os.path.join(repo_dir, "main.sh")
|
||||
main_py = os.path.join(repo_dir, "main.py")
|
||||
|
||||
if is_executable(main_sh):
|
||||
return main_sh
|
||||
|
||||
if is_executable(main_py) or os.path.exists(main_py):
|
||||
return main_py
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 6. Nothing found → treat as a hard error
|
||||
# ------------------------------------------------------------
|
||||
raise SystemExit(
|
||||
f"No executable command could be resolved for repository '{repo_identifier}'. "
|
||||
"No explicit 'command' configured, no system-managed binary under /usr/, "
|
||||
"no Nix profile binary, no non-system console script on PATH, and no "
|
||||
"main.sh/main.py found in the repository."
|
||||
)
|
||||
Reference in New Issue
Block a user