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 = '