Skip to content

Commit bf2bf11

Browse files
authored
Feature: Add pkgtool origin (Slackware support) (#439)
1 parent 9f46302 commit bf2bf11

8 files changed

Lines changed: 210 additions & 4 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
**qp** queries over 6x faster than native package searching while returning more comprehensive metadata than native package search solutions.
66

7-
Query packages from `apk`, `brew`, `pacman`, `apt`, `flatpak`, `snap`, `npm`, `pipx`, `dnf`, and `opkg`. Ecosystems are added frequently!
7+
Query packages from `apk`, `brew`, `pacman`, `apt`, `flatpak`, `snap`, `npm`, `pipx`, `dnf`, `pkgtool`, and `opkg`. Ecosystems are added frequently!
88

99
**qp** supports querying with full boolean logic for package metadata, dependency relations, and more.
1010

@@ -54,7 +54,8 @@ This package is compatible with the following platforms and distributions:
5454
- [Mabox Linux](https://maboxlinux.org/)
5555
- [Zorin OS](https://zorin.com/os/)
5656
- [elementary OS](https://elementary.io/)
57-
- The 50 other Arch, Debian, and Fedora-based distros, as long as they have `apk`, `apt`/`dpkg`, `brew`, `pacman`, `flatpak`, `dnf`/`yum`, or `opkg` installed.
57+
- [Slackware](http://www.slackware.com/)
58+
- The 50 other Arch, Debian, and Fedora-based distros, as long as they have `apk`, `apt`/`dpkg`, `brew`, `pacman`, `flatpak`, `dnf`/`yum`, `pkgtool`, or `opkg` installed.
5859

5960
**qp** also detects and queries other system level package managers like `flatpak`, `snap`, `npm`, and `pipx` for globally installed applications, expanding package discovery beyond traditional system package management.
6061

@@ -66,7 +67,7 @@ This package is compatible with the following platforms and distributions:
6667
* Compatible with MacOS, Arch, Debian, OpenWrt, and over 60 distros
6768
* Supports multiple ecosystems:
6869
* System package managers:
69-
* `apk`, `pacman`, `brew`, `apt`/`dpkg`, `dnf`/`yum`, and `opkg`
70+
* `apk`, `pacman`, `brew`, `apt`/`dpkg`, `dnf`/`yum`, `pkgtool`, and `opkg`
7071
* Application package managers:
7172
* `flatpak`, `snap`, `npm`, and `pipx`
7273
* Query packages using an expressive query language
@@ -197,6 +198,7 @@ Learn about installation [here](#Installation).
197198
| - | log levels || chunked cache (70% speed boost) |
198199
|| apk origin (alpine linux support) || optimize creation-time validation |
199200
| - | package for apk (alpine) | - | log file |
201+
|| pkgtool origin (slackware support) || multi-line queries |
200202

201203
## Installation
202204

@@ -349,6 +351,7 @@ The `pkgtype` field indicates the type or category of package within each ecosys
349351
| deb | none | - |
350352
| rpm | none | - |
351353
| opkg | none | - |
354+
| pkgtool | none | - |
352355
| pipx | none | - |
353356
| npm | none | - |
354357

internal/consts/origins.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const (
88
OriginNpm = "npm"
99
OriginOpkg = "opkg"
1010
OriginPacman = "pacman"
11+
OriginPkgtool = "pkgtool"
1112
OriginPipx = "pipx"
1213
OriginRpm = "rpm"
1314
OriginSnap = "snap"
@@ -21,6 +22,7 @@ var ValidOrigins = []string{
2122
OriginNpm,
2223
OriginOpkg,
2324
OriginPacman,
25+
OriginPkgtool,
2426
OriginPipx,
2527
OriginRpm,
2628
OriginSnap,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package pkgtool
2+
3+
const (
4+
packagesDbPath = "/var/log/packages"
5+
6+
fieldPackageName = "PACKAGE NAME"
7+
fieldUncompressedSize = "UNCOMPRESSED PACKAGE SIZE"
8+
fieldPackageDescription = "PACKAGE DESCRIPTION"
9+
fieldFileList = "FILE LIST"
10+
)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package pkgtool
2+
3+
import (
4+
"os"
5+
"qp/internal/consts"
6+
"qp/internal/origins/shared"
7+
"qp/internal/pkgdata"
8+
"qp/internal/storage"
9+
)
10+
11+
type PkgtoolDriver struct{}
12+
13+
func (d *PkgtoolDriver) Name() string {
14+
return consts.OriginPkgtool
15+
}
16+
17+
func (d *PkgtoolDriver) Detect() bool {
18+
_, err := os.Stat(packagesDbPath)
19+
20+
return err == nil
21+
}
22+
23+
func (d *PkgtoolDriver) Load(_ string) ([]*pkgdata.PkgInfo, error) {
24+
return fetchPackages(d.Name())
25+
}
26+
27+
func (d *PkgtoolDriver) ResolveDeps(pkgs []*pkgdata.PkgInfo) ([]*pkgdata.PkgInfo, error) {
28+
return pkgdata.ResolveDependencyGraph(pkgs, nil)
29+
}
30+
31+
func (d *PkgtoolDriver) LoadCache(cacheRoot string) ([]*pkgdata.PkgInfo, error) {
32+
return storage.LoadProtoCache(cacheRoot)
33+
}
34+
35+
func (d *PkgtoolDriver) SaveCache(cacheRoot string, pkgs []*pkgdata.PkgInfo) error {
36+
return storage.SaveProtoCache(cacheRoot, pkgs)
37+
}
38+
39+
func (d *PkgtoolDriver) IsCacheStale(cacheMtime int64) (bool, error) {
40+
return shared.BfsStale(packagesDbPath, cacheMtime, 1)
41+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package pkgtool
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"qp/internal/pkgdata"
8+
"qp/internal/worker"
9+
"sync"
10+
)
11+
12+
func fetchPackages(origin string) ([]*pkgdata.PkgInfo, error) {
13+
entries, err := os.ReadDir(packagesDbPath)
14+
if err != nil {
15+
return []*pkgdata.PkgInfo{}, fmt.Errorf("failed to read package install directories: %w", err)
16+
}
17+
18+
inputChan := make(chan string, len(entries))
19+
errChan := make(chan error, worker.DefaultBufferSize)
20+
var errGroup sync.WaitGroup
21+
22+
for _, entry := range entries {
23+
if !entry.IsDir() {
24+
inputChan <- filepath.Join(packagesDbPath, entry.Name())
25+
}
26+
}
27+
28+
close(inputChan)
29+
30+
resultChan := worker.RunWorkers(
31+
inputChan,
32+
errChan,
33+
&errGroup,
34+
func(packagePath string) (*pkgdata.PkgInfo, error) {
35+
return parsePackageFile(packagePath, origin)
36+
},
37+
0,
38+
len(entries),
39+
)
40+
41+
go func() {
42+
errGroup.Wait()
43+
close(errChan)
44+
}()
45+
46+
return worker.CollectOutput(resultChan, errChan)
47+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package pkgtool
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"os"
7+
"qp/internal/consts"
8+
"qp/internal/pkgdata"
9+
"strconv"
10+
"strings"
11+
"unicode"
12+
)
13+
14+
func parsePackageFile(packagePath string, origin string) (*pkgdata.PkgInfo, error) {
15+
data, err := os.ReadFile(packagePath)
16+
if err != nil {
17+
return nil, fmt.Errorf("failed to open package metadata: %w", err)
18+
}
19+
20+
pkg := &pkgdata.PkgInfo{
21+
Origin: origin,
22+
Reason: consts.ReasonExplicit,
23+
}
24+
25+
inDesc := false
26+
var description []string
27+
28+
for line := range bytes.SplitSeq(data, []byte("\n")) {
29+
parts := strings.SplitN(string(line), ":", 2)
30+
if len(parts) != 2 {
31+
continue
32+
}
33+
34+
key, value := parts[0], parts[1]
35+
36+
switch key {
37+
case fieldPackageName:
38+
extractMetadata(value, pkg)
39+
case fieldUncompressedSize:
40+
pkg.Size = extractSize(value)
41+
case fieldPackageDescription:
42+
inDesc = true
43+
case fieldFileList:
44+
// exit metadata file
45+
break
46+
default:
47+
if inDesc {
48+
description = append(description, strings.TrimSpace(value))
49+
}
50+
51+
}
52+
}
53+
54+
fileInfo, _ := os.Stat(packagePath)
55+
pkg.UpdateTimestamp = fileInfo.ModTime().Unix()
56+
pkg.Description = strings.Join(description, " ")
57+
58+
return pkg, nil
59+
}
60+
61+
func extractMetadata(value string, pkg *pkgdata.PkgInfo) {
62+
trimmed := strings.TrimSpace(value)
63+
parts := strings.Split(trimmed, "-")
64+
if len(parts) < 4 {
65+
return
66+
}
67+
68+
pkg.Arch = parts[len(parts)-2]
69+
pkg.Version = parts[len(parts)-3]
70+
pkg.Name = strings.Join(parts[:len(parts)-3], "-")
71+
}
72+
73+
func extractSize(value string) int64 {
74+
sizeStr := strings.TrimSpace(value)
75+
firstLetterIdx := strings.IndexFunc(sizeStr, func(char rune) bool {
76+
return unicode.IsLetter(char)
77+
})
78+
79+
rawSize, err := strconv.ParseFloat(sizeStr[:firstLetterIdx], 64)
80+
if err != nil {
81+
return 0
82+
}
83+
84+
unit := sizeStr[firstLetterIdx:]
85+
var size int64
86+
87+
switch unit {
88+
case "G":
89+
size = int64(rawSize * consts.GB)
90+
case "M":
91+
size = int64(rawSize * consts.MB)
92+
case "K":
93+
size = int64(rawSize * consts.KB)
94+
case "B":
95+
fallthrough
96+
default:
97+
size = int64(rawSize)
98+
}
99+
100+
return size
101+
}

internal/origins/registry.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"qp/internal/origins/drivers/opkg"
1111
"qp/internal/origins/drivers/pacman"
1212
"qp/internal/origins/drivers/pipx"
13+
"qp/internal/origins/drivers/pkgtool"
1314
"qp/internal/origins/drivers/rpm"
1415
"qp/internal/origins/drivers/snap"
1516
)
@@ -22,6 +23,7 @@ var registeredDrivers = []driver.Driver{
2223
&opkg.OpkgDriver{},
2324
&npm.NpmDriver{},
2425
&pacman.PacmanDriver{},
26+
&pkgtool.PkgtoolDriver{},
2527
&pipx.PipxDriver{},
2628
&rpm.RpmDriver{},
2729
&snap.SnapDriver{},

qp.1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ qp \- query packages. A CLI utility for querying installed packages across multi
88

99
.SH DESCRIPTION
1010
.B qp
11-
is a fast, flexible, and standalone CLI utility for querying installed packages across multiple package ecosystems and operating systems. It supports Arch Linux (pacman), Debian/Ubuntu (apt/dpkg), Homebrew (brew), OpenWrt (opkg), Fedora/RHEL (dnf/yum), Alpine Linux (apk), and application-level ecosystems like pipx, npm, snap, + flatpak.
11+
is a fast, flexible, and standalone CLI utility for querying installed packages across multiple package ecosystems and operating systems. It supports Arch Linux (pacman), Debian/Ubuntu (apt/dpkg), Homebrew (brew), OpenWrt (opkg), Fedora/RHEL (dnf/yum), Alpine Linux (apk), pkgtool (Slackware), and application-level ecosystems like pipx, npm, snap, + flatpak.
1212

1313
Features include:
1414
- Cross-platform package discovery

0 commit comments

Comments
 (0)