Skip to content

Commit ed704ee

Browse files
committed
fix: version parsing for packages with dashes, improve dry-run output
- Use xbps-uhelper for correct pkgver parsing (fixes visual-studio-code-insiders) - Add update detection for already-installed VUP packages - Show "Package already installed" vs "Dependencies already installed"
1 parent 9d960a5 commit ed704ee

3 files changed

Lines changed: 168 additions & 36 deletions

File tree

vuru/src/core/resolve/resolver.odin

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import "core:strings"
66
import errors "../../core/errors"
77
import index "../../core/index"
88
import template "../../core/template"
9+
import xbps "../../core/xbps"
910
import utils "../../utils"
1011
import config "../config"
1112

@@ -14,6 +15,29 @@ is_pkg_installed :: proc(name: string) -> bool {
1415
return utils.run_command_silent({"xbps-query", name}) == 0
1516
}
1617

18+
// Get the installed version of a package
19+
// Returns empty string if not installed
20+
get_installed_version :: proc(name: string) -> (version: string, ok: bool) {
21+
output, cmd_ok := utils.run_command_output({"xbps-query", name}, context.temp_allocator)
22+
if !cmd_ok {
23+
return "", false
24+
}
25+
26+
// Parse pkgver from output
27+
for line in strings.split_lines_iterator(&output) {
28+
if strings.has_prefix(line, "pkgver:") {
29+
value := strings.trim_space(line[7:])
30+
// Use xbps-uhelper for correct parsing of packages with dashes in names
31+
_, version, parse_ok := xbps.parse_pkgver(value)
32+
if parse_ok {
33+
return version, true
34+
}
35+
}
36+
}
37+
38+
return "", false
39+
}
40+
1741
// Check if package exists in official Void repos
1842
is_in_official_repos :: proc(name: string) -> (version: string, ok: bool) {
1943
output, cmd_ok := utils.run_command_output({"xbps-query", "-R", name}, context.temp_allocator)
@@ -25,9 +49,10 @@ is_in_official_repos :: proc(name: string) -> (version: string, ok: bool) {
2549
for line in strings.split_lines_iterator(&output) {
2650
if strings.has_prefix(line, "pkgver:") {
2751
value := strings.trim_space(line[7:])
28-
// Format: name-version
29-
if idx := strings.last_index(value, "-"); idx > 0 {
30-
return strings.clone(value[idx + 1:], context.temp_allocator), true
52+
// Use xbps-uhelper for correct parsing of packages with dashes in names
53+
_, version, parse_ok := xbps.parse_pkgver(value)
54+
if parse_ok {
55+
return version, true
3156
}
3257
}
3358
}
@@ -48,18 +73,35 @@ resolve_package :: proc(
4873
bool,
4974
) {
5075
// 1. Check if already installed
51-
if is_pkg_installed(name) {
52-
return Resolved_Package {
53-
name = strings.clone(name, allocator),
54-
source = .Official, // Treat as satisfied (empty version = already installed)
55-
version = "",
56-
depth = depth,
57-
}, true
58-
}
76+
installed_ver, is_installed := get_installed_version(name)
5977

6078
// 2. Check VUP index for binary
6179
if vup_pkg, ok := index.index_get_package(idx, name); ok {
6280
if url, url_ok := vup_pkg.repo_urls[arch]; url_ok {
81+
// If installed, check if VUP has a newer version
82+
if is_installed {
83+
if xbps.version_greater_than(vup_pkg.version, installed_ver, utils.run_command) {
84+
// VUP has a newer version - mark for upgrade
85+
return Resolved_Package {
86+
name = strings.clone(name, allocator),
87+
source = .VUP,
88+
version = strings.clone(vup_pkg.version, allocator),
89+
repo_url = strings.clone(url, allocator),
90+
category = strings.clone(vup_pkg.category, allocator),
91+
depth = depth,
92+
},
93+
true
94+
} else {
95+
// Already up to date
96+
return Resolved_Package {
97+
name = strings.clone(name, allocator),
98+
source = .Official, // Treat as satisfied (empty version = already installed)
99+
version = "",
100+
depth = depth,
101+
}, true
102+
}
103+
}
104+
// Not installed - install from VUP
63105
return Resolved_Package {
64106
name = strings.clone(name, allocator),
65107
source = .VUP,
@@ -72,7 +114,17 @@ resolve_package :: proc(
72114
}
73115
}
74116

75-
// 3. Check official Void repos
117+
// 3. Already installed but not in VUP index - satisfied
118+
if is_installed {
119+
return Resolved_Package {
120+
name = strings.clone(name, allocator),
121+
source = .Official, // Treat as satisfied (empty version = already installed)
122+
version = "",
123+
depth = depth,
124+
}, true
125+
}
126+
127+
// 4. Check official Void repos
76128
if version, ok := is_in_official_repos(name); ok {
77129
return Resolved_Package {
78130
name = strings.clone(name, allocator),
@@ -83,7 +135,7 @@ resolve_package :: proc(
83135
true
84136
}
85137

86-
// 4. Not found anywhere - return false, no allocations made
138+
// 5. Not found anywhere - return false, no allocations made
87139
return {}, false
88140
}
89141

@@ -97,7 +149,7 @@ resolve_deps :: proc(
97149
Resolution,
98150
bool,
99151
) {
100-
res := resolution_make(allocator)
152+
res := resolution_make(target, allocator)
101153

102154
arch, arch_ok := config.get_arch()
103155
if !arch_ok {
@@ -235,22 +287,37 @@ fetch_and_parse_template :: proc(
235287
resolution_print :: proc(r: ^Resolution) {
236288
fmt.println()
237289

238-
// Separate VUP and official packages
239-
vup_pkgs: [dynamic]string
240-
official_pkgs: [dynamic]string
290+
// Check if the target package itself is already installed
291+
target_satisfied := false
292+
for name in r.satisfied {
293+
if name == r.target {
294+
target_satisfied = true
295+
break
296+
}
297+
}
298+
299+
// If target is satisfied and nothing to install/build, just say package is installed
300+
if target_satisfied && len(r.to_install) == 0 && len(r.to_build) == 0 {
301+
fmt.printf("Package already installed: %s\n", r.target)
302+
return
303+
}
241304

305+
// Separate VUP packages and official dependencies to install
306+
vup_pkgs: [dynamic]string
307+
official_deps_to_install: [dynamic]string
242308

243309
for pkg in r.to_install {
244310
if pkg.source == .VUP {
245311
append(&vup_pkgs, pkg.name)
246312
} else {
247-
append(&official_pkgs, pkg.name)
313+
append(&official_deps_to_install, pkg.name)
248314
}
249315
}
250316

317+
// VUP packages to install
251318
if len(vup_pkgs) > 0 {
252319
fmt.printf("VUP packages (%d):\n", len(vup_pkgs))
253-
fmt.print(" ")
320+
fmt.print(" ")
254321
for name, i in vup_pkgs {
255322
if i > 0 {
256323
fmt.print(" ")
@@ -261,46 +328,59 @@ resolution_print :: proc(r: ^Resolution) {
261328
fmt.println()
262329
}
263330

264-
if len(official_pkgs) > 0 {
265-
fmt.printf("Official deps (%d):\n", len(official_pkgs))
266-
fmt.print(" ")
267-
for name, i in official_pkgs {
331+
// Build from source
332+
if len(r.to_build) > 0 {
333+
fmt.printf("Build from source (%d):\n", len(r.to_build))
334+
fmt.print(" ")
335+
for pkg, i in r.to_build {
268336
if i > 0 {
269337
fmt.print(" ")
270338
}
271-
fmt.print(name)
339+
fmt.print(pkg.name)
272340
}
273341
fmt.println()
342+
fmt.println()
274343
}
275344

276-
if len(r.to_build) > 0 {
277-
fmt.printf("Build from source (%d):\n", len(r.to_build))
278-
fmt.print(" ")
279-
for pkg, i in r.to_build {
345+
// Dependencies to install from official repos
346+
if len(official_deps_to_install) > 0 {
347+
fmt.printf("Dependencies to install (%d):\n", len(official_deps_to_install))
348+
fmt.print(" ")
349+
for name, i in official_deps_to_install {
280350
if i > 0 {
281351
fmt.print(" ")
282352
}
283-
fmt.print(pkg.name)
353+
fmt.print(name)
284354
}
285355
fmt.println()
286356
fmt.println()
287357
}
288358

289-
if len(r.satisfied) > 0 {
290-
fmt.printf("Already installed (%d):\n", len(r.satisfied))
291-
fmt.print(" ")
292-
for name, i in r.satisfied {
359+
// Dependencies already satisfied (exclude target)
360+
deps_satisfied: [dynamic]string
361+
for name in r.satisfied {
362+
if name != r.target {
363+
append(&deps_satisfied, name)
364+
}
365+
}
366+
367+
if len(deps_satisfied) > 0 {
368+
fmt.printf("Dependencies already installed (%d):\n", len(deps_satisfied))
369+
fmt.print(" ")
370+
for name, i in deps_satisfied {
293371
if i > 0 {
294372
fmt.print(" ")
295373
}
296374
fmt.print(name)
297375
}
298376
fmt.println()
377+
fmt.println()
299378
}
300379

380+
// Missing/unresolvable
301381
if len(r.missing) > 0 {
302382
fmt.printf("Missing (%d):\n", len(r.missing))
303-
fmt.print(" ")
383+
fmt.print(" ")
304384
for name, i in r.missing {
305385
if i > 0 {
306386
fmt.print(" ")

vuru/src/core/resolve/types.odin

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package resolve
22

33
import "core:mem"
4+
import "core:strings"
45

56
import errors "../../core/errors"
67
import template "../../core/template"
@@ -26,6 +27,9 @@ Resolved_Package :: struct {
2627

2728
// Resolution result
2829
Resolution :: struct {
30+
// The target package being resolved
31+
target: string,
32+
2933
// Packages to install from binary repos (in dependency order)
3034
to_install: [dynamic]Resolved_Package,
3135

@@ -90,13 +94,19 @@ resolution_free :: proc(r: ^Resolution) {
9094
}
9195
delete(r.missing)
9296

97+
// Free target
98+
if len(r.target) > 0 {
99+
delete(r.target, r.allocator)
100+
}
101+
93102
// Errors are temp-based views, ctx is not owned - just free the array
94103
delete(r.errors)
95104
}
96105

97106
// Create a new empty Resolution
98-
resolution_make :: proc(allocator := context.allocator) -> Resolution {
107+
resolution_make :: proc(target: string = "", allocator := context.allocator) -> Resolution {
99108
return Resolution {
109+
target = strings.clone(target, allocator) if len(target) > 0 else "",
100110
to_install = make([dynamic]Resolved_Package, allocator),
101111
to_build = make([dynamic]Resolved_Package, allocator),
102112
satisfied = make([dynamic]string, allocator),

vuru/src/core/xbps/common.odin

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "core:strings"
55
// Common utilities for XBPS operations
66

77
import "core:mem"
8+
import "../../utils"
89

910
// Type alias for command runner functions
1011
Command_Runner :: proc(args: []string) -> int
@@ -37,12 +38,53 @@ build_args_with_yes :: proc(
3738
return result
3839
}
3940

40-
// Parse "pkgname-version" format into (name, version)
41+
// Parse "pkgname-version" format into (name, version) using xbps-uhelper
42+
// This correctly handles package names with dashes (e.g., visual-studio-code-insiders-1.102.0.20250116_1)
4143
parse_pkgver :: proc(pkgver: string) -> (name: string, version: string, ok: bool) {
4244
if len(pkgver) == 0 {
4345
return "", "", false
4446
}
4547

48+
// Use xbps-uhelper getpkgname to correctly parse the package name
49+
name_output, name_ok := utils.run_command_output(
50+
{"xbps-uhelper", "getpkgname", pkgver},
51+
context.temp_allocator,
52+
)
53+
if !name_ok {
54+
// Fall back to simple parsing if xbps-uhelper fails
55+
return parse_pkgver_simple(pkgver)
56+
}
57+
58+
parsed_name := strings.trim_space(name_output)
59+
if len(parsed_name) == 0 {
60+
return parse_pkgver_simple(pkgver)
61+
}
62+
63+
// Use xbps-uhelper getpkgversion to correctly parse the version
64+
ver_output, ver_ok := utils.run_command_output(
65+
{"xbps-uhelper", "getpkgversion", pkgver},
66+
context.temp_allocator,
67+
)
68+
if !ver_ok {
69+
return parse_pkgver_simple(pkgver)
70+
}
71+
72+
parsed_version := strings.trim_space(ver_output)
73+
if len(parsed_version) == 0 {
74+
return parse_pkgver_simple(pkgver)
75+
}
76+
77+
return parsed_name, parsed_version, true
78+
}
79+
80+
// Parse "pkgname-version" format into (name, version) using simple string splitting
81+
// NOTE: This is a fallback and does NOT work correctly for packages with dashes in their names!
82+
// Prefer parse_pkgver which uses xbps-uhelper for correct parsing.
83+
parse_pkgver_simple :: proc(pkgver: string) -> (name: string, version: string, ok: bool) {
84+
if len(pkgver) == 0 {
85+
return "", "", false
86+
}
87+
4688
idx := strings.last_index(pkgver, "-")
4789
if idx <= 0 || idx >= len(pkgver) - 1 {
4890
return "", "", false

0 commit comments

Comments
 (0)