From 0117789c3d82d188c25889cb5ea2852a9b00521f Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 26 Feb 2026 20:26:44 +0300 Subject: [PATCH] vpm: support custom mirrors for package installation (fixes #26217) --- cmd/tools/vpm/common.v | 65 +++++++++++++++++++++++++++++++++-- cmd/tools/vpm/install.v | 9 ++--- cmd/tools/vpm/parse.v | 10 +++--- cmd/tools/vpm/settings.v | 41 +++++++++++++++------- cmd/tools/vpm/settings_test.v | 31 +++++++++++++++++ cmd/tools/vpm/update.v | 2 +- vlib/v/help/vpm/install.txt | 2 ++ 7 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 cmd/tools/vpm/settings_test.v diff --git a/cmd/tools/vpm/common.v b/cmd/tools/vpm/common.v index c8048a515..6bdca1987 100644 --- a/cmd/tools/vpm/common.v +++ b/cmd/tools/vpm/common.v @@ -15,6 +15,12 @@ struct ModuleVpmInfo { nr_downloads int } +struct VpmInstallServerSelector { +mut: + selected_url string + candidate_urls []string +} + @[params] struct ErrorOptions { details string @@ -56,12 +62,26 @@ fn active_server_urls() []string { } fn get_mod_vpm_info(name string) !ModuleVpmInfo { + mut selector := VpmInstallServerSelector{ + candidate_urls: if settings.server_urls.len > 0 { + settings.server_urls + } else { + vpm_server_urls + } + } + return get_mod_vpm_info_with_selector(name, mut selector) +} + +fn get_mod_vpm_info_with_selector(name string, mut selector VpmInstallServerSelector) !ModuleVpmInfo { if name.len < 2 || (!name[0].is_digit() && !name[0].is_letter()) { return error('invalid module name `${name}`.') } + if selector.candidate_urls.len == 0 { + return error('no vpm server urls configured.') + } mut errors := []string{} is_initial_selection := selected_server_url(false, '') == '' - for url in active_server_urls() { + for url in selector.metadata_server_urls() { modurl := url + '/api/packages/${name}' verbose_println_more(@FILE_LINE, @FN, 'Retrieving metadata for `${name}` from `${modurl}` by making a GET request ...') r := http.get(modurl) or { @@ -91,6 +111,10 @@ fn get_mod_vpm_info(name string) !ModuleVpmInfo { errors << 'Skipping module `${name}`, since it is missing name or url information.' continue } + if selector.selected_url == '' { + selector.selected_url = url + verbose_println_more(@FILE_LINE, @FN, 'Using `${url}` for this installation.') + } if is_initial_selection { selected_server_url(true, url) } @@ -102,6 +126,31 @@ fn get_mod_vpm_info(name string) !ModuleVpmInfo { return error(final_error) } +fn new_install_server_selector() VpmInstallServerSelector { + return VpmInstallServerSelector{ + candidate_urls: if settings.server_urls.len > 0 { + settings.server_urls + } else { + build_install_server_urls(vpm_server_urls, settings.mirror_urls) + } + } +} + +fn build_install_server_urls(default_urls []string, mirror_urls []string) []string { + mut urls := []string{} + urls << default_urls + urls << mirror_urls + return unique_server_urls(urls) +} + +fn (selector VpmInstallServerSelector) metadata_server_urls() []string { + return if selector.selected_url != '' { + [selector.selected_url] + } else { + selector.candidate_urls + } +} + fn get_ident_from_url(raw_url string) !(string, string) { verbose_println_more(@FILE_LINE, @FN, 'raw_url: ${raw_url}') url := urllib.parse(raw_url) or { return error('failed to parse module URL `${raw_url}`.') } @@ -242,14 +291,24 @@ fn ensure_vmodules_dir_exist() { verbose_println_more(@FILE_LINE, @FN, 'settings.vmodules_path: ${settings.vmodules_path}') } -fn increment_module_download_count(name string) ! { +fn increment_module_download_count(name string, preferred_server_url string) ! { if settings.no_dl_count_increment { println('Skipping download count increment for `${name}`.') return } + server_urls := if preferred_server_url != '' { + unique_server_urls([preferred_server_url]) + } else if settings.server_urls.len > 0 { + settings.server_urls + } else { + vpm_server_urls + } + if server_urls.len == 0 { + return error('no vpm server urls configured.') + } mut errors := []string{} is_initial_selection := selected_server_url(false, '') == '' - for url in active_server_urls() { + for url in server_urls { modurl := url + '/api/packages/${name}/incr_downloads' verbose_println_more(@FILE_LINE, @FN, 'making a POST request to modurl: ${modurl} ...') r := http.post(modurl, '') or { diff --git a/cmd/tools/vpm/install.v b/cmd/tools/vpm/install.v index d0a83fc20..69acc983b 100644 --- a/cmd/tools/vpm/install.v +++ b/cmd/tools/vpm/install.v @@ -15,6 +15,7 @@ fn vpm_install(query []string) { help.print_and_exit('vpm') } + mut selector := new_install_server_selector() mut modules := parse_query(if query.len == 0 { if os.exists('./v.mod') { // Case: `v install` was run in a directory of another V-module to install its dependencies @@ -34,7 +35,7 @@ fn vpm_install(query []string) { } } else { query - }) + }, mut selector) installed_modules := get_installed_modules() @@ -65,10 +66,10 @@ fn vpm_install(query []string) { } } - install_modules(modules) + install_modules(modules, selector.selected_url) } -fn install_modules(modules []Module) { +fn install_modules(modules []Module, selected_server_url string) { vpm_log(@FILE_LINE, @FN, 'modules: ${modules}') mut errors := 0 for m in modules { @@ -84,7 +85,7 @@ fn install_modules(modules []Module) { } } if !m.is_external { - increment_module_download_count(m.name) or { + increment_module_download_count(m.name, selected_server_url) or { vpm_error('failed to increment the download count for `${m.name}`', details: err.msg() ) diff --git a/cmd/tools/vpm/parse.v b/cmd/tools/vpm/parse.v index d109b8e25..0b64b8511 100644 --- a/cmd/tools/vpm/parse.v +++ b/cmd/tools/vpm/parse.v @@ -34,10 +34,10 @@ enum ModuleKind { local } -fn parse_query(query []string) []Module { +fn parse_query(query []string, mut selector VpmInstallServerSelector) []Module { mut p := Parser{} for m in query { - p.parse_module(m) + p.parse_module(m, mut selector) } if p.errors > 0 && p.errors == query.len { exit(1) @@ -45,7 +45,7 @@ fn parse_query(query []string) []Module { return p.modules.values() } -fn (mut p Parser) parse_module(m string) { +fn (mut p Parser) parse_module(m string, mut selector VpmInstallServerSelector) { kind := match true { m.starts_with('https://') { ModuleKind.https } m.starts_with('git@') { ModuleKind.ssh } @@ -126,7 +126,7 @@ fn (mut p Parser) parse_module(m string) { } } else { // VPM registered module. - info := get_mod_vpm_info(ident) or { + info := get_mod_vpm_info_with_selector(ident, mut selector) or { vpm_error('failed to retrieve metadata for `${ident}`.', details: err.msg()) p.errors++ return @@ -189,7 +189,7 @@ fn (mut p Parser) parse_module(m string) { if mod.manifest.dependencies.len > 0 { verbose_println('Found ${mod.manifest.dependencies.len} dependencies for `${mod.name}`: ${mod.manifest.dependencies}.') for d in mod.manifest.dependencies { - p.parse_module(d) + p.parse_module(d, mut selector) } } } diff --git a/cmd/tools/vpm/settings.v b/cmd/tools/vpm/settings.v index 38c6bbf79..2131ca445 100644 --- a/cmd/tools/vpm/settings.v +++ b/cmd/tools/vpm/settings.v @@ -5,8 +5,6 @@ import os.cmdline import log import v.vmod -const server_url_option_names = ['-m', '--mirror', '-server-url', '--server-url', '--server-urls'] - struct VpmSettings { mut: is_help bool @@ -15,6 +13,7 @@ mut: is_force bool is_local bool server_urls []string + mirror_urls []string vmodules_path string tmp_path string no_dl_count_increment bool @@ -71,7 +70,8 @@ fn init_settings() VpmSettings { is_verbose: '-v' in opts || '--verbose' in opts is_force: '-f' in opts || '--force' in opts is_local: is_local - server_urls: parse_server_urls(args) + server_urls: get_server_urls_from_args(args) + mirror_urls: get_mirror_urls_from_args(args) vcs: if '--hg' in opts { .hg } else { .git } vmodules_path: vmodules_path tmp_path: os.join_path(os.vtmp_dir(), 'vpm_modules') @@ -81,16 +81,33 @@ fn init_settings() VpmSettings { } } -fn parse_server_urls(args []string) []string { +fn get_server_urls_from_args(args []string) []string { mut server_urls := []string{} - for option_name in server_url_option_names { - for raw_url in cmdline.options(args, option_name) { - url := raw_url.trim_space().trim_string_right('/') - if url == '' || url in server_urls { - continue - } - server_urls << url + server_urls << cmdline.options(args, '-server-url') + server_urls << cmdline.options(args, '--server-url') + server_urls << cmdline.options(args, '--server-urls') + return unique_server_urls(server_urls) +} + +fn get_mirror_urls_from_args(args []string) []string { + mut mirror_urls := []string{} + mirror_urls << cmdline.options(args, '-m') + mirror_urls << cmdline.options(args, '--mirror') + return unique_server_urls(mirror_urls) +} + +fn unique_server_urls(urls []string) []string { + mut unique_urls := []string{} + for raw_url in urls { + url := normalize_server_url(raw_url) + if url == '' || url in unique_urls { + continue } + unique_urls << url } - return server_urls + return unique_urls +} + +fn normalize_server_url(url string) string { + return url.trim_space().trim_string_right('/') } diff --git a/cmd/tools/vpm/settings_test.v b/cmd/tools/vpm/settings_test.v new file mode 100644 index 000000000..c00848e3d --- /dev/null +++ b/cmd/tools/vpm/settings_test.v @@ -0,0 +1,31 @@ +module main + +fn test_get_server_urls_from_args_supports_all_flags() { + args := ['install', '-server-url', 'https://one.example/', '--server-url', + ' https://two.example ', '--server-urls', 'https://one.example'] + server_urls := get_server_urls_from_args(args) + assert server_urls == ['https://one.example', 'https://two.example'] +} + +fn test_get_mirror_urls_from_args_supports_short_and_long_flags() { + args := ['install', '-m', 'https://mirror1.example/', '--mirror', 'https://mirror2.example', + '-m', 'https://mirror1.example'] + mirror_urls := get_mirror_urls_from_args(args) + assert mirror_urls == ['https://mirror1.example', 'https://mirror2.example'] +} + +fn test_build_install_server_urls_prioritizes_default_servers() { + server_urls := build_install_server_urls(['https://official1.example', + 'https://official2.example'], ['https://mirror1.example', 'https://official2.example']) + assert server_urls == ['https://official1.example', 'https://official2.example', + 'https://mirror1.example'] +} + +fn test_metadata_server_urls_uses_selected_server() { + mut selector := VpmInstallServerSelector{ + candidate_urls: ['https://official.example', 'https://mirror.example'] + } + assert selector.metadata_server_urls() == ['https://official.example', 'https://mirror.example'] + selector.selected_url = 'https://mirror.example' + assert selector.metadata_server_urls() == ['https://mirror.example'] +} diff --git a/cmd/tools/vpm/update.v b/cmd/tools/vpm/update.v index 6da8bebf2..d382dfaaf 100644 --- a/cmd/tools/vpm/update.v +++ b/cmd/tools/vpm/update.v @@ -69,7 +69,7 @@ fn update_module(mut pp pool.PoolProcessor, idx int, wid int) &UpdateResult { println('Updated module `${ident}`.') } // Don't bail if the download count increment has failed. - increment_module_download_count(name) or { vpm_error(err.msg(), verbose: true) } + 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) diff --git a/vlib/v/help/vpm/install.txt b/vlib/v/help/vpm/install.txt index e764181a7..f4011cd5a 100644 --- a/vlib/v/help/vpm/install.txt +++ b/vlib/v/help/vpm/install.txt @@ -12,5 +12,7 @@ Options: --help, -h Prints the help menu --once Only install the package if it was not previously installed -v Print more details about the performed operation. + -m, --mirror Adds a fallback vpm mirror for package metadata and download count requests. + Can be given multiple times. Official vpm servers are still tried first. -server-url When doing network operations, use this vpm server. Can be given multiple times. -- 2.39.5