v/cmd/tools/vpm.v

497 lines
13 KiB
V

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 := '<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, 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.options(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)
}
}