all repos — searchix @ 347521a6dce587e6b51f33608744125d853c7078

Search engine for NixOS, nix-darwin, home-manager and NUR users

internal/programs/programs.go (view raw)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package programs

import (
	"context"
	"database/sql"
	"fmt"
	"os/exec"
	"strings"

	"alin.ovh/x/log"
	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
	_ "modernc.org/sqlite" //nolint:revive // sqlite driver needed for database/sql

	"alin.ovh/searchix/internal/config"
	"alin.ovh/searchix/internal/file"
)

type DB struct {
	source *config.Source
	logger *log.Logger
	root   *file.Root
	db     *sql.DB
	stmt   *sql.Stmt
}

type Options struct {
	Logger *log.Logger
	Root   *file.Root
}

func New(source *config.Source, options *Options) (*DB, error) {
	db, err := sql.Open("sqlite", fmt.Sprintf(
		"file:%s?mode=%s&_pragma=foreign_keys(1)&_pragma=mmap_size(%d)",
		//nolint:forbidigo // external package
		options.Root.JoinPath(
			source.JoinPath("programs.db"),
		),
		"rwc",
		16*1024*1024,
	))
	if err != nil {
		return nil, fault.Wrap(err, fmsg.With("failed to open sqlite database"))
	}
	options.Logger.Debug("opened sqlite database")

	return &DB{
		source: source,

		logger: options.Logger,
		root:   options.Root,
		db:     db,
	}, nil
}

func (p *DB) Instantiate(ctx context.Context) error {
	// nix-instantiate --eval --json -I nixpkgs=channel:nixos-unstable --expr 'toString <nixpkgs/programs.sqlite>'
	args := []string{
		"--eval",
		"--json",
		"-I", fmt.Sprintf("%s=channel:%s", p.source.Key, p.source.Channel),
		"--expr", fmt.Sprintf("toString <%s/%s>", p.source.Key, p.source.Programs.Attribute),
	}

	p.logger.Debug("nix-instantiate command", "args", args)
	cmd := exec.CommandContext(ctx, "nix-instantiate", args...)
	out, err := cmd.Output()
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to run nix-instantiate"))
	}

	outPath := strings.Trim(strings.TrimSpace(string(out)), "\"")
	p.logger.Debug("got output path", "outputPath", outPath)

	_, err = p.db.ExecContext(ctx, "ATTACH DATABASE ? AS input", outPath)
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to attach nix-store programs database"))
	}

	_, err = p.db.ExecContext(ctx, "DROP TABLE IF EXISTS programs")
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to drop programs table"))
	}

	_, err = p.db.ExecContext(ctx, `
CREATE TABLE programs AS
SELECT name, package
FROM input.Programs
GROUP BY name, package
`)
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to create programs table"))
	}
	p.logger.Debug("created programs table")

	_, err = p.db.ExecContext(ctx, `CREATE INDEX idx_package ON programs(package)`)
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to create idx_package index"))
	}
	p.logger.Debug("created idx_package index")

	_, err = p.db.ExecContext(ctx, "DETACH DATABASE input")
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to detach nix-store programs database"))
	}

	return nil
}

func (p *DB) Open(ctx context.Context) (err error) {
	if p.db == nil {
		return fault.New("database not open")
	}

	p.stmt, err = p.db.PrepareContext(ctx, `
SELECT name
FROM programs
WHERE package = ?
`)
	if err != nil {
		return fault.Wrap(err, fmsg.With("failed to prepare statement"))
	}
	p.logger.Debug("prepared statement")

	return nil
}

func (p *DB) Close() error {
	if err := p.db.Close(); err != nil {
		return fault.Wrap(err, fmsg.With("failed to close sqlite database"))
	}

	return nil
}

func (p *DB) GetPackagePrograms(ctx context.Context, pkg string) ([]string, error) {
	var programs []string
	rows, err := p.stmt.QueryContext(ctx, pkg)
	if err != nil {
		return nil, fault.Wrap(err, fmsg.With("failed to execute query"))
	}
	defer rows.Close()

	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			return nil, fault.Wrap(err, fmsg.With("failed to scan row"))
		}

		programs = append(programs, name)
	}
	rerr := rows.Close()
	if rerr != nil {
		return nil, fault.Wrap(rerr, fmsg.With("sql error"))
	}

	return programs, nil
}