Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vpm.tools: add parse functions to handle module updates #20214

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 112 additions & 6 deletions cmd/tools/vpm/parse.v
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import os
import net.http
import v.vmod

struct Module {
struct Parser {
mut:
modules map[string]Module
checked_settings_vcs bool
errors int
}

pub struct Module {
mut:
name string
url string
Expand All @@ -19,11 +26,10 @@ mut:
manifest vmod.Manifest
}

struct Parser {
mut:
modules map[string]Module
checked_settings_vcs bool
errors int
struct InstalledModule {
path string
is_global bool
url string
}

enum ModuleKind {
Expand Down Expand Up @@ -207,6 +213,106 @@ fn (mut m Module) get_installed() {
}
}

fn (mut p Parser) parse_outdated() {
for entry in os.ls(settings.vmodules_path) or { return } {
path := os.join_path(settings.vmodules_path, entry)
if entry in excluded_dirs || !os.is_dir(path) {
continue
}
// Global modules `vmodules_dir/module`.
if os.exists(os.join_path(path, 'v.mod')) {
if !is_outdated(path) {
continue
}
vcs := vcs_used_in_dir(path) or { continue }
args := vcs_info[vcs].args
// TODO: mercurial
url := os.execute_opt('${vcs.str()} ${args.path} ${path} config --get remote.origin.url') or {
vpm_error('failed to get url for `${entry}`.', details: err.msg())
continue
}.output.trim_space()
vpm_log(@FILE_LINE, @FN, 'url: ${url}')
if url.starts_with('https://github.com/vlang')
|| url.starts_with('git@github.com:vlang') {
p.parse_module(entry)
} else {
p.parse_module(url)
}
continue
}
// Modules under publisher namespace `vmodules_dir/publisher/module`.
for mod in os.ls(path) or { continue } {
mod_path := os.join_path(path, mod)
if os.exists(os.join_path(mod_path, 'v.mod')) && is_outdated(mod_path) {
p.parse_module('${entry}.${mod}')
}
}
}
}

fn (mut p Parser) parse_update_query(query []string) {
q_urls := query.filter(it.starts_with('https://') || it.starts_with('git@'))
mut installed := map[string]InstalledModule{}
for entry in os.ls(settings.vmodules_path) or { return } {
path := os.join_path(settings.vmodules_path, entry)
if entry in excluded_dirs || !os.is_dir(path) {
continue
}
// Global modules `vmodules_dir/module`.
if os.exists(os.join_path(path, 'v.mod')) {
vcs := vcs_used_in_dir(path) or { continue }
args := vcs_info[vcs].args
// TODO: mercurial
url := os.execute_opt('${vcs.str()} ${args.path} ${path} config --get remote.origin.url') or {
vpm_error('failed to get url for `${entry}`.', details: err.msg())
continue
}.output.trim_space()
vpm_log(@FILE_LINE, @FN, 'url: ${url}')
mod := InstalledModule{
path: path
is_global: true
url: if url.starts_with('https://github.com/vlang')
|| url.starts_with('git@github.com:vlang') {
''
} else {
url
}
}
if url in q_urls {
installed[url] = mod
} else {
installed[entry] = mod
}
continue
}
// Modules under a publisher namespace `vmodules_dir/publisher/module`.
for mod in os.ls(path) or { continue } {
mod_path := os.join_path(path, mod)
if os.exists(os.join_path(mod_path, 'v.mod')) {
installed['${entry}.${mod}'] = InstalledModule{
path: mod_path
}
}
}
}
for m in query {
if m !in installed {
vpm_error('failed to update `${m}`. Not installed.')
p.errors++
continue
}
if !is_outdated(installed[m].path) {
verbose_println('Skipping `${m}`. Already up to date.')
continue
}
if installed[m].is_global && installed[m].url != '' {
p.parse_module(installed[m].url)
} else {
p.parse_module(m)
}
}
}

fn get_tmp_path(relative_path string) !string {
tmp_path := os.real_path(os.join_path(settings.tmp_path, relative_path))
if os.exists(tmp_path) {
Expand Down
77 changes: 36 additions & 41 deletions cmd/tools/vpm/update.v
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import os
import sync.pool
import v.help

struct UpdateSession {
idents []string
}

pub struct UpdateResult {
mut:
success bool
Expand All @@ -17,61 +13,60 @@ fn vpm_update(query []string) {
if settings.is_help {
help.print_and_exit('update')
}
idents := if query.len == 0 {
get_installed_modules()
mut p := Parser{}
if query.len == 0 {
p.parse_outdated()
} else {
query.clone()
p.parse_update_query(query)
}
// In case dependencies have changed, new modules may need to be installed.
mut to_update, mut to_install := []Module{}, []Module{}
for m in p.modules.values() {
if m.is_installed {
to_update << m
} else {
to_install << m
}
}
if to_update.len == 0 {
if p.errors > 0 {
exit(1)
} else {
println('All modules are up to date.')
exit(0)
}
}
vpm_log(@FILE_LINE, @FN, 'Modules to update: ${to_update}')
vpm_log(@FILE_LINE, @FN, 'Modules to install: ${to_install}')
mut pp := pool.new_pool_processor(callback: update_module)
ctx := UpdateSession{idents}
pp.set_shared_context(ctx)
pp.work_on_items(idents)
pp.work_on_items(to_update)
mut errors := 0
for res in pp.get_results[UpdateResult]() {
if !res.success {
errors++
continue
}
}
if errors > 0 {
if to_install.len != 0 {
install_modules(to_install)
}
if p.errors > 0 || errors > 0 {
exit(1)
}
}

fn update_module(mut pp pool.PoolProcessor, idx int, wid int) &UpdateResult {
ident := pp.get_item[string](idx)
// Usually, the module `ident`ifier. `get_name_from_url` is only relevant for `v update <module_url>`.
name := get_name_from_url(ident) or { ident }
install_path := get_path_of_existing_module(ident) or {
vpm_error('failed to find path for `${name}`.', verbose: true)
return &UpdateResult{}
}
vcs := vcs_used_in_dir(install_path) or {
vpm_error('failed to find version control system for `${name}`.', verbose: true)
return &UpdateResult{}
}
vcs.is_executable() or {
vpm_error(err.msg())
return &UpdateResult{}
}
m := pp.get_item[Module](idx)
vcs := m.vcs or { settings.vcs }
args := vcs_info[vcs].args
cmd := [vcs.str(), args.path, os.quoted_path(install_path), args.update].join(' ')
vpm_log(@FILE_LINE, @FN, 'cmd: ${cmd}')
println('Updating module `${name}` in `${fmt_mod_path(install_path)}`...')
cmd := [vcs.str(), args.path, os.quoted_path(m.install_path), args.update].join(' ')
vpm_log(@FILE_LINE, @FN, '> cmd: ${cmd}')
println('Updating module `${m.name}` in `${fmt_mod_path(m.install_path)}`...')
res := os.execute_opt(cmd) or {
vpm_error('failed to update module `${name}` in `${install_path}`.', details: err.msg())
vpm_error('failed to update module `${m.name}` in `${m.install_path}`.', details: err.msg())
return &UpdateResult{}
}
vpm_log(@FILE_LINE, @FN, 'cmd output: ${res.output.trim_space()}')
if res.output.contains('Already up to date.') {
println('Skipped module `${ident}`. Already up to date.')
} else {
println('Updated module `${ident}`.')
}
vpm_log(@FILE_LINE, @FN, '>> output: ${res.output.trim_space()}')
// Don't bail if the download count increment has failed.
increment_module_download_count(name) or { vpm_error(err.msg(), verbose: true) }
ctx := unsafe { &UpdateSession(pp.get_shared_context()) }
vpm_log(@FILE_LINE, @FN, 'ident: ${ident}; ctx: ${ctx}')
resolve_dependencies(get_manifest(install_path), ctx.idents)
increment_module_download_count(m.name) or { vpm_error(err.msg(), verbose: true) }
return &UpdateResult{true}
}
74 changes: 56 additions & 18 deletions cmd/tools/vpm/update_test.v
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// vtest retry: 3
module main

import os
import rand
import test_utils
Expand All @@ -18,39 +20,75 @@ fn testsuite_end() {
os.rmdir_all(test_path) or {}
}

// Tests if `v update` detects installed modules and runs successfully.
fn test_update() {
os.execute_or_exit('${v} install pcre')
os.execute_or_exit('${v} install nedpals.args')
os.execute_or_exit('${v} install https://github.com/spytheman/vtray')
for m in ['pcre', 'https://github.com/spytheman/vtray', 'nedpals.args', 'ttytm.webview'] {
os.execute_or_exit('${v} install ${m}')
}
// "outdate" some modules, by removing their last commit.
for m in ['pcre', 'vtray', os.join_path('nedpals', 'args')] {
path := os.join_path(test_path, m)
os.execute_or_exit('git -C ${path} fetch --unshallow')
os.execute_or_exit('git -C ${path} reset --hard HEAD~')
assert is_outdated(path)
}
// Case: Run `v update` (without args).
res := os.execute('${v} update')
assert res.exit_code == 0, res.str()
assert res.output.contains('Updating module `pcre`'), res.output
assert res.output.contains('Updating module `nedpals.args`'), res.output
assert res.output.contains('Updating module `vtray`'), res.output
assert res.output.contains('Skipping download count increment for `nedpals.args`.'), res.output
assert res.output.contains('Updating module `nedpals.args`'), res.output
assert res.output.contains('Skipping download count increment for `pcre`.'), res.output
assert res.output.contains('Skipping download count increment for `nedpals.args`.'), res.output
assert !res.output.contains('Updating module `ttytm.webview`'), res.output
assert !res.output.contains('Skipping download count increment for `ttytm.webview`.'), res.output
for m in ['pcre', 'vtray', os.join_path('nedpals', 'args')] {
assert !is_outdated(os.join_path(test_path, m))
}
}

fn test_update_idents() {
mut res := os.execute('${v} update pcre')
fn test_update_short_ident() {
os.execute_or_exit('git -C ${os.join_path(test_path, 'pcre')} reset --hard HEAD~')
res := os.execute('${v} update pcre')
assert res.exit_code == 0, res.str()
assert res.output.contains('Updating module `pcre`'), res.output
res = os.execute('${v} update nedpals.args vtray')
}

fn test_update_ident() {
os.execute_or_exit('git -C ${os.join_path(test_path, 'nedpals', 'args')} reset --hard HEAD~')
res := os.execute('${v} update nedpals.args')
assert res.exit_code == 0, res.str()
assert res.output.contains('Updating module `vtray`'), res.output
assert res.output.contains('Updating module `nedpals.args`'), res.output
// Update installed module using its url.
res = os.execute('${v} update https://github.com/spytheman/vtray')
}

fn test_update_url() {
os.execute_or_exit('git -C ${os.join_path(test_path, 'vtray')} reset --hard HEAD~')
res := os.execute('${v} update https://github.com/spytheman/vtray')
assert res.exit_code == 0, res.str()
assert res.output.contains('Updating module `vtray`'), res.output
}

fn test_update_multi_ident() {
os.execute_or_exit('git -C ${os.join_path(test_path, 'nedpals', 'args')} reset --hard HEAD~')
os.execute_or_exit('git -C ${os.join_path(test_path, 'vtray')} reset --hard HEAD~')
res := os.execute('${v} update nedpals.args vtray')
assert res.exit_code == 0, res.str()
assert res.output.contains('Updating module `nedpals.args`'), res.output
assert res.output.contains('Updating module `vtray`'), res.output
// Try update not installed.
res = os.execute('${v} update vsl')
}

fn test_update_not_installed() {
res := os.execute('${v} update vsl')
assert res.exit_code == 1, res.str()
assert res.output.contains('failed to find `vsl`'), res.output
// Try update mixed.
res = os.execute('${v} update pcre vsl')
assert res.output.contains('failed to update `vsl`. Not installed.'), res.output
}

fn test_update_mixed_installed_not_installed() {
os.execute_or_exit('git -C ${os.join_path(test_path, 'pcre')} reset --hard HEAD~')
res := os.execute('${v} update pcre vsl')
assert res.exit_code == 1, res.str()
assert res.output.contains('Updating module `pcre`'), res.output
assert res.output.contains('failed to find `vsl`'), res.output
assert res.output.contains('failed to update `vsl`. Not installed.'), res.output
}

// TODO: hg tests
// TODO: recursive test
Loading