Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b2c657bfa | ||
|
|
e335ab05a1 | ||
|
|
75f963d6e2 |
@@ -1,3 +1,8 @@
|
||||
## [0.7.12] - 2025-12-09
|
||||
|
||||
* Fixed self refering alias during setup
|
||||
|
||||
|
||||
## [0.7.11] - 2025-12-09
|
||||
|
||||
* test: fix installer unit tests for OS packages and Nix dev shell
|
||||
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||
|
||||
pkgname=package-manager
|
||||
pkgver=0.7.11
|
||||
pkgver=0.7.12
|
||||
pkgrel=1
|
||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||
arch=('any')
|
||||
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
||||
package-manager (0.7.12-1) unstable; urgency=medium
|
||||
|
||||
* Fixed self refering alias during setup
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 23:36:35 +0100
|
||||
|
||||
package-manager (0.7.11-1) unstable; urgency=medium
|
||||
|
||||
* test: fix installer unit tests for OS packages and Nix dev shell
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "0.7.11";
|
||||
version = "0.7.12";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: package-manager
|
||||
Version: 0.7.11
|
||||
Version: 0.7.12
|
||||
Release: 1%{?dist}
|
||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
|
||||
@@ -77,6 +77,9 @@ echo ">>> package-manager removed. Nix itself was not removed."
|
||||
/usr/lib/package-manager/
|
||||
|
||||
%changelog
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.12-1
|
||||
- Fixed self refering alias during setup
|
||||
|
||||
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.11-1
|
||||
- test: fix installer unit tests for OS packages and Nix dev shell
|
||||
|
||||
|
||||
@@ -6,8 +6,14 @@ from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
from pkgmgr.core.repository.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: bool = False,
|
||||
preview: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Create a symlink for the repository's command.
|
||||
|
||||
@@ -18,6 +24,11 @@ def create_ink(repo, repositories_base_dir, bin_dir, all_repos,
|
||||
Behavior:
|
||||
- If repo["command"] is defined → create a symlink to it.
|
||||
- If repo["command"] is missing or None → do NOT create a link.
|
||||
|
||||
Safety:
|
||||
- If the resolved command path is identical to the final link target,
|
||||
we skip symlink creation to avoid self-referential symlinks that
|
||||
would break shell resolution ("too many levels of symbolic links").
|
||||
"""
|
||||
|
||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||
@@ -31,6 +42,27 @@ def create_ink(repo, repositories_base_dir, bin_dir, all_repos,
|
||||
|
||||
link_path = os.path.join(bin_dir, repo_identifier)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Safety guard: avoid self-referential symlinks
|
||||
#
|
||||
# Example of a broken situation we must avoid:
|
||||
# - command = ~/.local/bin/package-manager
|
||||
# - link_path = ~/.local/bin/package-manager
|
||||
# - create_ink() removes the real binary and creates a symlink
|
||||
# pointing to itself → zsh: too many levels of symbolic links
|
||||
#
|
||||
# If the resolved command already lives exactly at the target path,
|
||||
# we treat it as "already installed" and skip any modification.
|
||||
# ------------------------------------------------------------------
|
||||
if os.path.abspath(command) == os.path.abspath(link_path):
|
||||
if not quiet:
|
||||
print(
|
||||
f"[pkgmgr] Command for '{repo_identifier}' already lives at "
|
||||
f"'{link_path}'. Skipping symlink creation to avoid a "
|
||||
"self-referential link."
|
||||
)
|
||||
return
|
||||
|
||||
if preview:
|
||||
print(f"[Preview] Would link {link_path} → {command}")
|
||||
return
|
||||
@@ -65,7 +97,10 @@ def create_ink(repo, repositories_base_dir, bin_dir, all_repos,
|
||||
|
||||
if alias_name == repo_identifier:
|
||||
if not quiet:
|
||||
print(f"Alias '{alias_name}' equals identifier. Skipping alias creation.")
|
||||
print(
|
||||
f"Alias '{alias_name}' equals identifier. "
|
||||
"Skipping alias creation."
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "package-manager"
|
||||
version = "0.7.11"
|
||||
version = "0.7.12"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
"""
|
||||
Integration test: install all configured repositories using
|
||||
--clone-mode shallow (HTTPS shallow clone) and --no-verification.
|
||||
|
||||
This test is intended to be run inside the Docker container where:
|
||||
- network access is available,
|
||||
- the config/config.yaml is present,
|
||||
- and it is safe to perform real git operations.
|
||||
|
||||
It passes if the command completes without raising an exception.
|
||||
"""
|
||||
|
||||
import runpy
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from test_install_pkgmgr_shallow import (
|
||||
nix_profile_list_debug,
|
||||
remove_pkgmgr_from_nix_profile,
|
||||
pkgmgr_help_debug,
|
||||
)
|
||||
|
||||
|
||||
class TestIntegrationInstallAllShallow(unittest.TestCase):
|
||||
def _run_pkgmgr_install_all(self) -> None:
|
||||
"""
|
||||
Helper that runs the CLI command via main.py and provides
|
||||
extra diagnostics if the command exits with a non-zero code.
|
||||
"""
|
||||
cmd_repr = "pkgmgr install --all --clone-mode shallow --no-verification"
|
||||
original_argv = sys.argv
|
||||
try:
|
||||
sys.argv = [
|
||||
"pkgmgr",
|
||||
"install",
|
||||
"--all",
|
||||
"--clone-mode",
|
||||
"shallow",
|
||||
"--no-verification",
|
||||
]
|
||||
|
||||
try:
|
||||
# Execute main.py as if it was called from CLI.
|
||||
# This will run the full install pipeline inside the container.
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
# Convert SystemExit into a more helpful assertion with debug output.
|
||||
exit_code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
|
||||
print("\n[TEST] pkgmgr install --all failed with SystemExit")
|
||||
print(f"[TEST] Command : {cmd_repr}")
|
||||
print(f"[TEST] Exit code: {exit_code}")
|
||||
|
||||
# Additional Nix profile debug on failure
|
||||
nix_profile_list_debug("ON FAILURE (AFTER SystemExit)")
|
||||
|
||||
raise AssertionError(
|
||||
f"{cmd_repr!r} failed with exit code {exit_code}. "
|
||||
"Scroll up to see the full pkgmgr/make output inside the container."
|
||||
) from exc
|
||||
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
def test_install_all_repositories_shallow(self) -> None:
|
||||
"""
|
||||
Run: pkgmgr install --all --clone-mode shallow --no-verification
|
||||
|
||||
This will perform real installations/clones inside the container.
|
||||
The test succeeds if no exception is raised and `pkgmgr --help`
|
||||
works in a fresh interactive bash session afterwards.
|
||||
"""
|
||||
# Debug before cleanup
|
||||
nix_profile_list_debug("BEFORE CLEANUP")
|
||||
|
||||
# Cleanup: aggressively try to drop any pkgmgr/profile entries
|
||||
remove_pkgmgr_from_nix_profile()
|
||||
|
||||
# Debug after cleanup
|
||||
nix_profile_list_debug("AFTER CLEANUP")
|
||||
|
||||
# Run the actual install with extended diagnostics
|
||||
self._run_pkgmgr_install_all()
|
||||
|
||||
# After successful installation: show `pkgmgr --help`
|
||||
# via interactive bash (same as the pkgmgr-only test).
|
||||
pkgmgr_help_debug()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
0
tests/unit/pkgmgr/core/command/__init__.py
Normal file
0
tests/unit/pkgmgr/core/command/__init__.py
Normal file
108
tests/unit/pkgmgr/core/command/test_ink.py
Normal file
108
tests/unit/pkgmgr/core/command/test_ink.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.core.command.ink import create_ink
|
||||
|
||||
|
||||
class TestCreateInk(unittest.TestCase):
|
||||
@patch("pkgmgr.core.command.ink.get_repo_dir")
|
||||
@patch("pkgmgr.core.command.ink.get_repo_identifier")
|
||||
def test_self_referential_command_skips_symlink(
|
||||
self,
|
||||
mock_get_repo_identifier,
|
||||
mock_get_repo_dir,
|
||||
):
|
||||
"""
|
||||
If the resolved command path is identical to the final link target,
|
||||
create_ink() must NOT replace it with a self-referential symlink.
|
||||
|
||||
This simulates the situation where the command already lives at
|
||||
~/.local/bin/<identifier> and we would otherwise create a symlink
|
||||
pointing to itself.
|
||||
"""
|
||||
mock_get_repo_identifier.return_value = "package-manager"
|
||||
mock_get_repo_dir.return_value = "/fake/repo"
|
||||
|
||||
with tempfile.TemporaryDirectory() as bin_dir:
|
||||
# Simulate an existing real binary at the final link location.
|
||||
command_path = os.path.join(bin_dir, "package-manager")
|
||||
with open(command_path, "w", encoding="utf-8") as f:
|
||||
f.write("#!/bin/sh\necho package-manager\n")
|
||||
|
||||
# Sanity check: not a symlink yet.
|
||||
self.assertTrue(os.path.exists(command_path))
|
||||
self.assertFalse(os.path.islink(command_path))
|
||||
|
||||
repo = {"command": command_path}
|
||||
|
||||
# This must NOT turn the file into a self-referential symlink.
|
||||
create_ink(
|
||||
repo=repo,
|
||||
repositories_base_dir="/fake/base",
|
||||
bin_dir=bin_dir,
|
||||
all_repos=[],
|
||||
quiet=True,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
# After create_ink(), the file must still exist and must not be a symlink.
|
||||
self.assertTrue(os.path.exists(command_path))
|
||||
self.assertFalse(
|
||||
os.path.islink(command_path),
|
||||
"create_ink() must not create a self-referential symlink "
|
||||
"when command == link_path",
|
||||
)
|
||||
|
||||
@patch("pkgmgr.core.command.ink.get_repo_dir")
|
||||
@patch("pkgmgr.core.command.ink.get_repo_identifier")
|
||||
def test_create_symlink_for_normal_command(
|
||||
self,
|
||||
mock_get_repo_identifier,
|
||||
mock_get_repo_dir,
|
||||
):
|
||||
"""
|
||||
In the normal case (command path != link target), create_ink()
|
||||
must create a symlink in bin_dir pointing to the given command,
|
||||
and optionally an alias symlink when repo['alias'] is set.
|
||||
"""
|
||||
mock_get_repo_identifier.return_value = "mytool"
|
||||
|
||||
with tempfile.TemporaryDirectory() as repo_dir, tempfile.TemporaryDirectory() as bin_dir:
|
||||
mock_get_repo_dir.return_value = repo_dir
|
||||
|
||||
# Create a fake executable inside the repository.
|
||||
command_path = os.path.join(repo_dir, "main.sh")
|
||||
with open(command_path, "w", encoding="utf-8") as f:
|
||||
f.write("#!/bin/sh\necho mytool\n")
|
||||
os.chmod(command_path, 0o755)
|
||||
|
||||
repo = {
|
||||
"command": command_path,
|
||||
"alias": "mt",
|
||||
}
|
||||
|
||||
create_ink(
|
||||
repo=repo,
|
||||
repositories_base_dir="/fake/base",
|
||||
bin_dir=bin_dir,
|
||||
all_repos=[],
|
||||
quiet=True,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
link_path = os.path.join(bin_dir, "mytool")
|
||||
alias_path = os.path.join(bin_dir, "mt")
|
||||
|
||||
# Main link must exist and point to the command.
|
||||
self.assertTrue(os.path.islink(link_path))
|
||||
self.assertEqual(os.readlink(link_path), command_path)
|
||||
|
||||
# Alias must exist and point to the main link.
|
||||
self.assertTrue(os.path.islink(alias_path))
|
||||
self.assertEqual(os.readlink(alias_path), link_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user