diff options
Diffstat (limited to 'v_windows/v/old/cmd/tools/vpm.v')
-rw-r--r-- | v_windows/v/old/cmd/tools/vpm.v | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/v_windows/v/old/cmd/tools/vpm.v b/v_windows/v/old/cmd/tools/vpm.v new file mode 100644 index 0000000..a503d24 --- /dev/null +++ b/v_windows/v/old/cmd/tools/vpm.v @@ -0,0 +1,601 @@ +// Copyright (c) 2019-2021 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module main + +import os +import os.cmdline +import net.http +import json +import vhelp +import v.vmod + +const ( + default_vpm_server_urls = ['https://vpm.vlang.io'] + valid_vpm_commands = ['help', 'search', 'install', 'update', 'upgrade', 'outdated', + 'list', 'remove', 'show'] + excluded_dirs = ['cache', 'vlib'] + supported_vcs_systems = ['git', 'hg'] + supported_vcs_folders = ['.git', '.hg'] + supported_vcs_update_cmds = map{ + 'git': 'git pull' + 'hg': 'hg pull --update' + } + supported_vcs_install_cmds = map{ + 'git': 'git clone --depth=1' + 'hg': 'hg clone' + } + supported_vcs_outdated_steps = map{ + 'git': ['git fetch', 'git rev-parse @', 'git rev-parse @{u}'] + 'hg': ['hg incoming'] + } +) + +struct Mod { + id int + name string + url string + nr_downloads int + vcs string +} + +struct Vmod { +mut: + name string + version string + deps []string +} + +fn main() { + init_settings() + // This tool is intended to be launched by the v frontend, + // which provides the path to V inside os.getenv('VEXE') + // args are: vpm [options] SUBCOMMAND module names + params := cmdline.only_non_options(os.args[1..]) + verbose_println('cli params: $params') + if params.len < 1 { + vpm_help() + exit(5) + } + vpm_command := params[0] + mut module_names := params[1..] + ensure_vmodules_dir_exist() + // println('module names: ') println(module_names) + match vpm_command { + 'help' { + vpm_help() + } + 'search' { + vpm_search(module_names) + } + 'install' { + if module_names.len == 0 && os.exists('./v.mod') { + println('Detected v.mod file inside the project directory. Using it...') + manifest := vmod.from_file('./v.mod') or { panic(err) } + module_names = manifest.dependencies + } + vpm_install(module_names) + } + 'update' { + vpm_update(module_names) + } + 'upgrade' { + vpm_upgrade() + } + 'outdated' { + vpm_outdated() + } + 'list' { + vpm_list() + } + 'remove' { + vpm_remove(module_names) + } + 'show' { + vpm_show(module_names) + } + else { + println('Error: you tried to run "v $vpm_command"') + println('... but the v package management tool vpm only knows about these commands:') + for validcmd in valid_vpm_commands { + println(' v $validcmd') + } + exit(3) + } + } +} + +fn vpm_search(keywords []string) { + search_keys := keywords.map(it.replace('_', '-')) + if settings.is_help { + vhelp.show_topic('search') + exit(0) + } + if search_keys.len == 0 { + println('´v search´ requires *at least one* keyword.') + exit(2) + } + modules := get_all_modules() + installed_modules := get_installed_modules() + joined := search_keys.join(', ') + mut index := 0 + for mod in modules { + // TODO for some reason .filter results in substr error, so do it manually + for k in search_keys { + if !mod.contains(k) { + continue + } + if index == 0 { + println('Search results for "$joined":\n') + } + index++ + mut parts := mod.split('.') + // in case the author isn't present + if parts.len == 1 { + parts << parts[0] + parts[0] = ' ' + } else { + parts[0] = ' by ${parts[0]} ' + } + installed := if mod in installed_modules { ' (installed)' } else { '' } + println('${index}. ${parts[1]}${parts[0]}[$mod]$installed') + break + } + } + if index == 0 { + vexe := os.getenv('VEXE') + vroot := os.real_path(os.dir(vexe)) + mut messages := ['No module(s) found for `$joined` .'] + for vlibmod in search_keys { + if os.is_dir(os.join_path(vroot, 'vlib', vlibmod)) { + messages << 'There is already an existing "$vlibmod" module in vlib, so you can just `import $vlibmod` .' + } + } + for m in messages { + println(m) + } + } else { + println('\nUse "v install author_name.module_name" to install the module.') + } +} + +fn vpm_install(module_names []string) { + if settings.is_help { + vhelp.show_topic('install') + exit(0) + } + if module_names.len == 0 { + println('´v install´ requires *at least one* module name.') + exit(2) + } + mut errors := 0 + for n in module_names { + name := n.trim_space().replace('_', '-') + mod := get_module_meta_info(name) or { + errors++ + println('Errors while retrieving meta data for module $name:') + println(err) + continue + } + mut vcs := mod.vcs + if vcs == '' { + vcs = supported_vcs_systems[0] + } + if vcs !in supported_vcs_systems { + errors++ + println('Skipping module "$name", since it uses an unsupported VCS {$vcs} .') + continue + } + mod_name_as_path := mod.name.replace('.', os.path_separator).replace('-', '_').to_lower() + final_module_path := os.real_path(os.join_path(settings.vmodules_path, mod_name_as_path)) + if os.exists(final_module_path) { + vpm_update([name]) + continue + } + println('Installing module "$name" from $mod.url to $final_module_path ...') + vcs_install_cmd := supported_vcs_install_cmds[vcs] + cmd := '$vcs_install_cmd "$mod.url" "$final_module_path"' + verbose_println(' command: $cmd') + cmdres := os.execute(cmd) + if cmdres.exit_code != 0 { + errors++ + println('Failed installing module "$name" to "$final_module_path" .') + verbose_println('Failed command: $cmd') + verbose_println('Failed command output:\n$cmdres.output') + continue + } + resolve_dependencies(name, final_module_path, module_names) + } + if errors > 0 { + exit(1) + } +} + +fn vpm_update(m []string) { + mut module_names := m.clone() + if settings.is_help { + vhelp.show_topic('update') + exit(0) + } + if module_names.len == 0 { + module_names = get_installed_modules() + } + mut errors := 0 + for name in module_names { + final_module_path := valid_final_path_of_existing_module(name) or { continue } + os.chdir(final_module_path) + println('Updating module "$name"...') + verbose_println(' work folder: $final_module_path') + vcs := vcs_used_in_dir(final_module_path) or { continue } + vcs_cmd := supported_vcs_update_cmds[vcs[0]] + verbose_println(' command: $vcs_cmd') + vcs_res := os.execute('$vcs_cmd') + if vcs_res.exit_code != 0 { + errors++ + println('Failed updating module "$name".') + verbose_println('Failed command: $vcs_cmd') + verbose_println('Failed details:\n$vcs_res.output') + continue + } else { + verbose_println(' $vcs_res.output.trim_space()') + } + resolve_dependencies(name, final_module_path, module_names) + } + if errors > 0 { + exit(1) + } +} + +fn get_outdated() ?[]string { + module_names := get_installed_modules() + mut outdated := []string{} + for name in module_names { + final_module_path := valid_final_path_of_existing_module(name) or { continue } + os.chdir(final_module_path) + vcs := vcs_used_in_dir(final_module_path) or { continue } + vcs_cmd_steps := supported_vcs_outdated_steps[vcs[0]] + mut outputs := []string{} + for step in vcs_cmd_steps { + res := os.execute(step) + if res.exit_code < 0 { + verbose_println('Error command: $step') + verbose_println('Error details:\n$res.output') + return error('Error while checking latest commits for "$name".') + } + if vcs[0] == 'hg' { + if res.exit_code == 1 { + outdated << name + } + } else { + outputs << res.output + } + } + if vcs[0] == 'git' && outputs[1] != outputs[2] { + outdated << name + } + } + return outdated +} + +fn vpm_upgrade() { + outdated := get_outdated() or { exit(1) } + if outdated.len > 0 { + vpm_update(outdated) + } else { + println('Modules are up to date.') + } +} + +fn vpm_outdated() { + outdated := get_outdated() or { exit(1) } + if outdated.len > 0 { + println('Outdated modules:') + for m in outdated { + println(' $m') + } + } else { + println('Modules are up to date.') + } +} + +fn vpm_list() { + module_names := get_installed_modules() + if module_names.len == 0 { + println('You have no modules installed.') + exit(0) + } + println('Installed modules:') + for mod in module_names { + println(' $mod') + } +} + +fn vpm_remove(module_names []string) { + if settings.is_help { + vhelp.show_topic('remove') + exit(0) + } + if module_names.len == 0 { + println('´v remove´ requires *at least one* module name.') + exit(2) + } + for name in module_names { + final_module_path := valid_final_path_of_existing_module(name) or { continue } + println('Removing module "$name"...') + verbose_println('removing folder $final_module_path') + os.rmdir_all(final_module_path) or { + verbose_println('error while removing "$final_module_path": $err.msg') + } + // delete author directory if it is empty + author := name.split('.')[0] + author_dir := os.real_path(os.join_path(settings.vmodules_path, author)) + if !os.exists(author_dir) { + continue + } + if os.is_dir_empty(author_dir) { + verbose_println('removing author folder $author_dir') + os.rmdir(author_dir) or { + verbose_println('error while removing "$author_dir": $err.msg') + } + } + } +} + +fn valid_final_path_of_existing_module(name string) ?string { + mod_name_as_path := name.replace('.', os.path_separator).replace('-', '_').to_lower() + name_of_vmodules_folder := os.join_path(settings.vmodules_path, mod_name_as_path) + final_module_path := os.real_path(name_of_vmodules_folder) + if !os.exists(final_module_path) { + println('No module with name "$name" exists at $name_of_vmodules_folder') + return none + } + if !os.is_dir(final_module_path) { + println('Skipping "$name_of_vmodules_folder", since it is not a folder.') + return none + } + vcs_used_in_dir(final_module_path) or { + println('Skipping "$name_of_vmodules_folder", since it does not use a supported vcs.') + return none + } + return final_module_path +} + +fn ensure_vmodules_dir_exist() { + if !os.is_dir(settings.vmodules_path) { + println('Creating $settings.vmodules_path/ ...') + os.mkdir(settings.vmodules_path) or { panic(err) } + } +} + +fn vpm_help() { + vhelp.show_topic('vpm') +} + +fn vcs_used_in_dir(dir string) ?[]string { + mut vcs := []string{} + for repo_subfolder in supported_vcs_folders { + checked_folder := os.real_path(os.join_path(dir, repo_subfolder)) + if os.is_dir(checked_folder) { + vcs << repo_subfolder.replace('.', '') + } + } + if vcs.len == 0 { + return none + } + return vcs +} + +fn get_installed_modules() []string { + dirs := os.ls(settings.vmodules_path) or { return [] } + mut modules := []string{} + for dir in dirs { + adir := os.join_path(settings.vmodules_path, dir) + if dir in excluded_dirs || !os.is_dir(adir) { + continue + } + if os.exists(os.join_path(adir, 'v.mod')) && os.exists(os.join_path(adir, '.git', 'config')) { + // an official vlang module with a short module name, like `vsl`, `ui` or `markdown` + modules << dir + continue + } + author := dir + mods := os.ls(adir) or { continue } + for m in mods { + vcs_used_in_dir(os.join_path(adir, m)) or { continue } + modules << '${author}.$m' + } + } + return modules +} + +fn get_all_modules() []string { + url := get_working_server_url() + r := http.get(url) or { panic(err) } + if r.status_code != 200 { + println('Failed to search vpm.vlang.io. Status code: $r.status_code') + exit(1) + } + s := r.text + mut read_len := 0 + mut modules := []string{} + for read_len < s.len { + mut start_token := '<a href="/mod' + end_token := '</a>' + // get the start index of the module entry + mut start_index := s.index_after(start_token, read_len) + if start_index == -1 { + break + } + // get the index of the end of anchor (a) opening tag + // we use the previous start_index to make sure we are getting a module and not just a random 'a' tag + start_token = '">' + start_index = s.index_after(start_token, start_index) + start_token.len + // get the index of the end of module entry + end_index := s.index_after(end_token, start_index) + if end_index == -1 { + break + } + modules << s[start_index..end_index] + read_len = end_index + if read_len >= s.len { + break + } + } + return modules +} + +fn resolve_dependencies(name string, module_path string, module_names []string) { + vmod_path := os.join_path(module_path, 'v.mod') + if !os.exists(vmod_path) { + return + } + data := os.read_file(vmod_path) or { return } + vmod := parse_vmod(data) + mut deps := []string{} + // filter out dependencies that were already specified by the user + for d in vmod.deps { + if d !in module_names { + deps << d + } + } + if deps.len > 0 { + println('Resolving $deps.len dependencies for module "$name"...') + verbose_println('Found dependencies: $deps') + vpm_install(deps) + } +} + +fn parse_vmod(data string) Vmod { + keys := ['name', 'version', 'deps'] + mut m := map{ + 'name': '' + 'version': '' + 'deps': '' + } + for key in keys { + mut key_index := data.index('$key:') or { continue } + key_index += key.len + 1 + m[key] = data[key_index..data.index_after('\n', key_index)].trim_space().replace("'", + '').replace('[', '').replace(']', '') + } + mut vmod := Vmod{} + vmod.name = m['name'] + vmod.version = m['version'] + if m['deps'].len > 0 { + vmod.deps = m['deps'].split(',') + } + return vmod +} + +fn get_working_server_url() string { + server_urls := if settings.server_urls.len > 0 { + settings.server_urls + } else { + default_vpm_server_urls + } + for url in server_urls { + verbose_println('Trying server url: $url') + http.head(url) or { + verbose_println(' $url failed.') + continue + } + return url + } + panic('No responding vpm server found. Please check your network connectivity and try again later.') +} + +// settings context: +struct VpmSettings { +mut: + is_help bool + is_verbose bool + server_urls []string + vmodules_path string +} + +const ( + settings = &VpmSettings{} +) + +fn init_settings() { + mut s := &VpmSettings(0) + unsafe { + s = settings + } + s.is_help = '-h' in os.args || '--help' in os.args || 'help' in os.args + s.is_verbose = '-v' in os.args + s.server_urls = cmdline.options(os.args, '-server-url') + s.vmodules_path = os.vmodules_dir() +} + +fn verbose_println(s string) { + if settings.is_verbose { + println(s) + } +} + +fn get_module_meta_info(name string) ?Mod { + mut errors := []string{} + for server_url in default_vpm_server_urls { + modurl := server_url + '/jsmod/$name' + verbose_println('Retrieving module metadata from: $modurl ...') + r := http.get(modurl) or { + errors << 'Http server did not respond to our request for ${modurl}.' + errors << 'Error details: $err' + continue + } + if r.status_code == 404 || r.text.trim_space() == '404' { + errors << 'Skipping module "$name", since $server_url reported that "$name" does not exist.' + continue + } + if r.status_code != 200 { + errors << 'Skipping module "$name", since $server_url responded with $r.status_code http status code. Please try again later.' + continue + } + s := r.text + if s.len > 0 && s[0] != `{` { + errors << 'Invalid json data' + errors << s.trim_space().limit(100) + '...' + continue + } + mod := json.decode(Mod, s) or { + errors << 'Skipping module "$name", since its information is not in json format.' + continue + } + if '' == mod.url || '' == mod.name { + errors << 'Skipping module "$name", since it is missing name or url information.' + continue + } + return mod + } + return error(errors.join_lines()) +} + +fn vpm_show(module_names []string) { + installed_modules := get_installed_modules() + for module_name in module_names { + if module_name !in installed_modules { + module_meta_info := get_module_meta_info(module_name) or { continue } + print(' +Name: $module_meta_info.name +Homepage: $module_meta_info.url +Downloads: $module_meta_info.nr_downloads +Installed: False +-------- +') + continue + } + path := os.join_path(os.vmodules_dir(), module_name) + mod := vmod.from_file(os.join_path(path, 'v.mod')) or { continue } + print('Name: $mod.name +Version: $mod.version +Description: $mod.description +Homepage: $mod.repo_url +Author: $mod.author +License: $mod.license +Location: $path +Requires: ${mod.dependencies.join(', ')} +-------- +') + } +} |