module main import ( net.http os os.cmdline json filepath ) const ( default_vpm_server_urls = ['https://vpm.best', 'https://vpm.vlang.io'] valid_vpm_commands = ['help', 'search', 'install', 'update', 'remove'] excluded_dirs = ['cache', 'vlib'] supported_vcs_systems = ['git', 'hg'] supported_vcs_folders = ['.git', '.hg'] supported_vcs_update_cmds = { 'git': 'git pull --depth=1' 'hg': 'hg pull --update' } supported_vcs_install_cmds = { 'git': 'git clone --depth=1' 'hg': 'hg clone' } ) 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 := os.args // args are: vpm [options] SUBCOMMAND module names params := cmdline.only_non_options(args[1..]) verbose_println('cli params: $params') if params.len < 1 { vpm_help([]) exit(5) } vpm_command := params[0] module_names := params[1..] ensure_vmodules_dir_exist() // println('module names: ') println(module_names) match vpm_command { 'help' { vpm_help(module_names) } 'search' { vpm_search(module_names) } 'install' { vpm_install(module_names) } 'update' { vpm_update(module_names) } 'remove' { vpm_remove(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) { if settings.is_help { println('Usage:') println(' v search keyword1 [keyword2] [...]') println(' ^^^^^^^^^^^^^^^^^ will search https://vpm.vlang.io/ for matching modules,') println(' and will show details about them') show_vpm_options() exit(0) } if keywords.len == 0 { println(' v search requires *at least one* keyword') exit(2) } modules := get_all_modules() joined := keywords.join(', ') mut index := 0 for mod in modules { // TODO for some reason .filter results in substr error, so do it manually for k in keywords { 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] = '' } println('${index}. ${parts[1]} by ${parts[0]} [$mod]') break } } println('\nUse "v install author.module_name" to install the module') if index == 0 { println('No module(s) found for "$joined"') } } fn vpm_install(module_names []string) { if settings.is_help { println('Usage:') println(' v install module [module] [module] [...]') println(' ^^^^^^^^^^^^^ will install the modules you specified') show_vpm_options() exit(0) } if module_names.len == 0 { println(' v install requires *at least one* module name') exit(2) } mut errors := 0 url := get_working_server_url() for n in module_names { name := n.trim_space() modurl := url + '/jsmod/$name' r := http.get(modurl) or { println('Http server did not respond to our request for ${modurl}.') println('Error details: $err') errors++ continue } if r.status_code == 404 { println('Skipping module "$name", since $url reported that "$name" does not exist.') errors++ continue } if r.status_code != 200 { println('Skipping module "$name", since $url responded with $r.status_code http status code. Please try again later.') errors++ continue } s := r.text mod := json.decode(Mod,s) or { errors++ println('Skipping module "$name", since its information is not in json format.') continue } if ('' == mod.url || '' == mod.name) { errors++ // a possible 404 error, which means a missing module? println('Skipping module "$name", since it is missing name or url information.') 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 } final_module_path := os.realpath(filepath.join(settings.vmodules_path,mod.name.replace('.', os.path_separator))) 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.exec(cmd) or { errors++ println('Could not install module "$name" to "$final_module_path" .') verbose_println('Error command: $cmd') verbose_println('Error details: $err') continue } 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 if settings.is_help { println('Usage: ') println(' a) v update module [module] [module] [...]') println(' ^^^^^^^^^^^^ will update the listed modules to their latest versions') println(' b) v update') println(' ^^^^^^^^^^^^ will update ALL installed modules to their latest versions') show_vpm_options() 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.exec('${vcs_cmd}') or { errors++ println('Could not update module "$name".') verbose_println('Error command: ${vcs_cmd}') verbose_println('Error details:\n$err') continue } 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 } resolve_dependencies(name, final_module_path, module_names) } if errors > 0 { exit(1) } } fn vpm_remove(module_names []string) { if settings.is_help { println('Usage: ') println(' a) v remove module [module] [module] [...]') println(' ^^^^^^^^^^^^ will remove the listed modules') println(' b) v remove') println(' ^^^^^^^^^^^^ will remove ALL installed modules') show_vpm_options() exit(0) } if module_names.len == 0 { println(' v update 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_recursive(final_module_path) // delete author directory if it is empty author := name.split('.')[0] author_dir := os.realpath(filepath.join(settings.vmodules_path,author)) if os.is_dir_empty(author_dir) { verbose_println('removing author folder $author_dir') os.rmdir(author_dir) } } } fn valid_final_path_of_existing_module(name string) ?string { name_of_vmodules_folder := filepath.join(settings.vmodules_path,name.replace('.', os.path_separator)) final_module_path := os.realpath(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(module_names []string) { println('Usage:') println(' a) v install module [module] [module] [...]') println(' b) v update [module] [...]') println(' c) v remove [module] [...]') println(' d) v search keyword1 [keyword2] [...]') println('') println(' You can also pass -h or --help after each vpm command from the above, to see more details about it.') } fn vcs_used_in_dir(dir string) ?[]string { mut vcs := []string for repo_subfolder in supported_vcs_folders { checked_folder := os.realpath(filepath.join(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 := filepath.join(settings.vmodules_path,dir) if dir in excluded_dirs || !os.is_dir(adir) { continue } author := dir mods := os.ls(adir) or { continue } for m in mods { vcs_used_in_dir(filepath.join(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.best. 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 := '' 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, module_path string, module_names []string) { vmod_path := filepath.join(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 := { '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 = '-verbose' in os.args || '--verbose' in os.args s.server_urls = cmdline.many_values(os.args, '-server-url') s.vmodules_path = os.home_dir() + '.vmodules' } fn show_vpm_options() { println('Options:') println(' -help - Show usage info') println(' -verbose - Print more details about the performed operation') println(' -server-url - When doing network operations, use this vpm server. Can be given multiple times.') } fn verbose_println(s string) { if settings.is_verbose { println(s) } }