diff --git a/build.vsh b/build.vsh index 9308ea45..24c88bfb 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', '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 +// 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/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/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/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 new file mode 100644 index 00000000..4c9bfd6f --- /dev/null +++ b/src/multicall/main.v @@ -0,0 +1,112 @@ +module main + +import arch +import hostid +import logname +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 +// 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. +// +// 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 +// 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 + 'logname': logname.run + 'pwd': pwd.run + 'true': true_tool.run + 'tty': tty.run + 'whoami': whoami.run + 'yes': yes.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. + // 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_name in tools { + name = invoked_name + 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/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 } 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/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 diff --git a/src/whoami/lib_default.c.v b/src/whoami/lib_default.c.v index 32a2bea4..7667defa 100644 --- a/src/whoami/lib_default.c.v +++ b/src/whoami/lib_default.c.v @@ -1,3 +1,5 @@ +module whoami + import os #include 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 } 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 }