Skip to content

Commit d30b331

Browse files
fix(pacstall): prioritises pacstall handling of deb files
1 parent 743931b commit d30b331

5 files changed

Lines changed: 64 additions & 30 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [1.2.2] - 2026-04-10
6+
7+
### Added
8+
- **Smart Pacstall Deduplication**: Added metadata parsing for `gives`, `provides`, and `replaces` fields. `upk` now correctly identifies Pacstall packages (like `bitwarden-deb`) as aliasing standard system packages, preventing duplicate entries and prioritizing clean removals.
9+
510
## [1.2.1] - 2026-03-18
611

712
### Refactored

upk/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2.1
1+
1.2.2

upk/backends/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class PackageInfo:
1313
source: str
1414
description: Optional[str] = None
1515
installed_version: Optional[str] = None
16+
provides: List[str] = None
1617

1718
@property
1819
def is_installed(self) -> bool:

upk/backends/pacstall.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -112,22 +112,35 @@ def _load_installed_cache(self) -> None:
112112
name = line
113113

114114
if name:
115-
# Try to get version and description from metadata file if unknown
116-
if version == "unknown":
117-
metadata_path = f"/var/lib/pacstall/metadata/{name}"
118-
if os.path.exists(metadata_path):
119-
try:
120-
with open(metadata_path, 'r') as f:
121-
for meta_line in f:
122-
if meta_line.startswith('_version='):
123-
version = meta_line.split('=', 1)[1].strip().strip('"').strip("'")
124-
break
125-
except Exception:
126-
pass
115+
provides = []
116+
# Try to get metadata from file
117+
metadata_path = f"/var/lib/pacstall/metadata/{name}"
118+
if os.path.exists(metadata_path):
119+
try:
120+
with open(metadata_path, 'r') as f:
121+
for meta_line in f:
122+
# Get version
123+
if version == "unknown" and meta_line.startswith('_version='):
124+
version = meta_line.split('=', 1)[1].strip().strip('"').strip("'")
125+
126+
# Get gives/replaces/provides to link names
127+
if meta_line.startswith(('_gives=', '_provides=', '_replaces=')):
128+
val = meta_line.split('=', 1)[1].strip().strip('"').strip("'")
129+
# Handle array format like ("name1" "name2")
130+
if val.startswith('(') and val.endswith(')'):
131+
val = val[1:-1].strip()
132+
names = [n.strip().strip('"').strip("'") for n in val.split(' ')]
133+
provides.extend([n for n in names if n])
134+
else:
135+
if val:
136+
provides.append(val)
137+
except Exception:
138+
pass
127139

128140
self._installed_cache[name] = {
129141
"version": version,
130-
"description": None
142+
"description": None,
143+
"provides": list(set(provides)) # Deduplicate
131144
}
132145
except (subprocess.TimeoutExpired, FileNotFoundError):
133146
self._installed_cache = {}
@@ -220,6 +233,7 @@ def list_packages(self) -> List[PackageInfo]:
220233
version=info["version"],
221234
source=self.name,
222235
description=info.get("description"),
223-
installed_version=info["version"]
236+
installed_version=info["version"],
237+
provides=info.get("provides", [])
224238
))
225239
return packages

upk/upk.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,38 @@ def get_version():
2323

2424
def deduplicate_results(results: list) -> list:
2525
"""
26-
Deduplicate results when a package is reported by both APT and Pacstall.
27-
Pacstall is preferred for removal and listing if it's the same package.
26+
Deduplicate results when a package is reported by both APT and another backend.
27+
Backends like Pacstall are preferred if they provide the same software.
2828
"""
29-
# Only care about installed packages for this specific deduplication
30-
pacstall_installed = {p.name: p for p in results if p.source == 'pacstall' and p.is_installed}
31-
if not pacstall_installed:
29+
# Find all names provided by non-apt installed packages
30+
non_apt_installed = [p for p in results if p.source != 'apt' and p.is_installed]
31+
if not non_apt_installed:
3232
return results
3333

34+
# Map of names that should cause an APT package to be hidden
35+
hidden_names = set()
36+
for p in non_apt_installed:
37+
hidden_names.add(p.name)
38+
if p.provides:
39+
for provided in p.provides:
40+
hidden_names.add(provided)
41+
3442
final_results = []
35-
for p in results:
36-
# If it's an APT package that's also installed via Pacstall
37-
if p.source == 'apt' and p.name in pacstall_installed:
38-
# Transfer description to Pacstall if the latter is missing one
39-
if not pacstall_installed[p.name].description and p.description:
40-
pacstall_installed[p.name].description = p.description
41-
# Skip this APT entry
43+
apt_results = [p for p in results if p.source == 'apt']
44+
other_results = [p for p in results if p.source != 'apt']
45+
46+
for p in apt_results:
47+
if p.name in hidden_names:
48+
# This APT package is covered by another backend
49+
# Transfer description to the provider if it's missing
50+
for other in non_apt_installed:
51+
if other.name == p.name or (other.provides and p.name in other.provides):
52+
if not other.description and p.description:
53+
other.description = p.description
4254
continue
4355
final_results.append(p)
56+
57+
final_results.extend(other_results)
4458
return final_results
4559

4660
@click.group(context_settings=dict(help_option_names=['-h', '--help']), name="upk")
@@ -430,9 +444,9 @@ def upgrade(package: str):
430444

431445
if package:
432446
console.print(f"Checking where [bold cyan]{package}[/bold cyan] is installed...")
433-
from .search import search_all_backends
434-
results = search_all_backends(available_backends, package)
435-
installed_sources = {pkg.source for pkg in results if pkg.name == package and pkg.is_installed}
447+
from .search import list_all_backends
448+
results = list_all_backends(available_backends)
449+
installed_sources = {pkg.source for pkg in results if pkg.name.lower() == package.lower()}
436450

437451
if not installed_sources:
438452
console.print(f"[yellow]No installed exact match for '{package}' found in any selected source.[/yellow]")

0 commit comments

Comments
 (0)