module main import os import net.http import net.urllib import sync.pool import v.vmod import json import term import log struct Module { mut: // Fields determined by the url or the info received from the VPM API. name string url string vcs string // Fields based on preference / environment. version string // specifies the requested version. install_path string install_path_fmted string is_installed bool is_external bool installed_version string } struct ModuleVpmInfo { // id int name string url string vcs string nr_downloads int } pub struct ModuleDateInfo { name string mut: outdated bool exec_err bool } @[params] struct ErrorOptions { details string verbose bool // is used to only output the error message if the verbose setting is enabled. } const home_dir = os.home_dir() fn parse_query(query []string) ([]Module, []Module) { mut vpm_modules, mut external_modules := []Module{}, []Module{} mut errors := 0 for m in query { ident, version := m.rsplit_once('@') or { m, '' } mut mod := if ident.starts_with('https://') { name := get_name_from_url(ident) or { vpm_error(err.msg()) errors++ continue } install_path := os.real_path(os.join_path(settings.vmodules_path, name)) if !has_vmod(ident, install_path) { errors++ continue } Module{ name: name url: ident install_path: install_path install_path_fmted: fmt_mod_path(install_path) is_external: true } } else { info := get_mod_vpm_info(ident) or { vpm_error('failed to retrieve metadata for `${ident}`.', details: err.msg()) errors++ continue } name_normalized := info.name.replace('-', '_').to_lower() name_as_path := name_normalized.replace('.', os.path_separator) install_path := os.real_path(os.join_path(settings.vmodules_path, name_as_path)) Module{ name: info.name url: info.url vcs: info.vcs install_path: install_path install_path_fmted: fmt_mod_path(install_path) } } mod.version = version if v := os.execute_opt('git ls-remote --tags ${mod.install_path}') { mod.is_installed = true mod.installed_version = v.output.all_after_last('/').trim_space() } if mod.is_external { external_modules << mod } else { vpm_modules << mod } } if errors > 0 && errors == query.len { exit(1) } return vpm_modules, external_modules } fn has_vmod(url string, install_path string) bool { if os.exists((os.join_path(install_path, 'v.mod'))) { // Safe time fetchting the repo when the module is already installed and has a `v.mod`. return true } head_branch := os.execute_opt('git ls-remote --symref ${url} HEAD') or { vpm_error('failed to find git HEAD for `${url}`.', details: err.msg()) return false }.output.all_after_last('/').all_before(' ').all_before('\t') url_ := if url.ends_with('.git') { url.replace('.git', '') } else { url } manifest_url := '${url_}/blob/${head_branch}/v.mod' vpm_log(@FILE_LINE, @FN, 'manifest_url: ${manifest_url}') has_vmod := http.head(manifest_url) or { vpm_error('failed to retrieve module data for `${url}`.') return false }.status_code == 200 if !has_vmod { vpm_error('failed to find `v.mod` for `${url}`.') return false } return true } fn get_mod_date_info(mut pp pool.PoolProcessor, idx int, wid int) &ModuleDateInfo { mut result := &ModuleDateInfo{ name: pp.get_item[string](idx) } path := get_path_of_existing_module(result.name) or { return result } vcs := vcs_used_in_dir(path) or { return result } is_hg := vcs.cmd == 'hg' mut outputs := []string{} for step in vcs.args.outdated { cmd := '${vcs.cmd} ${vcs.args.path} "${path}" ${step}' res := os.execute(cmd) if res.exit_code < 0 { verbose_println('Error command: ${cmd}') verbose_println('Error details:\n${res.output}') result.exec_err = true return result } if is_hg && res.exit_code == 1 { result.outdated = true return result } outputs << res.output } // vcs.cmd == 'git' if !is_hg && outputs[1] != outputs[2] { result.outdated = true } return result } fn get_mod_vpm_info(name string) !ModuleVpmInfo { if name.len < 2 || (!name[0].is_digit() && !name[0].is_letter()) { return error('invalid module name `${name}`.') } mut errors := []string{} for url in vpm_server_urls { modurl := url + '/api/packages/${name}' verbose_println('Retrieving metadata for `${name}` 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.body.trim_space() == '404' { errors << 'Skipping module `${name}`, since `${url}` reported that `${name}` does not exist.' continue } if r.status_code != 200 { errors << 'Skipping module `${name}`, since `${url}` responded with ${r.status_code} http status code. Please try again later.' continue } s := r.body if s.len > 0 && s[0] != `{` { errors << 'Invalid json data' errors << s.trim_space().limit(100) + '...' continue } mod := json.decode(ModuleVpmInfo, 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 } vpm_log(@FILE_LINE, @FN, 'name: ${name}; mod: ${mod}') return mod } return error(errors.join_lines()) } fn get_name_from_url(raw_url string) !string { url := urllib.parse(raw_url) or { return error('failed to parse module URL `${raw_url}`.') } owner, mut name := url.path.trim_left('/').rsplit_once('/') or { return error('failed to retrieve module name for `${url}`.') } vpm_log(@FILE_LINE, @FN, 'raw_url: ${raw_url}; owner: ${owner}; name: ${name}') name = if name.ends_with('.git') { name.replace('.git', '') } else { name } return name.replace('-', '_').to_lower() } fn get_outdated() ![]string { installed := get_installed_modules() mut outdated := []string{} mut pp := pool.new_pool_processor(callback: get_mod_date_info) pp.work_on_items(installed) for res in pp.get_results[ModuleDateInfo]() { if res.exec_err { return error('failed to check the latest commits for `${res.name}`.') } if res.outdated { outdated << res.name } } return outdated } fn get_all_modules() []string { url := get_working_server_url() r := http.get(url) or { vpm_error(err.msg(), verbose: true) exit(1) } if r.status_code != 200 { vpm_error('failed to search vpm.vlang.io.', details: 'Status code: ${r.status_code}') exit(1) } s := r.body mut read_len := 0 mut modules := []string{} for read_len < s.len { mut start_token := "' // get the start index of the module entry mut start_index := s.index_after(start_token, read_len) if start_index == -1 { start_token = '