Skip to content

Commit 472adfc

Browse files
authored
Feature: Partial support for formulae from taps (#333)
1 parent 57d0fea commit 472adfc

5 files changed

Lines changed: 309 additions & 33 deletions

File tree

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,22 @@ learn about installation [here](#installation)
143143
|| bottles in brew || casks in brew |
144144
| - | replaced-by resolution | - | multi-license support |
145145
|| short-args for queries || key/value output |
146-
| - | rpm origin (dnf/yum support) | - | homebrew packaging |
146+
| - | rpm origin (dnf/yum support) || homebrew packaging |
147+
| - | pip origin (python packaging) | - | formulae from taps (brew) |
148+
| - | casks from taps (brew) | - | dependencies for casks |
147149

148150
## installation
149151

152+
### homebrew (macOS or linuxbrew)
153+
154+
if you have [homebrew](https://brew.sh/) (`brew`), install via `qp`'s cask repo:
155+
```bash
156+
brew tap zweih/qp
157+
brew install zweih/qp/qp
158+
```
159+
160+
**note**: until we are added to the official `homebrew/core` repo, ensure that the package you install is `zweih/qp/qp`
161+
150162
### arch-based systems (AUR)
151163

152164
install using an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `yay`, `paru`, `aura`, etc.:

internal/origins/drivers/brew/discover.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import (
77
"strings"
88
)
99

10-
func getInstalledFormulae(cellarRoot, binRoot string) ([]installedPkg, error) {
10+
func getInstalledFormulae(cellarRoot, binRoot string) ([]*installedPkg, error) {
1111
entries, err := os.ReadDir(cellarRoot)
1212
if err != nil {
1313
return nil, fmt.Errorf("failed to read Cellar directory: %w", err)
1414
}
1515

16-
var pkgs []installedPkg
16+
var iPkgs []*installedPkg
1717
for _, entry := range entries {
1818
if !entry.IsDir() {
1919
continue
@@ -25,15 +25,15 @@ func getInstalledFormulae(cellarRoot, binRoot string) ([]installedPkg, error) {
2525
continue
2626
}
2727

28-
pkgs = append(pkgs, installedPkg{
28+
iPkgs = append(iPkgs, &installedPkg{
2929
Name: name,
3030
Version: version,
31-
ReceiptPath: filepath.Join(cellarRoot, name, version, receiptName),
3231
VersionPath: filepath.Join(cellarRoot, name, version),
32+
IsTap: true,
3333
})
3434
}
3535

36-
return pkgs, nil
36+
return iPkgs, nil
3737
}
3838

3939
func resolveLinkedVersion(pkgName string, cellarRoot string, binRoot string) (string, error) {
@@ -67,3 +67,60 @@ func resolveLinkedVersion(pkgName string, cellarRoot string, binRoot string) (st
6767

6868
return parts[len(parts)-3], nil
6969
}
70+
71+
func getTapPackageNames(prefix string, pkgType string) (map[string]struct{}, error) {
72+
tapsRoot := filepath.Join(prefix, "Homebrew/Library/Taps")
73+
result := make(map[string]struct{})
74+
75+
users, err := os.ReadDir(tapsRoot)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
for _, user := range users {
81+
userPath := filepath.Join(tapsRoot, user.Name())
82+
repos, err := os.ReadDir(userPath)
83+
if err != nil {
84+
continue
85+
}
86+
87+
for _, repo := range repos {
88+
if !repo.IsDir() {
89+
continue
90+
}
91+
92+
repoPath := filepath.Join(userPath, repo.Name())
93+
var searchDirs []string
94+
95+
switch pkgType {
96+
case typeFormula:
97+
searchDirs = []string{
98+
filepath.Join(repoPath, "Formula"),
99+
filepath.Join(repoPath, "HomebrewFormula"),
100+
repoPath,
101+
}
102+
case typeCask:
103+
searchDirs = []string{filepath.Join(repoPath, "Casks")}
104+
default:
105+
continue
106+
}
107+
108+
for _, dir := range searchDirs {
109+
entries, err := os.ReadDir(dir)
110+
if err != nil {
111+
continue
112+
}
113+
114+
for _, entry := range entries {
115+
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".rb") {
116+
name := strings.TrimSuffix(entry.Name(), ".rb")
117+
result[name] = struct{}{}
118+
}
119+
}
120+
break
121+
}
122+
}
123+
}
124+
125+
return result, nil
126+
}

internal/origins/drivers/brew/fetch_formulae.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import (
1010
type installedPkg struct {
1111
Name string
1212
Version string
13-
ReceiptPath string
1413
VersionPath string
15-
ModTime int64
14+
IsTap bool
1615
}
1716

1817
func fetchFormulae(
@@ -34,9 +33,13 @@ func fetchFormulae(
3433
return
3534
}
3635

37-
wanted := make(map[string]struct{}, len(installedPkgs))
36+
tapPkgs, _ := getTapPackageNames(prefix, typeFormula)
37+
wanted := make(map[string]struct{})
3838
for _, iPkg := range installedPkgs {
39-
wanted[iPkg.Name] = struct{}{}
39+
if _, fromTap := tapPkgs[iPkg.Name]; !fromTap {
40+
wanted[iPkg.Name] = struct{}{}
41+
iPkg.IsTap = false
42+
}
4043
}
4144

4245
var formulaMeta map[string]*FormulaMetadata
@@ -49,7 +52,7 @@ func fetchFormulae(
4952
formulaMeta, metaErr = loadMetadata(formulaCachePath, getFormulaKey, wanted)
5053
}()
5154

52-
inputChan := make(chan installedPkg, len(installedPkgs))
55+
inputChan := make(chan *installedPkg, len(installedPkgs))
5356
for _, iPkg := range installedPkgs {
5457
inputChan <- iPkg
5558
}
@@ -60,8 +63,8 @@ func fetchFormulae(
6063
inputChan,
6164
errChan,
6265
errGroup,
63-
func(iPkg installedPkg) (*pkgdata.PkgInfo, error) {
64-
return parseFormulaReceipt(iPkg.ReceiptPath, iPkg.Version)
66+
func(iPkg *installedPkg) (*pkgdata.PkgInfo, error) {
67+
return parseFormulaReceipt(iPkg)
6568
},
6669
0,
6770
len(installedPkgs),

internal/origins/drivers/brew/formula_parser.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"path/filepath"
77
"qp/internal/consts"
88
"qp/internal/pkgdata"
9-
"strings"
109

1110
json "github.com/goccy/go-json"
1211
)
@@ -34,8 +33,8 @@ type FormulaMetadata struct {
3433
RecommendedDependencies []string `json:"recommended_dependencies"`
3534
}
3635

37-
func parseFormulaReceipt(path string, version string) (*pkgdata.PkgInfo, error) {
38-
data, err := os.ReadFile(path)
36+
func parseFormulaReceipt(iPkg *installedPkg) (*pkgdata.PkgInfo, error) {
37+
data, err := os.ReadFile(filepath.Join(iPkg.VersionPath, receiptName))
3938
if err != nil {
4039
return nil, fmt.Errorf("failed to read receipt JSON: %v", err)
4140
}
@@ -45,26 +44,25 @@ func parseFormulaReceipt(path string, version string) (*pkgdata.PkgInfo, error)
4544
return nil, fmt.Errorf("failed to parse receipt JSON: %v", err)
4645
}
4746

48-
pkgName, err := getPkgNameFromPath(path)
49-
if err != nil {
50-
return nil, err
51-
}
52-
5347
reason := consts.ReasonExplicit
5448
if !receipt.InstalledOnRequest {
5549
reason = consts.ReasonDependency
5650
}
5751

5852
pkg := &pkgdata.PkgInfo{
5953
InstallTimestamp: receipt.Time,
60-
Name: pkgName,
54+
Name: iPkg.Name,
6155
Reason: reason,
62-
Version: version,
56+
Version: iPkg.Version,
6357
Arch: receipt.Arch,
6458
PkgType: typeFormula,
6559
Depends: parseDepends(receipt),
6660
}
6761

62+
if iPkg.IsTap {
63+
inferTapMetadata(pkg, iPkg.VersionPath)
64+
}
65+
6866
inferBuildDate(pkg, receipt)
6967

7068
return pkg, nil
@@ -76,16 +74,6 @@ func inferBuildDate(pkg *pkgdata.PkgInfo, receipt FormulaReceipt) {
7674
}
7775
}
7876

79-
func getPkgNameFromPath(path string) (string, error) {
80-
parts := strings.Split(filepath.Clean(path), string(os.PathSeparator))
81-
82-
if len(parts) >= 3 {
83-
return parts[len(parts)-3], nil
84-
}
85-
86-
return "", fmt.Errorf("unexpected receipt path format: %s", path)
87-
}
88-
8977
func parseDepends(receipt FormulaReceipt) []pkgdata.Relation {
9078
rels := make([]pkgdata.Relation, 0, len(receipt.RuntimeDependencies))
9179

0 commit comments

Comments
 (0)