Skip to content

Commit

Permalink
shy: add command line tool
Browse files Browse the repository at this point in the history
  • Loading branch information
larpon committed Oct 5, 2022
1 parent 40a4cf0 commit c1e8d25
Show file tree
Hide file tree
Showing 7 changed files with 635 additions and 0 deletions.
175 changes: 175 additions & 0 deletions cli/cli.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
module cli

import os
import shy.vxt

pub const (
exe_version = version()
exe_name = os.file_name(os.executable())
exe_short_name = os.file_name(os.executable()).replace('.exe', '')
exe_dir = os.dir(os.real_path(os.executable()))
exe_args_description = 'input
or: shy <sub-command> [options] input'
exe_description = 'shy is a module and tool made with love.
It is primarily aimed at V developers roaming the creative corners of coding.
shy can compile, package and deploy V apps for a wide range of platforms like:
Linux, macOS, Windows, Android and HTML5 (WASM).
The following does the same as if they were passed to the "v" compiler:
Flags:
-autofree, -gc <type>, -g, -cg, -prod, -showcc
Sub-commands:
run Run the V code
doctor Display useful info about your system for bug reports'
exe_git_hash = shy_commit_hash()
work_directory = shy_tmp_work_dir()
cache_directory = shy_cache_dir()
rip_vflags = ['-autofree', '-gc', '-g', '-cg', '-prod', 'run', '-showcc']
subcmds = ['complete', 'test-cleancode']
accepted_input_files = ['.v']
)

pub const shy_env_vars = [
'SHY_FLAGS',
'VEXE',
'VMODULES',
]

// check_essentials ensures that the work environment has all needed dependencies
// and meet all required needs.
pub fn check_essentials(exit_on_error bool) {
// Validate V install
if vxt.vexe() == '' {
eprintln('No V install could be detected')
eprintln('Please install V from https://github.com/vlang/v')
eprintln('or provide a valid path to V via VEXE env variable')
if exit_on_error {
exit(1)
}
}
}

// dot_shy_path returns the path to the `.shy` file next to `file_or_dir_path` if found, an empty string otherwise.
pub fn dot_shy_path(file_or_dir_path string) string {
if os.is_dir(file_or_dir_path) {
if os.is_file(os.join_path(file_or_dir_path, '.shy')) {
return os.join_path(file_or_dir_path, '.shy')
}
} else {
if os.is_file(os.join_path(os.dir(file_or_dir_path), '.shy')) {
return os.join_path(os.dir(file_or_dir_path), '.shy')
}
}
return ''
}

// launch_cmd launches an external command.
pub fn launch_cmd(args []string) ! {
mut cmd := args[0]
tool_args := args[1..]
if cmd.starts_with('test-') {
cmd = cmd.all_after('test-')
}
v := vxt.vexe()
tool_exe := os.join_path(cli.exe_dir, 'cmd', cmd)
if os.is_executable(v) {
hash_file := os.join_path(cli.exe_dir, 'cmd', '.' + cmd + '.hash')

mut hash := ''
if os.is_file(hash_file) {
hash = os.read_file(hash_file) or { '' }
}
if hash != cli.exe_git_hash {
v_cmd := [
v,
tool_exe + '.v',
'-o',
tool_exe,
]
res := os.execute(v_cmd.join(' '))
if res.exit_code < 0 {
return error('${@MOD}.${@FN} failed compiling "$cmd": $res.output')
}
if res.exit_code == 0 {
os.write_file(hash_file, cli.exe_git_hash) or {}
} else {
vcmd := v_cmd.join(' ')
return error('${@MOD}.${@FN} "$vcmd" failed:\n$res.output')
}
}
}
if os.is_executable(tool_exe) {
os.setenv('SHY_EXE', os.join_path(cli.exe_dir, cli.exe_name), true)
$if windows {
exit(os.system('${os.quoted_path(tool_exe)} $tool_args'))
} $else $if js {
// no way to implement os.execvp in JS backend
exit(os.system('$tool_exe $tool_args'))
} $else {
os.execvp(tool_exe, args) or { panic(err) }
}
exit(2)
}
exec := (tool_exe + ' ' + tool_args.join(' ')).trim_right(' ')
v_message := if !os.is_executable(v) { ' (v was not found)' } else { '' }
return error('${@MOD}.${@FN} failed executing "$exec"$v_message')
}

