1// Package usecase contains application business logic.
2package usecase
3
4import (
5 "context"
6 "strings"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12const (
13 defaultMinGTK4Version = "4.20"
14 defaultMinWebKitGTKVersion = "2.50"
15 defaultMinGLibVersion = "2.84"
16)
17
18type RuntimeDependencyID string
19
20const (
21 RuntimeDependencyGTK4 RuntimeDependencyID = "gtk4"
22 RuntimeDependencyWebKitGTK RuntimeDependencyID = "webkitgtk-6.0"
23)
24
25// RuntimeDependencyStatus contains the result of checking a runtime dependency.
26type RuntimeDependencyStatus struct {
27 ID RuntimeDependencyID
28 PkgConfigName string
29 DisplayName string
30
31 Installed bool
32 Version string
33
34 RequiredVersion string
35 MeetsRequirement bool
36
37 Error string
38}
39
40// CheckRuntimeDependenciesUseCase validates runtime requirements for the GUI browser.
41type CheckRuntimeDependenciesUseCase struct {
42 probe port.RuntimeVersionProbe
43}
44
45// NewCheckRuntimeDependenciesUseCase creates a new use case.
46func NewCheckRuntimeDependenciesUseCase(probe port.RuntimeVersionProbe) *CheckRuntimeDependenciesUseCase {
47 return &CheckRuntimeDependenciesUseCase{probe: probe}
48}
49
50// CheckRuntimeDependenciesInput contains options for runtime dependency checks.
51type CheckRuntimeDependenciesInput struct {
52 // Prefix optionally points to a custom runtime prefix (e.g. /opt/webkitgtk).
53 // Implementations may use this to influence pkg-config search paths.
54 Prefix string
55
56 // Min versions. If empty, defaults are used.
57 MinGTK4Version string
58 MinWebKitGTKVersion string
59 MinGLibVersion string
60}
61
62// CheckRuntimeDependenciesOutput contains the result of the runtime dependency checks.
63type CheckRuntimeDependenciesOutput struct {
64 Prefix string
65 OK bool
66 Checks []RuntimeDependencyStatus
67}
68
69// Execute checks WebKitGTK and GTK runtime versions.
70func (uc *CheckRuntimeDependenciesUseCase) Execute(
71 ctx context.Context,
72 input CheckRuntimeDependenciesInput,
73) (*CheckRuntimeDependenciesOutput, error) {
74 log := logging.FromContext(ctx).With().Str("component", "runtime-check").Logger()
75
76 minGTK := input.MinGTK4Version
77 if minGTK == "" {
78 minGTK = defaultMinGTK4Version
79 }
80 minWebKit := input.MinWebKitGTKVersion
81 if minWebKit == "" {
82 minWebKit = defaultMinWebKitGTKVersion
83 }
84 minGLib := input.MinGLibVersion
85 if minGLib == "" {
86 minGLib = defaultMinGLibVersion
87 }
88
89 checks := []RuntimeDependencyStatus{
90 {
91 ID: RuntimeDependencyGTK4,
92 PkgConfigName: "gtk4",
93 DisplayName: "GTK4",
94 RequiredVersion: minGTK,
95 },
96 {
97 ID: RuntimeDependencyWebKitGTK,
98 PkgConfigName: "webkitgtk-6.0",
99 DisplayName: "WebKitGTK 6.0",
100 RequiredVersion: minWebKit,
101 },
102 {
103 ID: "glib-2.0",
104 PkgConfigName: "glib-2.0",
105 DisplayName: "GLib",
106 RequiredVersion: minGLib,
107 },
108 {
109 ID: "gio-2.0",
110 PkgConfigName: "gio-2.0",
111 DisplayName: "GIO",
112 RequiredVersion: minGLib,
113 },
114 }
115
116 allOK := true
117 for i := range checks {
118 status := &checks[i]
119
120 version, err := uc.probe.PkgConfigModVersion(ctx, status.PkgConfigName, input.Prefix)
121 if err != nil {
122 status.Installed = false
123 status.MeetsRequirement = false
124 status.Error = err.Error()
125 allOK = false
126 continue
127 }
128
129 status.Installed = true
130 status.Version = strings.TrimSpace(version)
131
132 cmp, ok := compareVersion(status.Version, status.RequiredVersion)
133 if !ok {
134 status.MeetsRequirement = false
135 status.Error = "could not parse version"
136 allOK = false
137 continue
138 }
139
140 status.MeetsRequirement = cmp >= 0
141 if !status.MeetsRequirement {
142 allOK = false
143 }
144 }
145
146 log.Debug().Bool("ok", allOK).Str("prefix", input.Prefix).Msg("runtime dependency check complete")
147 return &CheckRuntimeDependenciesOutput{Prefix: input.Prefix, OK: allOK, Checks: checks}, nil
148}
149
150// compareVersion compares two version strings.
151// Returns 1 if a > b, 0 if a == b, -1 if a < b. ok is false if either cannot be parsed.
152func compareVersion(a, b string) (cmp int, ok bool) {
153 av, ok := parseVersionPrefix(a)
154 if !ok {
155 return 0, false
156 }
157 bv, ok := parseVersionPrefix(b)
158 if !ok {
159 return 0, false
160 }
161
162 maxLen := len(av)
163 if len(bv) > maxLen {
164 maxLen = len(bv)
165 }
166
167 for i := 0; i < maxLen; i++ {
168 x := 0
169 if i < len(av) {
170 x = av[i]
171 }
172 y := 0
173 if i < len(bv) {
174 y = bv[i]
175 }
176 switch {
177 case x > y:
178 return 1, true
179 case x < y:
180 return -1, true
181 }
182 }
183 return 0, true
184}
185
186// parseVersionPrefix parses a dotted numeric version prefix (e.g. 4.20.3).
187// It stops at the first non-digit/dot after a numeric segment.
188func parseVersionPrefix(s string) ([]int, bool) {
189 var parts []int
190 cur := 0
191 inNum := false
192
193loop:
194 for i := 0; i < len(s); i++ {
195 c := s[i]
196 switch {
197 case c >= '0' && c <= '9':
198 inNum = true
199 cur = cur*10 + int(c-'0')
200 case c == '.':
201 if !inNum {
202 return nil, false
203 }
204 parts = append(parts, cur)
205 cur = 0
206 inNum = false
207 default:
208 break loop
209 }
210 }
211
212 if inNum {
213 parts = append(parts, cur)
214 }
215 if len(parts) == 0 {
216 return nil, false
217 }
218 return parts, true
219}