diff --git a/android/env/env.v b/android/env/env.v index b27258d..fa2ab8a 100644 --- a/android/env/env.v +++ b/android/env/env.v @@ -320,6 +320,44 @@ pub fn install(components string, verbosity int) int { return 0 } +// remove_components removess various external components installed by `install_components` +// These components can be (TODO: Android SDK components or) extra commands. +pub fn remove_components(arguments []string, verbosity int) ! { + if arguments.len == 0 { + return error('${@FN} requires at least one argument') + } + + mut args := arguments.clone() + if args[0] == 'remove' { + args = args[1..].clone() // skip `remove` part + } + if args.len == 0 { + return error('${@FN} requires an argument') + } + + components := args[0] + // vab remove extra ... + if components == 'extra' { + if args.len == 1 { + return error('${@FN} extra requires an argument') + } + extra.remove_command(input: args[1..].clone(), verbosity: verbosity) or { + return error('Removing of command failed: ${err}') + } + if verbosity > 0 { + println('Removed successfully') + } + return + } + + // TODO: vab remove "x;y;z,i;j;k" (sdkmanager compatible tuple) + // Allows to specify a string list of things to remove + return error('${@FN} TODO: currently `remove` only supports removing extra commands via `vab remove extra ...`') + // if verbosity > 0 { + // println('Removed successfully') + // } +} + // install_components installs various external components that vab can use. // These components can be Android SDK components or extra commands. pub fn install_components(arguments []string, verbosity int) ! { diff --git a/cli/cli.v b/cli/cli.v index ea3d81f..a930d2e 100644 --- a/cli/cli.v +++ b/cli/cli.v @@ -30,7 +30,9 @@ Sub-commands: doctor Display useful info about your system, (useful for bug reports) install Install various components. Example: - `vab install "platforms;android-21"' + `vab install "platforms;android-21" + remove Remove various components. Example: + `vab remove extra github:larpon/vab-sdl' pub const exe_git_hash = vab_commit_hash() pub const work_directory = paths.tmp_work() @@ -38,7 +40,7 @@ pub const cache_directory = paths.cache() pub const rip_vflags = ['-autofree', '-gc', '-g', '-cg', '-prod', 'run', '-showcc', '-skip-unused', '-no-bounds-checking'] // NOTE this can be removed when the deprecated `cli.args_to_options()` is removed pub const subcmds = ['complete', 'test-all', 'test-cleancode', 'test-runtime'] -pub const subcmds_builtin = ['doctor', 'install'] +pub const subcmds_builtin = ['doctor', 'install', 'remove'] pub const accepted_input_files = ['.v', '.apk', '.aab'] pub const vab_env_vars = [ diff --git a/cli/utils.v b/cli/utils.v index 9600d77..f0d1665 100644 --- a/cli/utils.v +++ b/cli/utils.v @@ -53,10 +53,22 @@ pub fn input_suggestions(input string) []string { $if vab_allow_extra_commands ? { for extra_alias in extra.installed_aliases() { similarity := f32(int(strings.levenshtein_distance_percentage(input, extra_alias) * 1000)) / 1000 - if similarity > 0.25 { + if similarity > 30 { suggests << extra_alias } } } + for builtin_command in subcmds_builtin { + similarity := f32(int(strings.levenshtein_distance_percentage(input, builtin_command) * 1000)) / 1000 + if similarity > 30 { + suggests << builtin_command + } + } + for sub_command in subcmds { + similarity := f32(int(strings.levenshtein_distance_percentage(input, sub_command) * 1000)) / 1000 + if similarity > 30 { + suggests << sub_command + } + } return suggests } diff --git a/extra/extra.v b/extra/extra.v index 97de9a0..c908e57 100644 --- a/extra/extra.v +++ b/extra/extra.v @@ -4,14 +4,17 @@ module extra import os +import os.filelock +import time import strings import compress.szip +import crypto.sha1 import vab.paths import vab.util import vab.vxt import net.http -const valid_sources = ['github'] +const valid_sources = ['github', 'local'] pub const command_prefix = 'vab' pub const data_path = os.join_path(paths.data(), 'extra') pub const temp_path = os.join_path(paths.tmp_work(), 'extra') @@ -23,6 +26,13 @@ pub: verbosity int } +@[params] +pub struct RemoveOptions { +pub: + input []string + verbosity int +} + pub struct Command { pub: id string @@ -39,6 +49,11 @@ pub: url string } +struct PathInfo { +pub: + sha string +} + // verbose prints `msg` to STDOUT if `InstallOptions.verbosity` level is >= `verbosity_level`. pub fn (io &InstallOptions) verbose(verbosity_level int, msg string) { if io.verbosity >= verbosity_level { @@ -46,6 +61,13 @@ pub fn (io &InstallOptions) verbose(verbosity_level int, msg string) { } } +// verbose prints `msg` to STDOUT if `InstallOptions.verbosity` level is >= `verbosity_level`. +pub fn (io &RemoveOptions) verbose(verbosity_level int, msg string) { + if io.verbosity >= verbosity_level { + println(msg) + } +} + // run_command runs a extra installed command if found in `args`. // If the command is found this function will call `exit()` with the result // returned by the executed command. @@ -103,19 +125,23 @@ fn launch_command(args []string) int { return 1 } -// install_command retrieves, installs and registers external extra commands +// install_command retrieves, installs and registers external extra commands. pub fn install_command(opt InstallOptions) ! { - // `vab install cmd xyz/abc` + // `vab install extra xyz/abc` if opt.input.len == 0 { return error('${@FN} requires input') } component := opt.input[0] // Only 1 argument needed for now if component.count(':') == 0 { + mut default_input := ['github:${component}'] // no source protocol detected, slap on default and try again... + if os.is_dir(component) { + default_input = ['local:${component}'] + } mod_opt := InstallOptions{ ...opt - input: ['github:${component}'] + input: default_input } return install_command(mod_opt) } @@ -130,39 +156,105 @@ pub fn install_command(opt InstallOptions) ! { } unit := component.all_after(':') + opt.verbose(1, 'Installing ${source}:${unit}...') + match source { 'github' { return install_from_github(unit, opt.verbosity) } + 'local' { + return install_from_local(unit, opt.verbosity) + } else { return error('${@FN} unknown source `${source}`. Valid sources are ${valid_sources}') } } } -fn install_from_github(unit string, verbosity int) ! { +// remove_command removes external extra commands. +pub fn remove_command(opt RemoveOptions) ! { + // `vab remove extra (source:)xyz/abc` + if opt.input.len == 0 { + return error('${@FN} requires input') + } + id := opt.input[0] // Only 1 argument needed for now + if id == '' { + return error('${@FN} requires input') + } + if id.count(':') > 1 { + return error('${@FN} "${id}" specifies too many sources (via ":"). An example valid `id` with source: "github:larpon/vab-sdl"') + } + if id.count('/') != 1 { + return error('${@FN} "${id}" must contain one "/" to specify the unit. An example valid `id` with source: "github:larpon/vab-sdl"') + } + mut component := id + mut source := '' + if component.count(':') > 0 { + source = id.all_before(':') + component = id.all_after(':') + } + if source != '' && source !in valid_sources { + return error('${@FN} unknown source `${source}`. Valid sources are ${valid_sources}') + } + unit := component + cmd_author, cmd_name := valid_unit(unit)! + + opt.verbose(1, 'Removing ${source}:${unit}...') + + base_path := os.join_path(data_path, 'commands') + mut concrete_path := base_path + if source != '' { + concrete_path = os.join_path(base_path, source, cmd_author, cmd_name) + } else { + mut found_command := ?Command{} + for key, command in commands() { + if command.unit == unit { + if fcommand := found_command { + return error('${@FN} multiple commands matches `${key}` (at least ${command.source}:${command.unit} and ${fcommand.source}:${fcommand.unit}). Please specify which source you want removed') + } + found_command = command + } + } + if command := found_command { + source = command.source + concrete_path = os.join_path(base_path, command.source, cmd_author, cmd_name) + } + } + if concrete_path == '' || concrete_path == base_path { + return error('${@FN} could not locate `${id}`') + } + os.rmdir_all(concrete_path) or {} + record_remove(source, unit)! +} + +fn valid_unit(unit string) !(string, string) { if unit.count('/') != 1 { return error('${@MOD} ${@FN} `${unit}` should contain exactly one "/" character') } unit_parts := unit.split('/') - // TODO: support @ notation for specific commits/branches? - // mut at_part := unit.all_after('@') - if !(valid_identifier(unit_parts[0]) && valid_identifier(unit_parts[1])) { return error('${@MOD} ${@FN} `${unit}` is not a valid identifier') } + return unit_parts[0], unit_parts[1] +} - cmd_author := unit_parts[0] - cmd_name := unit_parts[1] - if has_command(cmd_name) { +fn check_unit_not_installed(unit string) ! { + _, command_name := valid_unit(unit)! + if has_command(command_name) { extra_commands := commands() - if command := extra_commands[cmd_name] { + if command := extra_commands[command_name] { if command.unit != unit { return error('${@MOD} ${@FN} `${unit}` is already installed from `${command.unit}` via ${command.source}') } } } +} + +fn install_from_github(unit string, verbosity int) ! { + cmd_author, cmd_name := valid_unit(unit)! + + check_unit_not_installed(unit)! initial_dst := os.join_path(data_path, 'commands', 'github', cmd_author) @@ -183,7 +275,7 @@ fn install_from_github(unit string, verbosity int) ! { return error('${@MOD} ${@FN} failed to download `${unit}`: ${err}') } } - final_dst := os.join_path(initial_dst, unit_parts[1]) + final_dst := os.join_path(initial_dst, cmd_name) // Install if verbosity > 1 { println('Installing `${unit}` to "${final_dst}"...') @@ -201,31 +293,100 @@ fn install_from_github(unit string, verbosity int) ! { record_install(cmd_name, 'github', unit, sha)! } -fn record_install(id string, source string, unit string, hash string) ! { +fn install_from_local(path string, verbosity int) ! { + if !os.is_dir(path) { + return error('${@MOD} ${@FN} `${path}` should be a directory containing V source code') + } + path_trimmed := path.trim_right(os.path_separator) + if path_trimmed.count(os.path_separator) < 1 { + return error('${@MOD} ${@FN} `${path_trimmed}` should contain one or more "${os.path_separator}" characters') + } + unit := os.dir(path_trimmed).all_after_last(os.path_separator) + '/' + + os.file_name(path_trimmed) + cmd_author, cmd_name := valid_unit(unit)! + + check_unit_not_installed(unit)! + + initial_dst := os.join_path(data_path, 'commands', 'local', cmd_author) + + path_info := get_path_info(path)! + + sha := path_info.sha + + final_dst := os.join_path(initial_dst, cmd_name) + // Install + if verbosity > 1 { + println('Installing `${unit}` to "${final_dst}"...') + } + paths.ensure(initial_dst)! + + if os.exists(final_dst) { + os.rmdir_all(final_dst) or {} + } + os.cp_all(path, final_dst, false)! + + build_command(final_dst, verbosity)! + record_install(cmd_name, 'local', unit, sha)! +} + +fn record_remove(source string, unit string) ! { path := data_path - paths.ensure(path)! installs_db := os.join_path(path, 'installed.txt') - installs_db_bak := os.join_path(path, 'installed.txt.bak') if !os.exists(installs_db) { - os.create(installs_db)! + return } - mut installs := os.read_lines(installs_db)! + mut fl := filelock.new('${installs_db}.lock') + defer { fl.release() } + if fl.wait_acquire(5 * time.second) { + installs_db_bak := os.join_path(path, 'installed.txt.bak') + mut installs := os.read_lines(installs_db)! - for i, install_line in installs { - if install_line == '' || install_line.starts_with('#') { - continue + for i, install_line in installs { + if install_line == '' || install_line.starts_with('#') { + continue + } + split := install_line.split(';') + if split.len > 2 { + if split[1] == source && split[2] == unit { + installs.delete(i) + break + } + } } - split := install_line.split(';') - if split.len > 2 { - if split[1] == source && split[2] == unit { - installs.delete(i) - break + os.mv(installs_db, installs_db_bak, overwrite: true)! + os.write_lines(installs_db, installs)! + } +} + +fn record_install(id string, source string, unit string, hash string) ! { + path := data_path + paths.ensure(path)! + installs_db := os.join_path(path, 'installed.txt') + mut fl := filelock.new('${installs_db}.lock') + defer { fl.release() } + if fl.wait_acquire(5 * time.second) { + installs_db_bak := os.join_path(path, 'installed.txt.bak') + if !os.exists(installs_db) { + os.create(installs_db)! + } + mut installs := os.read_lines(installs_db)! + + for i, install_line in installs { + if install_line == '' || install_line.starts_with('#') { + continue + } + split := install_line.split(';') + if split.len > 2 { + if split[1] == source && split[2] == unit { + installs.delete(i) + break + } } } + installs << '${id};${source};${unit};${hash}' + os.mv(installs_db, installs_db_bak, overwrite: true)! + os.write_lines(installs_db, installs)! } - installs << '${id};${source};${unit};${hash}' - os.mv(installs_db, installs_db_bak, overwrite: true)! - os.write_lines(installs_db, installs)! } fn get_github_info(unit string) !GitHubInfo { @@ -270,6 +431,19 @@ fn get_github_info(unit string) !GitHubInfo { } } +fn get_path_info(path string) !PathInfo { + v_files := os.walk_ext(path, '.v') + mut sha_hash := '' + for v_file in v_files { + hash := sha1.sum(os.read_bytes(v_file) or { 'deadbeef'.bytes() }).hex() + combined_sha := '${sha_hash}${hash}' + sha_hash = sha1.sum(combined_sha.bytes()).hex() + } + return PathInfo{ + sha: sha_hash + } +} + // installed returns an array of the extra commands installed via // `vab install extra ...` // See also: installed_aliases @@ -314,29 +488,33 @@ pub fn commands() map[string]Command { path := data_path installs_db := os.join_path(path, 'installed.txt') if os.exists(installs_db) { - installs := os.read_lines(installs_db) or { return installed } - for install_line in installs { - if install_line == '' || install_line.starts_with('#') { - continue - } - split := install_line.split(';') - if split.len > 3 { - id := split[0] - alias := id.trim_left('${command_prefix}-') - source := split[1] or { 'unknown' } - unit := split[2] or { 'unknown/unknown' } - hash := split[3] or { 'deadbeef' } - unit_parts := unit.split('/') - final_dst := os.join_path(data_path, 'commands', source, unit_parts[0], - unit_parts[1]) - - installed[id] = Command{ - id: id - alias: alias - source: source - unit: unit - hash: hash - exe: os.join_path(final_dst, id) + mut fl := filelock.new('${installs_db}.lock') + defer { fl.release() } + if fl.wait_acquire(5 * time.second) { + installs := os.read_lines(installs_db) or { return installed } + for install_line in installs { + if install_line == '' || install_line.starts_with('#') { + continue + } + split := install_line.split(';') + if split.len > 3 { + id := split[0] + alias := id.trim_left('${command_prefix}-') + source := split[1] or { 'unknown' } + unit := split[2] or { 'unknown/unknown' } + hash := split[3] or { 'deadbeef' } + unit_parts := unit.split('/') + final_dst := os.join_path(data_path, 'commands', source, unit_parts[0], + unit_parts[1]) + + installed[id] = Command{ + id: id + alias: alias + source: source + unit: unit + hash: hash + exe: os.join_path(final_dst, id) + } } } } diff --git a/vab.v b/vab.v index 1fb7140..918abc6 100644 --- a/vab.v +++ b/vab.v @@ -139,13 +139,31 @@ fn main() { exit(0) } - if opt.run_builtin_cmd == 'install' { - install_args := os.args[os.args.index('install')..] - env.install_components(install_args, opt.verbosity) or { - util.vab_error('Failed to install components', details: '${err}') - exit(1) + match opt.run_builtin_cmd { + 'install' { + install_args := os.args[os.args.index(opt.run_builtin_cmd)..] + env.install_components(install_args, opt.verbosity) or { + util.vab_error('Failed to install components', details: '${err}') + exit(1) + } + exit(0) + } + 'remove' { + remove_args := os.args[os.args.index(opt.run_builtin_cmd)..] + env.remove_components(remove_args, opt.verbosity) or { + util.vab_error('Failed to remove components', details: '${err}') + exit(1) + } + exit(0) + } + else { + if opt.run_builtin_cmd != '' && opt.run_builtin_cmd !in cli.subcmds_builtin { + util.vab_error('Unknown sub-command "${opt.run_builtin_cmd}"', + details: 'Accepted sub-commands are: ${cli.subcmds_builtin}' + ) + exit(1) + } } - exit(0) } // Validate environment