// string_to_args converts `input` string to an `os.args`-like array.
// string_to_args preserves strings delimited by both `"` and `'`.
pub fn string_to_args(input string) ![]string {
mut args := []string{}
mut buf := ''
mut in_string := false
mut delim := byte(` `)
for ch in input {
if ch in [`"`, `'`] {
if !in_string {
delim = ch
}
in_string = !in_string && ch == delim
if !in_string {
if buf != '' && buf != ' ' {
args << buf
}
buf = ''
delim = ` `
}
continue
}
buf += ch.ascii_str()
if !in_string && ch == ` ` {
if buf != '' && buf != ' ' {
args << buf[..buf.len - 1]
}
buf = ''
continue
}
}
if buf != '' && buf != ' ' {
args << buf
}
if in_string {
return error(@FN +
': could not parse input, missing closing string delimiter `$delim.ascii_str()`')
}
return args
}

// validate_input validates `input` for use with shy.
pub fn validate_input(input string) ! {
input_ext := os.file_ext(input)

accepted_input_ext := input_ext in cli.accepted_input_files
if !(os.is_dir(input) || accepted_input_ext) {
return error('input should be a V file or a directory containing V sources')
}
if accepted_input_ext {
if !os.is_file(input) {
return error('input should be a V file or a directory containing V sources')
}
}
}
51 changes: 51 additions & 0 deletions cli/doctor.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module cli

import os
import shy.vxt

// doctor prints various useful information to the shell to aid
// diagnosticing the work environment.
pub fn doctor(opt Options) {
env_vars := os.environ()

// shy section
println('$exe_short_name
Version $exe_version $exe_git_hash
Path "$exe_dir"')

// Shell environment
print_var_if_set := fn (vars map[string]string, var_name string) {
if var_name in vars {
println('\t$var_name=' + os.getenv(var_name))
}
}
println('env')
for env_var in shy_env_vars {
print_var_if_set(env_vars, env_var)
}

// Product section
println('Product
Output "$opt.output"')

// V section
println('V
Version $vxt.version() $vxt.version_commit_hash()
Path "$vxt.home()"')
if opt.v_flags.len > 0 {
println('\tFlags $opt.v_flags')
}
// Print output of `v doctor` if v is found
if vxt.found() {
println('')
v_cmd := [
vxt.vexe(),
'doctor',
]
v_res := os.execute(v_cmd.join(' '))
out_lines := v_res.output.split('\n')
for line in out_lines {
println('\t$line')
}
}
}
147 changes: 147 additions & 0 deletions cli/options.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
module cli

import os
import flag

pub struct Options {
pub:
// These fields would make little sense to change during a run
verbosity int
work_dir string = work_directory
//
run bool
parallel bool = true // Run, what can be run, in parallel
cache bool // defaults to false in os.args/flag parsing phase
gles_version int // = android.default_gles_version
// Detected environment
dump_usage bool
pub mut:
// I/O
input string
output string
additional_args []string // additional_args passed via os.args
is_prod bool
c_flags []string // flags passed to the C compiler(s)
v_flags []string // flags passed to the V compiler
assets_extra []string
libs_extra []string
}

// options_from_env returns an `Option` struct filled with flags set via
// the `SHY_FLAGS` env variable otherwise it returns a default `Option` struct.
pub fn options_from_env(defaults Options) !Options {
env_flags := os.getenv('SHY_FLAGS')
if env_flags != '' {
mut flags := [os.args[0]]
flags << string_to_args(env_flags)!
opts, _ := args_to_options(flags, defaults)!
return opts
}
return defaults
}

// extend_from_dot_shy will merge the `Options` with any content
// found in any `.shy` config files.
pub fn (mut opt Options) extend_from_dot_shy() {
// Look up values in input .shy file next to input if no flags or defaults was set
// TODO use TOML format here
// dot_shy_file := dot_shy_path(opt.input)
// dot_shy := os.read_file(dot_shy_file) or { '' }
}

// validate_env ensures that `Options` meet all runtime requirements.
pub fn (opt &Options) validate_env() ! {
}

// args_to_options returns an `Option` merged from (CLI/Shell) `arguments` using `defaults` as
// values where no value can be obtained from `arguments`.
pub fn args_to_options(arguments []string, defaults Options) !(Options, &flag.FlagParser) {
mut args := arguments.clone()

// Indentify sub-commands.
for subcmd in subcmds {
if subcmd in args {
// First encountered known sub-command is executed on the spot.
launch_cmd(args[args.index(subcmd)..]) or {
eprintln(err)
exit(1)
}
exit(0)
}
}

mut v_flags := []string{}
mut cmd_flags := []string{}
// Indentify special flags in args before FlagParser ruin them.
// E.g. the -autofree flag will result in dump_usage being called for some weird reason???
for special_flag in rip_vflags {
if special_flag in args {
if special_flag == '-gc' {
gc_type := args[(args.index(special_flag)) + 1]
v_flags << special_flag + ' $gc_type'
args.delete(args.index(special_flag) + 1)
} else if special_flag.starts_with('-') {
v_flags << special_flag
} else {
cmd_flags << special_flag
}
args.delete(args.index(special_flag))
}
}

mut fp := flag.new_flag_parser(args)
fp.application(exe_short_name)
fp.version(version_full())
fp.description(exe_description)
fp.arguments_description(exe_args_description)

fp.skip_executable()

mut verbosity := fp.int_opt('verbosity', `v`, 'Verbosity level 1-3') or { defaults.verbosity }
// TODO implement FlagParser 'is_sat(name string) bool' or something in vlib for this usecase?
if ('-v' in args || 'verbosity' in args) && verbosity == 0 {
verbosity = 1
}

mut opt := Options{
assets_extra: fp.string_multi('assets', `a`, 'Asset dir(s) to include in build')
libs_extra: fp.string_multi('libs', `a`, 'Lib dir(s) to include in build')
v_flags: fp.string_multi('flag', `f`, 'Additional flags for the V compiler')
c_flags: fp.string_multi('cflag', `c`, 'Additional flags for the C compiler')
gles_version: fp.int('gles', 0, defaults.gles_version, 'GLES version to use from any of 2,3')
//
run: 'run' in cmd_flags
dump_usage: fp.bool('help', `h`, defaults.dump_usage, 'Show this help message and exit')
cache: !fp.bool('nocache', 0, defaults.cache, 'Do not use build cache')
//
output: fp.string('output', `o`, defaults.output, 'Path to output (dir/file)')
//
verbosity: verbosity
parallel: !fp.bool('no-parallel', 0, false, 'Do not run tasks in parallel.')
//
work_dir: defaults.work_dir
}

opt.additional_args = fp.finalize() or {
return error(@FN + ': flag parser failed finalizing: $err')
}

mut c_flags := []string{}
c_flags << opt.c_flags
for c_flag in defaults.c_flags {
if c_flag !in c_flags {
c_flags << c_flag
}
}
opt.c_flags = c_flags

v_flags << opt.v_flags
for v_flag in defaults.v_flags {
if v_flag !in v_flags {
v_flags << v_flag
}
}
opt.v_flags = v_flags

return opt, fp
}
Loading

0 comments on commit c1e8d25

Please sign in to comment.