diff --git a/cmd/tools/vpm/parse.v b/cmd/tools/vpm/parse.v index f8063efa6638bb..873744d3605023 100644 --- a/cmd/tools/vpm/parse.v +++ b/cmd/tools/vpm/parse.v @@ -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 @@ -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 { @@ -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) { diff --git a/cmd/tools/vpm/update.v b/cmd/tools/vpm/update.v index 6da8bebf214f82..5ca91c619b534f 100644 --- a/cmd/tools/vpm/update.v +++ b/cmd/tools/vpm/update.v @@ -4,10 +4,6 @@ import os import sync.pool import v.help -struct UpdateSession { - idents []string -} - pub struct UpdateResult { mut: success bool @@ -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 `. - 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} } diff --git a/cmd/tools/vpm/update_test.v b/cmd/tools/vpm/update_test.v index 528c83debabfcf..94a4e11c77952f 100644 --- a/cmd/tools/vpm/update_test.v +++ b/cmd/tools/vpm/update_test.v @@ -1,4 +1,6 @@ // vtest retry: 3 +module main + import os import rand import test_utils @@ -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