Skip to content
54 changes: 54 additions & 0 deletions build.vsh
Original file line number Diff line number Diff line change
Expand Up @@ -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)!
Expand All @@ -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
}
Expand Down Expand Up @@ -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) }
}
12 changes: 12 additions & 0 deletions common/common.v
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ("<msg>; code: <code>") away from a POSIX and Win32 error to make
// it match what GNU coreutils return
Expand Down
14 changes: 8 additions & 6 deletions src/arch/arch.v
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 2 additions & 0 deletions src/hostid/gethostid_default.c.v
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module hostid

/*
** Get hostid on unsupported platforms
*/
Expand Down
2 changes: 2 additions & 0 deletions src/hostid/gethostid_nix.c.v
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module hostid

#include <unistd.h>

fn C.gethostid() int
Expand Down
13 changes: 9 additions & 4 deletions src/hostid/hostid.v
Original file line number Diff line number Diff line change
@@ -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.'
Expand All @@ -8,12 +9,15 @@ const app_description = 'Print the numeric identifier (in hexadecimal) of the cu
** hostid clone written in V
** Original author: B. <SheatNoisette> 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
Expand All @@ -23,4 +27,5 @@ fn main() {

// Other flags
fp.remaining_parameters()
return 0
}
54 changes: 24 additions & 30 deletions src/logname/logname.v
Original file line number Diff line number Diff line change
@@ -1,33 +1,14 @@
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
*/

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)
Expand All @@ -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
Expand Down Expand Up @@ -73,20 +56,31 @@ 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

// Uses C.getlogin internally
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
}
112 changes: 112 additions & 0 deletions src/multicall/main.v
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading