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 := max(len(bv), len(av))
163
164 for i := range maxLen {
165 x := 0
166 if i < len(av) {
167 x = av[i]
168 }
169 y := 0
170 if i < len(bv) {
171 y = bv[i]
172 }
173 switch {
174 case x > y:
175 return 1, true
176 case x < y:
177 return -1, true
178 }
179 }
180 return 0, true
181}
182
183// parseVersionPrefix parses a dotted numeric version prefix (e.g. 4.20.3).
184// It stops at the first non-digit/dot after a numeric segment.
185func parseVersionPrefix(s string) ([]int, bool) {
186 var parts []int
187 cur := 0
188 inNum := false
189
190loop:
191 for i := 0; i < len(s); i++ {
192 c := s[i]
193 switch {
194 case c >= '0' && c <= '9':
195 inNum = true
196 cur = cur*10 + int(c-'0')
197 case c == '.':
198 if !inNum {
199 return nil, false
200 }
201 parts = append(parts, cur)
202 cur = 0
203 inNum = false
204 default:
205 break loop
206 }
207 }
208
209 if inNum {
210 parts = append(parts, cur)
211 }
212 if len(parts) == 0 {
213 return nil, false
214 }
215 return parts, true
216}