Skip to content

build: make all 76 utilities compile against current V - #195

Merged
medvednikov merged 1 commit into
vlang:mainfrom
metif12:fix/build-on-latest-v
Oct 3, 2026
Merged

medvednikov merged 1 commit into
vlang:mainfrom
metif12:fix/build-on-latest-v

Conversation

@metif12

@metif12 metif12 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

This is a prerequisite for everything else: main currently does not build, so every
CI job has been red since 2025-12-21 (last green run before that was the
Timeout implementation merge).

$ v run build.vsh
failed    cmd: v -Wimpure-v -o bin/expand ./expand
expand/expand.v:11:17: error: if condition must be `bool`, not `int`
...
$ make testfmt
.../src/shuf/shuf.v is not vfmt'ed
Encountered a total of: 14 formatting errors.

9 of the 76 utilities failed to compile, and v fmt -verify . rejected 14 files.
With this change all 76 build and make testfmt passes.

Compile fixes

Cause Utilities
v.mathutil removed; min/max moved to math basenc, cut, tail
for ; cond; post {} / for i := n; i; i-- {} now need a bool condition cksum, expand
time.sleep takes a time.Duration sleep
const cannot hold a function result printenv
?(...) returns yield no err, but the callers read one cp, mv, head
C.statvfs already declared privately by vlib/builtin/cfns.c.v stat
struct utmpx declared twice with different layouts users
$embed_file no longer resolves inside a function body stat

Each is a one-line-or-so change with no behaviour change. Two deserve comment:

sleep previously wrote time.sleep(seconds * time.second). The conversion is
now explicit and saturates at max_duration, so sleep inf keeps waiting
instead of overflowing to roughly zero.

cp/mv/head build their settings with ?, but every failure path in those
functions calls the @[noreturn] common.exit_with_error_message. They can
never return an error, so the callers' or { ... err.msg() } was dead code that
could not compile. They now return plain tuples.

stat

stat needed more than a compile fix.

It hung the compiler. stat_to_vlib.v (now modes.v) imported
v.scanner and v.pref to decode --printf escapes. Building src/stat
pulled in the whole V compiler: v build never finished, and it took the
machine down with it. Measured directly:

before:  timeout after 150s, no output
after:   compiles in 2s

The escapes are now decoded by a small purpose-built routine, which also picks
up the \NNN and \xHH forms GNU supports and the old version silently
dropped.

It segfaulted on every call. statx() wrote the kernel's 224-byte
struct statx through a voidptr into a V struct of 160 bytes, so the write
ran past the end of the allocation. struct statvfs has the same problem; its
V mirror is now the full ABI including the trailing spare. The C structs are
declared and used for real now.

<bits/statx.h> is glibc-internal, so it is missing on musl. This is
issue #145; the kernel uapi <linux/stat.h> is used instead.

Spot-checked against GNU coreutils 9.4:

$ stat -c '%n %s %F' /tmp/sc.txt      $ ./bin/stat -c '%n %s %F' /tmp/sc.txt
/tmp/sc.txt 6 regular file            /tmp/sc.txt 6 regular file

$ stat --printf 'A\tB\x41C\101D\n' …  identical
$ stat -c '%N' /tmp/sc_link           identical

wc

read_chunk returned ?FileChunk while its caller inspected err, which does
not exist for an option. os.File.read also signals end of file with an
os.Eof error, and a zero-length read indexed buffer[-1], so wc on an
empty file would have panicked. It now returns !FileChunk and separates
exhaustion from a real read error. Also: \r was matched twice in the same
match, and byte(bool) is no longer a valid conversion.

Output now matches GNU for empty files, stdin, every count flag, multi-file
column alignment, and the missing-file error.

Formatting

v fmt -w . over the 16 files v fmt -verify rejected.

Known remaining failures

make test still fails, in 10 files. These are pre-existing logic bugs, not
build regressions, and each is independent:

Test file Problem
groups_nix_test.v, id_nix_test.v group lookup returns garbage ids and repeats root four times
numfmt_test.v 2K renders 2000.0; --to=none gives 2e+06
truncate_test.v 24G overflows to -1769803776 (32-bit size)
shuf_test.v issue #189, --random-source does not reproduce GNU's permutation
stat_nix_test.v one format line differs by 6 bytes
fmt_test.v, tail_test.v, string_to_i64_test.v, sum_test.v not yet diagnosed

I would rather land the build fix on its own and take these as separate PRs, so
each is reviewable. Happy to keep going in this branch instead if you prefer.

Testing

  • v run build.vsh — all 76 utilities build, 0 failures
  • make testfmt — passes
  • make test — 37/47 test files pass (was: 9 utilities could not build at all,
    so their tests could not run at all)

Verified on V 0.5.2 (0dc6a69, upstream vlang/v master) on Ubuntu 24.04 with
GNU coreutils 9.4, which is the same V the CI checks out.

