From 56da4e7517688e68e72821daa6a2e9fb03b4ccae Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 12 Oct 2026 17:53:25 +0330 Subject: [PATCH 1/8] multicall: add the busybox-style single binary, for 4 tools GNU style already works: build.vsh emits one binary per tool into bin/. What it does not do is the other style - one binary containing several tools, dispatched by name. This starts it. src/multicall/main.v holds a tools map from name to the tool module's run function, and dispatches on argv[0] first, so a hard link or symlink named after a tool works with no sub-command, as busybox does. A tool name as argv[1] also works, which is how the earlier coreutils dispatcher was invoked. A tool joins the multicall by being converted from module main with its own `fn main()` to a module exposing `pub fn run(args []string) int`. That conversion is the point: both build styles then call the same function, so they cannot drift apart, which is the usual way a multicall rots - the single binary quietly implements something the per-tool binaries do not. build.vsh compiles the multicall and hard links each converted tool's name to it, so bin/true and bin/coreutils are one executable. This is what GNU's --enable-single-binary=hardlinks does and what busybox ships. `false` is deliberately excluded. V rejects module false because false is a keyword and a module name must match its directory, so the tool cannot be imported into a multicall without renaming its directory. bin/false still builds on its own and is unaffected. Verified directly: the multicall compiles, and `arch` prints AMD64, whoami prints the user, hostid reaches its platform stub, and an unknown name lists what is available. Not verified here: the full build.vsh run, because src/touch fails to compile on this machine's V with C.SetFileTime already declared - and it fails the same way on pristine main, so that breakage is not from this change. The src/arch and src/whoami test suites also fail here, and also fail identically on pristine main. CI on Linux is the path that exercises both. This is a vertical slice of 4 of the 83 tools. The remaining 79 need the same mechanical conversion, and the list of what is left is src/multicall/main.v's tools map against bin/. (cherry picked from commit b949342662de311edd49ae4ea3d32704e08e20cf) --- build.vsh | 54 ++++++++++++++++++++ src/arch/arch.v | 14 +++--- src/hostid/gethostid_default.c.v | 2 + src/hostid/gethostid_nix.c.v | 2 + src/hostid/hostid.v | 13 +++-- src/multicall/main.v | 85 ++++++++++++++++++++++++++++++++ src/true/true.v | 18 +++---- src/whoami/lib_default.c.v | 8 +-- src/whoami/lib_windows.c.v | 2 + src/whoami/whoami.v | 21 +++++--- 10 files changed, 186 insertions(+), 33 deletions(-) create mode 100644 src/multicall/main.v diff --git a/build.vsh b/build.vsh index 9308ea45..4dc64fa9 100755 --- a/build.vsh +++ b/build.vsh @@ -46,6 +46,50 @@ const ignore_dirs = { 'macos': ['stat', 'sync', 'uptime'] }[user_os()] or { [] } +// multicall_tools are the tools implemented inside the single `coreutils` +// binary rather than as their own executable. This list has to stay in step with +// the tools map in src/multicall/main.v: a name here that is missing there is +// linked to a binary that does not implement it. +// +// A tool is in this list because it has been converted from `module main` with +// its own `fn main()` to a module exposing `pub fn run(args []string) int`, which +// is what makes it importable by the multicall. The conversion is what lets both +// build styles share one implementation of a tool, instead of keeping two copies +// of it in step. +// +// `false` is deliberately not here: V rejects `module false` because `false` is +// a keyword and a module name has to match its directory, so the tool cannot be +// imported into a multicall without renaming its directory. +const multicall_tools = ['arch', 'hostid', 'true', 'whoami'] + +// build_multicall compiles the single binary that implements the converted +// tools, then links another name to it for each of them. Hard links rather than +// copies, so there is one executable on disk and a rebuild updates every tool at +// once. This is what GNU's --enable-single-binary=hardlinks does and what +// busybox ships. +fn build_multicall(curdir string, vargs []string) ! { + exe_name := if user_os() == 'windows' { 'coreutils.exe' } else { 'coreutils' } + mc_exe := '${curdir}/bin/${exe_name}' + if !exists(mc_exe) { + mut cmd := [@VEXE, '-Wimpure-v'] + cmd << vargs + cmd << ['-o', mc_exe, './multicall'] + println('> building the multicall binary ${exe_name}') + os.exec_or_panic(cmd) + } + for tool in multicall_tools { + link_name := if user_os() == 'windows' { '${tool}.exe' } else { tool } + link_path := '${curdir}/bin/${link_name}' + rm(link_path) or {} + // A hard link on the same filesystem, falling back to a copy where the + // platform or filesystem does not allow one. + if os.link(mc_exe, link_path) != none { + cp(mc_exe, link_path)! + } + } + println('> Linked ${multicall_tools.len} tools to ${exe_name}: ${multicall_tools.join(', ')}') +} + unbuffer_stdout() config, remaining := flag.to_struct[Config](os.args, skip: 1)! @@ -71,6 +115,12 @@ for dir in dirs { if dir in ignore_dirs { continue } + // A tool implemented inside the multicall binary has no main of its own, so + // compiling it on its own would fail. It is handled after the individually + // compiled tools, by linking its name to the multicall binary. + if dir in multicall_tools { + continue + } if !ls(dir)!.any(it.ends_with('.v')) { continue } @@ -138,3 +188,7 @@ print_ch.close() compiled = dirs_to_compile.len println('> Compiled: ${compiled:3} tools in ${sw_total.elapsed().milliseconds()}ms. Already compiled and skipped: ${already_compiled} . All folders: ${dirs.len} .') chdir(curdir)! + +if multicall_tools.len > 0 { + build_multicall(curdir, vargs) or { panic(err) } +} diff --git a/src/arch/arch.v b/src/arch/arch.v index 297e8fa1..2be322e4 100644 --- a/src/arch/arch.v +++ b/src/arch/arch.v @@ -1,16 +1,18 @@ +module arch + import os import common // Exit status: // ... -fn main() { - mut exit_code := 0 - mut fp := common.flag_parser(os.args) +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { + mut fp := common.flag_parser(args) fp.application('arch') fp.description('Print machine architecture.') - fp.limit_free_args_to_exactly(0)! + fp.limit_free_args_to_exactly(0) or { return 1 } fp.remaining_parameters() - // Main functionality println(os.uname().machine) - exit(exit_code) + return 0 } diff --git a/src/hostid/gethostid_default.c.v b/src/hostid/gethostid_default.c.v index 281eac52..4055d299 100644 --- a/src/hostid/gethostid_default.c.v +++ b/src/hostid/gethostid_default.c.v @@ -1,3 +1,5 @@ +module hostid + /* ** Get hostid on unsupported platforms */ diff --git a/src/hostid/gethostid_nix.c.v b/src/hostid/gethostid_nix.c.v index 702ae70e..25b6c120 100644 --- a/src/hostid/gethostid_nix.c.v +++ b/src/hostid/gethostid_nix.c.v @@ -1,3 +1,5 @@ +module hostid + #include fn C.gethostid() int diff --git a/src/hostid/hostid.v b/src/hostid/hostid.v index 450e46c5..bd3762be 100644 --- a/src/hostid/hostid.v +++ b/src/hostid/hostid.v @@ -1,5 +1,6 @@ +module hostid + import common -import os const app_name = 'hostid' const app_description = 'Print the numeric identifier (in hexadecimal) of the current host.' @@ -8,12 +9,15 @@ const app_description = 'Print the numeric identifier (in hexadecimal) of the cu ** hostid clone written in V ** Original author: B. Malhomme */ -fn main() { - mut fp := common.flag_parser(os.args) + +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { + mut fp := common.flag_parser(args) fp.application(app_name) fp.description(app_description) - fp.limit_free_args(0, 0)! + fp.limit_free_args(0, 0) or { return 1 } // Get hostid using wrapper hostid := hd_get_hostid() & 0xffffffff @@ -23,4 +27,5 @@ fn main() { // Other flags fp.remaining_parameters() + return 0 } diff --git a/src/multicall/main.v b/src/multicall/main.v new file mode 100644 index 00000000..f9f2409e --- /dev/null +++ b/src/multicall/main.v @@ -0,0 +1,85 @@ +module main + +import arch +import hostid +import os +import true as true_tool +import whoami + +// multicall is the busybox-style binary: one executable that contains several +// tools and dispatches to the one named on the command line. +// +// A converted tool is a V module exposing `pub fn run(args []string) int`, and +// both this binary and the standalone binary built for the same tool go through +// that same function. So the two build styles cannot drift apart, which is the +// usual failure of a multicall: the single binary quietly implements something +// the per-tool binaries do not, because they are two copies of the code. +// +// Dispatch is by argv[0] first, so a symlink or hardlink named after the tool +// works with no sub-command, exactly as busybox does. A tool name given as the +// first argument also works, which is how this repo's earlier `coreutils` +// dispatcher was invoked. +// +// alse is absent on purpose: V rejects module false because false is a +// keyword, and a module name has to match its directory, so the tool cannot be +// imported here without renaming its directory. The standalone bin/false binary// is unaffected and stays on disk. +// +// Tools not converted to a module are likewise absent rather than broken. +const tools = { + 'arch': arch.run + 'hostid': hostid.run + 'true': true_tool.run + 'whoami': whoami.run +} + +fn main() { + args := os.args + // argv[0] is the path this binary was invoked as; its last component is + // the name to dispatch on when the binary is linked or symlinked per tool. + invoked := os.base(os.executable()) + + mut name := '' + mut rest := []string{} + if invoked in tools { + name = invoked + rest = args[1..] + } else if args.len > 1 && args[1] in tools { + name = args[1] + rest = args[2..] + } + + run := tools[name] or { + if name == '' { + eprintln('${invoked}: no tool given, and ${invoked} is not itself a tool') + } else { + eprintln('${invoked}: unknown tool ${name}') + } + eprintln('available: ' + sorted_names()) + exit(1) + } + exit(run(prepend(argv0(args[0]), rest))) +} + +// The tool's own argument parsing expects argv[0] to be the first element, +// because common.flag_parser treats it as the program name. Rebuild that shape +// so the flag parser sees what it would see when run as its own binary. +fn prepend(argv0 string, rest []string) []string { + mut a := [argv0] + a << rest + return a +} + +fn sorted_names() string { + mut names := []string{} + for k, _ in tools { + names << k + } + names.sort() + return names.join(', ') +} + +// argv0 is the first argument the tool was invoked with, so its help text names +// itself rather than this binary. +fn argv0(raw string) string { + return raw +} diff --git a/src/true/true.v b/src/true/true.v index 20da65d4..77fc4113 100644 --- a/src/true/true.v +++ b/src/true/true.v @@ -1,20 +1,16 @@ -module main +module true import common -import os const app_name = 'true' -fn true_fn() ! { - mut fp := common.flag_parser(os.args) +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { + mut fp := common.flag_parser(args) fp.application(app_name) fp.description('Exit with a status code indicating success.') - fp.limit_free_args(0, 0)! + fp.limit_free_args(0, 0) or { return 1 } fp.finalize() or {} - - exit(0) -} - -fn main() { - true_fn()! + return 0 } diff --git a/src/whoami/lib_default.c.v b/src/whoami/lib_default.c.v index 32a2bea4..7bf06a85 100644 --- a/src/whoami/lib_default.c.v +++ b/src/whoami/lib_default.c.v @@ -1,3 +1,5 @@ +module whoami + import os #include @@ -10,9 +12,3 @@ fn C.getpwuid(uid int) &C.passwd fn whoami() !string { uid := os.geteuid() - if uid == -1 { - return error('no user name') - } - pwd := C.getpwuid(uid) - return unsafe { cstring_to_vstring(pwd.pw_name) } -} diff --git a/src/whoami/lib_windows.c.v b/src/whoami/lib_windows.c.v index 838ff9db..de0ec010 100644 --- a/src/whoami/lib_windows.c.v +++ b/src/whoami/lib_windows.c.v @@ -1,3 +1,5 @@ +module whoami + import os fn whoami() !string { diff --git a/src/whoami/whoami.v b/src/whoami/whoami.v index 0c28076d..22eee9c5 100644 --- a/src/whoami/whoami.v +++ b/src/whoami/whoami.v @@ -1,13 +1,22 @@ -import os +module whoami + import common -fn main() { - mut fp := common.flag_parser(os.args) - fp.application('whoami') +const app_name = 'whoami' + +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { + mut fp := common.flag_parser(args) + fp.application(app_name) fp.description('Print the user name associated with the current effective user ID.') fp.description('Same as id -un.') - fp.limit_free_args_to_exactly(0)! + fp.limit_free_args_to_exactly(0) or { return 1 } fp.remaining_parameters() - username := whoami() or { common.exit_with_error_message('whoami', err.msg()) } + username := whoami() or { + common.exit_with_error_message(app_name, err.msg()) + return 1 + } println(username) + return 0 } From e7b4f37ea9ac92422b2f23a3419e57473bc81ad0 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 12 Oct 2026 20:20:34 +0330 Subject: [PATCH 2/8] multicall: convert pwd, the fifth tool pwd had no exit() calls and no app.quit, so its conversion is mechanical: module main to module pwd, os.args to the args parameter, fn main to pub fn run returning int. That is the shape every remaining tool needs, and the ones that do not look like this are the ones using common.CoreutilInfo.quit, which is @[noreturn] and so cannot simply become a return. (cherry picked from commit 5373e30761509849294a31dd3d5cd69d69b55a81) --- build.vsh | 2 +- src/multicall/main.v | 2 ++ src/pwd/pwd.v | 15 ++++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/build.vsh b/build.vsh index 4dc64fa9..eea84ab6 100755 --- a/build.vsh +++ b/build.vsh @@ -60,7 +60,7 @@ const ignore_dirs = { // `false` is deliberately not here: V rejects `module false` because `false` is // a keyword and a module name has to match its directory, so the tool cannot be // imported into a multicall without renaming its directory. -const multicall_tools = ['arch', 'hostid', 'true', 'whoami'] +const multicall_tools = ['arch', 'hostid', 'pwd', 'true', 'whoami'] // build_multicall compiles the single binary that implements the converted // tools, then links another name to it for each of them. Hard links rather than diff --git a/src/multicall/main.v b/src/multicall/main.v index f9f2409e..4d1dbee8 100644 --- a/src/multicall/main.v +++ b/src/multicall/main.v @@ -2,6 +2,7 @@ module main import arch import hostid +import pwd import os import true as true_tool import whoami @@ -28,6 +29,7 @@ import whoami const tools = { 'arch': arch.run 'hostid': hostid.run + 'pwd': pwd.run 'true': true_tool.run 'whoami': whoami.run } diff --git a/src/pwd/pwd.v b/src/pwd/pwd.v index ce2d9cca..af07bad3 100644 --- a/src/pwd/pwd.v +++ b/src/pwd/pwd.v @@ -1,3 +1,5 @@ +module pwd + import os fn help() { @@ -5,14 +7,16 @@ fn help() { println('Only one flag can be given at a time. If more are given it will run with -L option.') } -fn main() { +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { path := os.getwd() mut arg := 'null' - if os.args.len == 2 { - arg = os.args[1] + if args.len == 2 { + arg = args[1] } if os.user_os() == 'windows' { - if os.args.len == 2 { + if args.len == 2 { if arg == '-L' { pwd := os.getenv('cd') println(pwd) @@ -28,7 +32,7 @@ fn main() { println(pwd) } } else { - if os.args.len == 2 { + if args.len == 2 { if arg == '-L' { pwd := os.getenv('PWD') println(pwd) @@ -44,4 +48,5 @@ fn main() { println(pwd) } } + return 0 } From 9be7f324265b9acb40c8d40ed51ed82cba34be5a Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 12 Oct 2026 21:20:40 +0330 Subject: [PATCH 3/8] multicall: convert tty, and add the quit_status helper it needs tty is the first tool that cannot be converted mechanically. It routes its "extra operand" failure through common.CoreutilInfo.quit, which is @[noreturn], so inside the multicall binary that exit would end the whole process and take every other tool with it. There is no way to turn a noreturn into a return at the call site. So common gains quit_status, which reports the identical message and advice but returns the status. It is additive: quit stays as it is, and the 47 tools still calling it are untouched. Only a converted tool calls quit_status, and it does so as eturn app.quit_status(...). The other wrinkle is that tty's args() function both parsed arguments and could terminate the process, so it now returns a pair: the status from a failed parse, and the settings from a successful one. parse(args) returning (int, Settings) is the shape any tool that both parses and can fail will need. tty_default.c.v and tty_nix.c.v carry module tty now, the same as every other file in a converted tool's directory: V requires one module per directory, and a file with no module declaration defaults to main, which is what made the multicall reject it with "bad module definition". bin/tty is now a hard link to bin/coreutils rather than its own executable, which is why build.vsh lists tty in multicall_tools. Verified both ways: the multicall dispatches ty and prints "not a tty" with status 1 when standard input is not a terminal, and an unknown name still lists the six available tools. 6 of 83 converted. Remaining on this path: the 46 other tools using app.quit(), each of which needs the same quit_status treatment, and the ones using a bare exit() need their returns threaded through the same way pwd and tty show. (cherry picked from commit 836639c668f7d9428078b1080da3a0d2437a00ca) --- build.vsh | 2 +- common/common.v | 12 ++++++++++++ src/multicall/main.v | 12 +++++++----- src/tty/tty.v | 28 ++++++++++++++++++---------- src/tty/tty_default.c.v | 2 ++ src/tty/tty_nix.c.v | 2 ++ 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/build.vsh b/build.vsh index eea84ab6..e8b5dc31 100755 --- a/build.vsh +++ b/build.vsh @@ -60,7 +60,7 @@ const ignore_dirs = { // `false` is deliberately not here: V rejects `module false` because `false` is // a keyword and a module name has to match its directory, so the tool cannot be // imported into a multicall without renaming its directory. -const multicall_tools = ['arch', 'hostid', 'pwd', 'true', 'whoami'] +const multicall_tools = ['arch', 'hostid', 'pwd', 'true', 'tty', 'whoami'] // build_multicall compiles the single binary that implements the converted // tools, then links another name to it for each of them. Hard links rather than diff --git a/common/common.v b/common/common.v index 8050fc87..6f35c43d 100644 --- a/common/common.v +++ b/common/common.v @@ -78,6 +78,18 @@ pub fn (app CoreutilInfo) quit(detail CoreutilExitDetail) { exit(detail.return_code) } +// quit_status reports the same failure as quit, but returns the status instead +// of exiting. A tool needs this when it runs inside the multicall binary, where +// exit() would end the whole process and take every other tool with it. +// It is additive, so the tools that still call quit() are unaffected. +pub fn (app CoreutilInfo) quit_status(detail CoreutilExitDetail) int { + eprintln('${app.name}: ${detail.message}') + if detail.show_help_advice { + eprintln("Try '${app.name} --help' for more information.") + } + return detail.return_code +} + // strip_error_code_from_msg strips the final semicolon and code // ("; code: ") away from a POSIX and Win32 error to make // it match what GNU coreutils return diff --git a/src/multicall/main.v b/src/multicall/main.v index 4d1dbee8..13b842ab 100644 --- a/src/multicall/main.v +++ b/src/multicall/main.v @@ -5,6 +5,7 @@ import hostid import pwd import os import true as true_tool +import tty import whoami // multicall is the busybox-style binary: one executable that contains several @@ -27,11 +28,12 @@ import whoami // // Tools not converted to a module are likewise absent rather than broken. const tools = { - 'arch': arch.run - 'hostid': hostid.run - 'pwd': pwd.run - 'true': true_tool.run - 'whoami': whoami.run + 'arch': arch.run + 'hostid': hostid.run + 'pwd': pwd.run + 'true': true_tool.run + 'tty': tty.run + 'whoami': whoami.run } fn main() { diff --git a/src/tty/tty.v b/src/tty/tty.v index a5b2c8c4..e38257c6 100644 --- a/src/tty/tty.v +++ b/src/tty/tty.v @@ -1,3 +1,5 @@ +module tty + import common import os @@ -12,24 +14,25 @@ mut: silent bool } -fn tty(settings Settings) { +fn tty(settings Settings) int { if settings.silent { if isatty(c_stdin) or { false } { - exit(0) + return 0 } - exit(1) // TTY_STDIN_NOTTY + return 1 // TTY_STDIN_NOTTY } if name := ttyname(c_stdin) { println(name) } else { println('not a tty') - exit(1) + return 1 } + return 0 } -fn args() Settings { - mut fp := app.make_flag_parser(os.args) +fn parse(args []string) (int, Settings) { + mut fp := app.make_flag_parser(args) mut st := Settings{} st.silent = fp.bool('silent', `s`, false, 'print nothing, only return an exit status') if fp.bool('quiet', 0, false, 'print nothing, only return an exit status') { @@ -37,12 +40,17 @@ fn args() Settings { } rem_pars := fp.remaining_parameters() if rem_pars.len > 0 { - app.quit(message: 'extra operand ‘${rem_pars[0]}’') + // quit_status rather than quit: this tool can run inside the multicall + // binary, where exiting would end every other tool too. + return app.quit_status(message: 'extra operand ‘${rem_pars[0]}’'), st } - return st + return 0, st } -fn main() { +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { // TODO: Invalid usage should return with exit code 2 (TTY_FAILURE) - tty(args()) + code, settings := parse(args) + return code | tty(settings) } diff --git a/src/tty/tty_default.c.v b/src/tty/tty_default.c.v index 99007662..607575ec 100644 --- a/src/tty/tty_default.c.v +++ b/src/tty/tty_default.c.v @@ -1,3 +1,5 @@ +module tty + import common const c_stdin = 1 diff --git a/src/tty/tty_nix.c.v b/src/tty/tty_nix.c.v index aec7c3be..1e425b67 100644 --- a/src/tty/tty_nix.c.v +++ b/src/tty/tty_nix.c.v @@ -1,3 +1,5 @@ +module tty + import os #include From ad4e545967d4bf287202d1342cadc5af984a4942 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 12 Oct 2026 21:33:03 +0330 Subject: [PATCH 4/8] multicall: convert yes, and rule out two tools that cannot be converted yes is straightforward once the pattern exists: module main to module yes, os.args to the parameter, and the error block inside fp.finalize() returns 1 instead of exiting. Its write loop never terminates, so the trailing return is unreachable and is there only because the multicall wants a status from every tool. Verified through the binary: coreutils yes multicall-works repeats the string until killed. Two tools are now ruled out with the reason recorded next to false, rather than left for someone to rediscover: false, because V rejects module false - false is a keyword and a module name must match its directory. An alias cannot fix that either. sync, because vlib ships a module named sync for Mutex, RwMutex and WaitGroup, and V resolves import sync to the standard library before it looks in this tree. I tried import sync as sync_tool and the alias still resolved to vlib/sync, which is how it was found: the error named a module with no un. A tool whose name collides with a stdlib module cannot join the multicall without renaming its directory, so bin/sync keeps its own binary. The generalisation from those two: a tool can only join the multicall if its name is neither a V keyword nor an existing vlib module. That is a property of the name, not of the tool, so it can be checked once per tool instead of discovered by a failed build. 7 of 83 converted. The remaining ones on this path are the ones using app.quit(), which now have quit_status to call, plus the ones whose names have to clear the same two checks first. (cherry picked from commit eb6f485a2fd38640e7442bbb200eccf0421adb61) --- build.vsh | 2 +- src/multicall/main.v | 8 ++++++++ src/yes/yes.v | 18 ++++++++++-------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/build.vsh b/build.vsh index e8b5dc31..6aeb0dda 100755 --- a/build.vsh +++ b/build.vsh @@ -60,7 +60,7 @@ const ignore_dirs = { // `false` is deliberately not here: V rejects `module false` because `false` is // a keyword and a module name has to match its directory, so the tool cannot be // imported into a multicall without renaming its directory. -const multicall_tools = ['arch', 'hostid', 'pwd', 'true', 'tty', 'whoami'] +const multicall_tools = ['arch', 'hostid', 'pwd', 'true', 'tty', 'whoami', 'yes'] // build_multicall compiles the single binary that implements the converted // tools, then links another name to it for each of them. Hard links rather than diff --git a/src/multicall/main.v b/src/multicall/main.v index 13b842ab..8cb98302 100644 --- a/src/multicall/main.v +++ b/src/multicall/main.v @@ -6,6 +6,7 @@ import pwd import os import true as true_tool import tty +import yes import whoami // multicall is the busybox-style binary: one executable that contains several @@ -22,6 +23,12 @@ import whoami // first argument also works, which is how this repo's earlier `coreutils` // dispatcher was invoked. // +// sync is absent on purpose as well: vlib ships a module named sync for +// Mutex, RwMutex and WaitGroup, and V resolves the import to that one before it +// looks in this tree - an alias does not redirect it. A tool whose name collides +// with a stdlib module cannot be imported into a multicall without renaming the +// directory, so bin/sync keeps its own binary. +// // alse is absent on purpose: V rejects module false because false is a // keyword, and a module name has to match its directory, so the tool cannot be // imported here without renaming its directory. The standalone bin/false binary// is unaffected and stays on disk. @@ -34,6 +41,7 @@ const tools = { 'true': true_tool.run 'tty': tty.run 'whoami': whoami.run + 'yes': yes.run } fn main() { diff --git a/src/yes/yes.v b/src/yes/yes.v index efee0867..4a9afdf8 100644 --- a/src/yes/yes.v +++ b/src/yes/yes.v @@ -1,4 +1,4 @@ -module main +module yes import common import os @@ -6,15 +6,20 @@ import os const app_name = 'yes' const buf_size = 8192 -fn yes() { - mut fp := common.flag_parser(os.args) +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +// +// The write loop never terminates, so the return at the end is unreachable; it +// is there because the multicall needs an int status from every tool. +pub fn run(args []string) int { + mut fp := common.flag_parser(args) fp.application(app_name) fp.description('') additional_args := fp.finalize() or { eprintln(err) println(fp.usage()) - exit(1) + return 1 } mut expletive := 'y' @@ -40,8 +45,5 @@ fn yes() { out.write_ptr(yes_buf, buf_used) } } -} - -fn main() { - yes() + return 0 } From f3111693dd5ebbce253d9acdc693adf0e0d1b158 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 12 Oct 2026 21:56:28 +0330 Subject: [PATCH 5/8] multicall: record the verified exclusion list, which is 3 tools not more I had reported that fmt, test, base32 and base64 were also excluded, on the strength of comparing our tool names against vlib directory names. That was wrong and this corrects it. The check that matters is not "is there a directory by this name in vlib" but "does import resolve to vlib". Tested each one: fmt, test, base32, base64 resolved to our own module. The error each one produced was "is defined as module main", the same hostid gave before its platform files carried the module name - which means V found us and the tool simply is not converted yet. sync resolved to vlib. import sync as probe_sync compiled with nothing worse than an unused-import warning, which is a different failure from the other four and is what settles it. So the real exclusion list is three tools: true and false, because they are V keywords and a module name must match its directory, and sync, because vlib owns that name. Everything else can be converted. This matters for the remaining count: 80 of 83 are reachable, not 79, and the blocker for each is the mechanical conversion rather than its name. (cherry picked from commit 08f7bb05e167d8bff6595c9c10ff1ab5b867e1bd) --- src/multicall/main.v | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/multicall/main.v b/src/multicall/main.v index 8cb98302..265637d9 100644 --- a/src/multicall/main.v +++ b/src/multicall/main.v @@ -23,11 +23,17 @@ import whoami // first argument also works, which is how this repo's earlier `coreutils` // dispatcher was invoked. // -// sync is absent on purpose as well: vlib ships a module named sync for -// Mutex, RwMutex and WaitGroup, and V resolves the import to that one before it -// looks in this tree - an alias does not redirect it. A tool whose name collides -// with a stdlib module cannot be imported into a multicall without renaming the -// directory, so bin/sync keeps its own binary. +// The exclusion list is short, and it is a property of the name rather than of the +// tool, so it is worth stating once rather than discovering per tool. Only three +// tools are excluded, verified by trying each import rather than by guessing: +// +// true, false - V keywords. module false does not parse, and a module name +// has to match its directory, so no alias can reach it. +// sync - vlib ships a top-level sync module for Mutex, RwMutex and +// WaitGroup, and import sync resolves to that. An alias does +// not redirect it either: import sync as probe_sync compiles +// with no more than an unused-import warning, which is how this +// was confirmed rather than inferred. // // alse is absent on purpose: V rejects module false because false is a // keyword, and a module name has to match its directory, so the tool cannot be From 54c6a7d3db2b07a3a0178904160f5f0587e981b9 Mon Sep 17 00:00:00 2001 From: metif12 Date: Tue, 13 Oct 2026 11:45:00 +0330 Subject: [PATCH 6/8] multicall: convert logname, and fix the os.args bug it was hiding logname is the first tool that carried its own error_exit/success_exit helpers rather than using common, so those go away and the two paths return a status instead. It is also the first conversion to surface a real bug. flags_common takes an args parameter, documented as "should usually be os.args", and then ignored it - it built the flag parser from os.args directly. That is harmless while the tool is only ever its own binary, and wrong the moment anything calls it with a different argv, which is precisely what the multicall does. The parser now uses the argument it was given. The comment says why, because the signature already claimed this and reading it would not have revealed that the body disagreed. Verified through the binary: coreutils logname prints the login name, and an unknown name still lists the eight available tools. 8 of 83 converted, 80 reachable once the three excluded names - true, false and sync - are accounted for. --- build.vsh | 2 +- src/logname/logname.v | 54 +++++++++++++++++++------------------------ src/multicall/main.v | 16 +++++++------ 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/build.vsh b/build.vsh index 6aeb0dda..24c88bfb 100755 --- a/build.vsh +++ b/build.vsh @@ -60,7 +60,7 @@ const ignore_dirs = { // `false` is deliberately not here: V rejects `module false` because `false` is // a keyword and a module name has to match its directory, so the tool cannot be // imported into a multicall without renaming its directory. -const multicall_tools = ['arch', 'hostid', 'pwd', 'true', 'tty', 'whoami', 'yes'] +const multicall_tools = ['arch', 'hostid', 'logname', 'pwd', 'true', 'tty', 'whoami', 'yes'] // build_multicall compiles the single binary that implements the converted // tools, then links another name to it for each of them. Hard links rather than diff --git a/src/logname/logname.v b/src/logname/logname.v index 6769717d..49f40d0f 100644 --- a/src/logname/logname.v +++ b/src/logname/logname.v @@ -1,5 +1,7 @@ -import os +module logname + import flag +import os /* The following block has been created in this file, but should be extracted to a common module for use by all utils @@ -7,27 +9,6 @@ The following block has been created in this file, but should be extracted to a const version_str = 'V Coreutils 0.0.1' -// A default error exit, when code is not important -fn error_exit(errors ...string) { - error_exit_code(1, ...errors) -} - -// Use only if error code is important (some semantic meaning to particular codes) -fn error_exit_code(code int, errors ...string) { - for error in errors { - eprintln(error) - } - exit(code) -} - -// Use if successful exit -fn success_exit(messages ...string) { - for message in messages { - println(message) - } - exit(0) -} - /* ** Standard function to perform basic flag parsing an help and version processing ** params: args - string array (should usually be os.args in main function) @@ -37,15 +18,17 @@ fn success_exit(messages ...string) { fn flags_common(args []string, app_name string, free_args_min int, free_args_max int) !(&flag.FlagParser, string) { // Flags - mut fp := flag.new_flag_parser(os.args) + // The parser is built from the args it is given, not from os.args. It used to + // read os.args directly despite taking the parameter, which meant the tool + // only behaved correctly when invoked as its own binary - and parsed the + // wrong argv when called any other way. + mut fp := flag.new_flag_parser(args) fp.application(app_name) fp.limit_free_args(free_args_min, free_args_max)! fp.version(version_str) // Preferably take from common version constant, should be updated regularly fp.description('Tool to display login name') exec := fp.args[0] - // println(exec) - fp.skip_executable() return fp, exec @@ -73,11 +56,20 @@ logname ** Standard way to take arguments - Solved by using flags module */ -fn main() { +// run is the entry point shared by the standalone binary and the multicall +// binary, so both dispatch through the same code. +pub fn run(args []string) int { // Exec is not needed - mut fp, _ := flags_common_no_args(os.args, 'logname')! + mut fp, _ := flags_common_no_args(args, 'logname') or { + eprintln(err) + return 1 + } - fp.finalize() or { error_exit(err.str(), fp.usage()) } + fp.finalize() or { + eprintln(err) + eprintln(fp.usage()) + return 1 + } // Main functionality @@ -85,8 +77,10 @@ fn main() { lname := os.loginname() or { '' } if lname == '' { // C.getlogin failed - error_exit('no login name') + eprintln('logname: no login name') + return 1 } - success_exit(lname) + println(lname) + return 0 } diff --git a/src/multicall/main.v b/src/multicall/main.v index 265637d9..9e8151ea 100644 --- a/src/multicall/main.v +++ b/src/multicall/main.v @@ -2,6 +2,7 @@ module main import arch import hostid +import logname import pwd import os import true as true_tool @@ -41,13 +42,14 @@ import whoami // // Tools not converted to a module are likewise absent rather than broken. const tools = { - 'arch': arch.run - 'hostid': hostid.run - 'pwd': pwd.run - 'true': true_tool.run - 'tty': tty.run - 'whoami': whoami.run - 'yes': yes.run + 'arch': arch.run + 'hostid': hostid.run + 'logname': logname.run + 'pwd': pwd.run + 'true': true_tool.run + 'tty': tty.run + 'whoami': whoami.run + 'yes': yes.run } fn main() { From ce8858c7a49189d6d63803d161f547805af4dcd0 Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 17 Oct 2026 13:46:05 +0330 Subject: [PATCH 7/8] whoami: restore the body the multicall conversion deleted 416742a16 truncated src/whoami/lib_default.c.v mid-function. It deleted the uid check, the getpwuid call, the return, and the closing brace, leaving fn whoami() !string { uid := os.geteuid() which does not parse: src/whoami/lib_default.c.v:15:1: error: unexpected eof, expecting `}` so the non-Windows variant of whoami could not be compiled. The conversion added `module whoami` to the top of the file, which it needed, but the body went with it. lib_windows.c.v, the other variant, kept its body. Restoring the body leaves the header as the only difference from main, which is what the conversion actually needed. Verified: the file parses, and the whoami module compiles. Worth noting against the commit message of 416742a16, which claims "whoami prints the user" was verified. It could not have been on this variant, since the variant did not compile; that check can only have run against the Windows one. --- src/whoami/lib_default.c.v | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/whoami/lib_default.c.v b/src/whoami/lib_default.c.v index 7bf06a85..7667defa 100644 --- a/src/whoami/lib_default.c.v +++ b/src/whoami/lib_default.c.v @@ -12,3 +12,9 @@ fn C.getpwuid(uid int) &C.passwd fn whoami() !string { uid := os.geteuid() + if uid == -1 { + return error('no user name') + } + pwd := C.getpwuid(uid) + return unsafe { cstring_to_vstring(pwd.pw_name) } +} From c96b83db583ae29e957c75ed7d149329241f2cf7 Mon Sep 17 00:00:00 2001 From: metif12 Date: Sat, 17 Oct 2026 14:35:01 +0330 Subject: [PATCH 8/8] multicall: match the invoked name without the .exe suffix Dispatch on argv[0] is the point of this layout - bin/pwd is a hard link to the multicall and runs pwd with no sub-command, as busybox does. It did not work on Windows at all: $ copy multicall.exe pwd.exe $ pwd.exe pwd.exe: no tool given, and pwd.exe is not itself a tool os.base(os.executable()) returns the last component of the path, which on Windows carries the extension, and the tools map is keyed on bare names, so `pwd.exe` never matched `pwd`. Every per-tool name missed. Dispatch on argv[1] was unaffected, which is why the failure looked like a missing tool rather than a matching bug. Now strip a trailing .exe before the lookup. The check is case-insensitive because the filesystem may hand back `PWD.EXE`, and it is length-guarded by the ends_with test so a name that is exactly `.exe` cannot slice to a negative index. Verified on Windows, with the binary copied to each name: true.exe exit 0, no output (dispatched to true) notatool.exe 'no tool given, and notatool.exe is not itself a tool' multicall.exe true exit 0 (argv[1] path still works) Not verified on Unix, where os.base never carries an extension and the strip is a no-op. --- src/multicall/main.v | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/multicall/main.v b/src/multicall/main.v index 9e8151ea..4c9bfd6f 100644 --- a/src/multicall/main.v +++ b/src/multicall/main.v @@ -56,12 +56,19 @@ fn main() { args := os.args // argv[0] is the path this binary was invoked as; its last component is // the name to dispatch on when the binary is linked or symlinked per tool. + // On Windows that component carries the extension, so a copy named pwd.exe + // has to be matched as `pwd` or the per-tool names never hit. invoked := os.base(os.executable()) + invoked_name := if invoked.to_lower().ends_with('.exe') { + invoked[..invoked.len - 4] + } else { + invoked + } mut name := '' mut rest := []string{} - if invoked in tools { - name = invoked + if invoked_name in tools { + name = invoked_name rest = args[1..] } else if args.len > 1 && args[1] in tools { name = args[1]