main does not build with V 0.5.2, and `make testfmt` is red too, so every
CI job has been failing. This gets both gates back to green without changing
any intended behaviour.

Compile fixes, one per cause:

- `v.mathutil` is gone; `min`/`max` now live in `math`. Affects basenc, cut
  and tail.
- `for ; cond; post {}` and `for i := n; i; i-- {}` are no longer accepted:
  the condition has to be `bool`. Affects cksum and expand.
- `time.sleep` takes a `time.Duration`, not a number of seconds. sleep now
  converts explicitly and saturates instead of overflowing, so `sleep inf`
  keeps waiting rather than wrapping around to roughly zero.
- `const` cannot hold the result of a function call, so printenv's NUL
  terminator became a function.
- `setup_cp_command`/`setup_mv_command`/`setup_command` (head) were declared
  with `?`, but every failure path in them calls the @[noreturn]
  `common.exit_with_error_message`, so they never return an error and their
  callers had no `err` to look at. They now return plain tuples.
- `C.statvfs` is already declared privately by vlib/builtin/cfns.c.v, so a
  local declaration resolved to that one and failed to compile. stat binds a
  distinct name to the libc call with #define.
- `struct utmpx` was declared twice, in common/readutmp_nix.c.v and again in
  src/users/users.c.v with a different, shorter field list, which V now
  rejects as a redeclaration. Declared once, with the full layout.

stat additionally needed more than a compile fix:

- src/stat/stat_to_vlib.v (now src/stat/modes.v) imported v.scanner and
  v.pref to decode --printf escapes. Building it pulled the whole V compiler
  into the program: `v build` of src/stat never finished, and taking the
  machine down with it. The escapes are now decoded directly.
- `<bits/statx.h>` is a glibc internal header, so it is absent on musl based
  distributions (issue vlang#145). The kernel uapi `<linux/stat.h>` is used instead.
- `statx()` wrote the kernel's 224 byte struct through a `voidptr` into a
  V struct that was 160 bytes, so every call corrupted memory past the end of
  the allocation; stat segfaulted on every invocation. The C struct is now
  declared and used for real. `struct statvfs` has the same problem and its
  V mirror now matches the ABI including the trailing spare.
- `$embed_file` no longer resolves inside a function body, so fstypes.txt is
  embedded at module scope.

wc needed a fix of its own:

- `read_chunk` returned `?FileChunk`, but its caller inspected `err`, which
  does not exist for an option. os.File.read also reports end of file as an
  `os.Eof` error, and reading a zero-length chunk indexed `buffer[-1]`, so
  `wc` on an empty file would have panicked. It now returns `!FileChunk` and
  distinguishes exhaustion from a real read error.
- `\r` was matched both as "ignore" and as whitespace, which V rejects as a
  duplicate case.
- `byte(bool)` is no longer a valid conversion; the single-count check counts
  the selected options instead.

Finally, `v fmt -w .` over the 16 files that `v fmt -verify` rejected.

With this, `make` builds all 76 utilities and `make testfmt` passes.
@medvednikov
medvednikov merged commit 0cc1d27 into vlang:main Oct 3, 2026
JalonSolov pushed a commit that referenced this pull request Oct 4, 2026
…#196)

* build: make all 76 utilities compile against current V

main does not build with V 0.5.2, and `make testfmt` is red too, so every
CI job has been failing. This gets both gates back to green without changing
any intended behaviour.

Compile fixes, one per cause:

- `v.mathutil` is gone; `min`/`max` now live in `math`. Affects basenc, cut
  and tail.
- `for ; cond; post {}` and `for i := n; i; i-- {}` are no longer accepted:
  the condition has to be `bool`. Affects cksum and expand.
- `time.sleep` takes a `time.Duration`, not a number of seconds. sleep now
  converts explicitly and saturates instead of overflowing, so `sleep inf`
  keeps waiting rather than wrapping around to roughly zero.
- `const` cannot hold the result of a function call, so printenv's NUL
  terminator became a function.
- `setup_cp_command`/`setup_mv_command`/`setup_command` (head) were declared
  with `?`, but every failure path in them calls the @[noreturn]
  `common.exit_with_error_message`, so they never return an error and their
  callers had no `err` to look at. They now return plain tuples.
- `C.statvfs` is already declared privately by vlib/builtin/cfns.c.v, so a
  local declaration resolved to that one and failed to compile. stat binds a
  distinct name to the libc call with #define.
- `struct utmpx` was declared twice, in common/readutmp_nix.c.v and again in
  src/users/users.c.v with a different, shorter field list, which V now
  rejects as a redeclaration. Declared once, with the full layout.

stat additionally needed more than a compile fix:

- src/stat/stat_to_vlib.v (now src/stat/modes.v) imported v.scanner and
  v.pref to decode --printf escapes. Building it pulled the whole V compiler
  into the program: `v build` of src/stat never finished, and taking the
  machine down with it. The escapes are now decoded directly.
- `<bits/statx.h>` is a glibc internal header, so it is absent on musl based
  distributions (issue #145). The kernel uapi `<linux/stat.h>` is used instead.
- `statx()` wrote the kernel's 224 byte struct through a `voidptr` into a
  V struct that was 160 bytes, so every call corrupted memory past the end of
  the allocation; stat segfaulted on every invocation. The C struct is now
  declared and used for real. `struct statvfs` has the same problem and its
  V mirror now matches the ABI including the trailing spare.
- `$embed_file` no longer resolves inside a function body, so fstypes.txt is
  embedded at module scope.

wc needed a fix of its own:

- `read_chunk` returned `?FileChunk`, but its caller inspected `err`, which
  does not exist for an option. os.File.read also reports end of file as an
  `os.Eof` error, and reading a zero-length chunk indexed `buffer[-1]`, so
  `wc` on an empty file would have panicked. It now returns `!FileChunk` and
  distinguishes exhaustion from a real read error.
- `\r` was matched both as "ignore" and as whitespace, which V rejects as a
  duplicate case.
- `byte(bool)` is no longer a valid conversion; the single-count check counts
  the selected options instead.

Finally, `v fmt -w .` over the 16 files that `v fmt -verify` rejected.

With this, `make` builds all 76 utilities and `make testfmt` passes.

* fix: repair the 10 test files that were failing for unrelated reasons

Follow-up to #195, which got the build and the formatting gate green. This
brings `make test` from 37/47 to 46/47. The only remaining failure is
shuf_test.v, which is issue #189 and gets its own PR.

## common/pwd: group ids came back as garbage

getgrouplist() and getgroups() fill an array of gid_t, which is 32 bits wide,
but the V declarations handed them a []int, i.e. i64. The kernel wrote the low
half of each slot and left the upper half holding the previous entry, so gids
arrived as 103079215108 (24 * 2^32 + 4, i.e. adm's 4 with cdrom's 24 above it).
The two functions now fill a []u32.

getgrgid() was also declared as returning `&C.passwd` rather than
`&C.group`. That happened to work because gr_name sits at the same offset as
pw_name, but gr_mem/gr_gid do not line up, so there is now a proper struct.

groups and id now agree with GNU for the plain, named-user, -nG and -G forms.

## tail: KiB was decimal, and PB/EB used the binary multiplier

The suffix table mapped both `kb` and `kib` to 1000, both `mb` and `mib` to
1000^2, and so on, so `tail -c 1KiB` read a third of what it should have.
`petabyte` was also `terra * kilo` and `exabyte` `peta * kilo`, i.e. the binary
multipliers, instead of continuing the decimal chain. The two chains are now
separate and correct, and the test asserts GNU's actual values rather than
mirroring the constants.

The overflow check was `result == 0 && number != 0`, which cannot catch a
wrapped product; it now checks that the division comes back out. `20E` is
rejected like GNU rejects it. Z/Y/R/Q stay unsupported, because although
GNU's help mentions them, GNU itself rejects all of them with "Value too large
for defined data type" (checked against 9.4).

Verified end to end against GNU for 2b, k, K, KB, KiB, M, MB, MiB, G, GB, GiB,
T, TB, TiB, P, PB, PiB, E, EB and EiB, plus the rejection cases.

## numfmt: 2000.0 and 2e+06

f64.str() always keeps a fractional part and switches to scientific notation
above a million, so `--to=none 2000000` printed `2e+06` and `2K` printed
`2000.0`. Both now go through a helper based on strlong() that drops a trailing
`.0`.

## stat: the default Device: field

The default format used `%Dh/%dd`, which printed the device number in hex over
the same number in decimal: `830h/2096d` where GNU prints `8,48`. GNU's
default is `Device: %Hd,%Ld`, and %H/%L are two character tokens that
scan_for_tokens() did not handle; there was already a TODO about it. Both
tokens are implemented and the default format matches.

## Three tests were asserting the wrong thing, or crashing

* `tail_test` and `fmt_test` captured `&result` for a local in `setup()` and
  returned the closure. Once setup() returned, the pointer dangled and the
  closure appended to freed stack memory:
  `malloc_uninit(0xd0a07408218d1b1f < 0)`. The array header is now heap
  allocated. (cut_test and paste_test use the same shape but call the closure
  in scope, so they are fine.)
* `string_to_i64_test` used `!` on an option return, which does not compile.
* `sum_test` had `const util` next to `import io.util`. The import is aliased.
* `truncate_test` computed its expectations as `24 * 1000 * 1000 * 1000`. V's
  `int` is 32-bit, so the expectation wrapped to -1769803776 while the value
  under test, a u64, was right all along. The expected values are now u64.

## Testing

`v run build.vsh` builds all 76 utilities, `v fmt -verify .` passes, and
`v test .` reports 46 of 47 files passing, up from 37.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants