tools: extend vpm to support specifying git version tags when installing modules (#19835)

This commit is contained in:
Turiiya 2023-11-11 13:40:34 +01:00 committed by GitHub
parent 44cf1451bc
commit 19bc16516f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 52 deletions

View file

@ -19,6 +19,7 @@ mut:
version string // specifies the requested version. version string // specifies the requested version.
install_path string install_path string
is_installed bool is_installed bool
is_external bool
installed_version string installed_version string
} }
@ -48,22 +49,22 @@ fn parse_query(query []string) ([]Module, []Module) {
mut errors := 0 mut errors := 0
for m in query { for m in query {
ident, version := m.rsplit_once('@') or { m, '' } ident, version := m.rsplit_once('@') or { m, '' }
mut is_external := false
mut mod := if ident.starts_with('https://') { mut mod := if ident.starts_with('https://') {
is_external = true
name := get_name_from_url(ident) or { name := get_name_from_url(ident) or {
vpm_error(err.msg()) vpm_error(err.msg())
errors++ errors++
continue continue
} }
if !has_vmod(ident) { install_path := os.real_path(os.join_path(settings.vmodules_path, name))
if !has_vmod(ident, install_path) {
errors++ errors++
continue continue
} }
Module{ Module{
name: name name: name
url: ident url: ident
install_path: os.real_path(os.join_path(settings.vmodules_path, name)) install_path: install_path
is_external: true
} }
} else { } else {
info := get_mod_vpm_info(ident) or { info := get_mod_vpm_info(ident) or {
@ -85,7 +86,7 @@ fn parse_query(query []string) ([]Module, []Module) {
mod.is_installed = true mod.is_installed = true
mod.installed_version = v.output.all_after_last('/').trim_space() mod.installed_version = v.output.all_after_last('/').trim_space()
} }
if is_external { if mod.is_external {
external_modules << mod external_modules << mod
} else { } else {
vpm_modules << mod vpm_modules << mod
@ -97,7 +98,11 @@ fn parse_query(query []string) ([]Module, []Module) {
return vpm_modules, external_modules return vpm_modules, external_modules
} }
fn has_vmod(url string) bool { 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 { head_branch := os.execute_opt('git ls-remote --symref ${url} HEAD') or {
vpm_error('failed to find git HEAD for `${url}`.', details: err.msg()) vpm_error('failed to find git HEAD for `${url}`.', details: err.msg())
return false return false

View file

@ -50,10 +50,10 @@ fn test_install_dependencies_in_module_dir() {
assert v_mod.dependencies == ['markdown', 'pcre', 'https://github.com/spytheman/vtray'] assert v_mod.dependencies == ['markdown', 'pcre', 'https://github.com/spytheman/vtray']
// Run `v install` // Run `v install`
res := os.execute_or_exit('${v} install') res := os.execute_or_exit('${v} install')
assert res.output.contains('Detected v.mod file inside the project directory. Using it...') assert res.output.contains('Detected v.mod file inside the project directory. Using it...'), res.output
assert res.output.contains('Installing module `markdown`') assert res.output.contains('Installing `markdown`'), res.output
assert res.output.contains('Installing module `pcre`') assert res.output.contains('Installing `pcre`'), res.output
assert res.output.contains('Installing module `vtray`') assert res.output.contains('Installing `vtray`'), res.output
assert get_mod_name(os.join_path(test_path, 'markdown', 'v.mod')) == 'markdown' assert get_mod_name(os.join_path(test_path, 'markdown', 'v.mod')) == 'markdown'
assert get_mod_name(os.join_path(test_path, 'pcre', 'v.mod')) == 'pcre' assert get_mod_name(os.join_path(test_path, 'pcre', 'v.mod')) == 'pcre'
assert get_mod_name(os.join_path(test_path, 'vtray', 'v.mod')) == 'vtray' assert get_mod_name(os.join_path(test_path, 'vtray', 'v.mod')) == 'vtray'
@ -61,9 +61,9 @@ fn test_install_dependencies_in_module_dir() {
fn test_resolve_external_dependencies_during_module_install() { fn test_resolve_external_dependencies_during_module_install() {
res := os.execute_or_exit('${v} install https://github.com/ttytm/emoji-mart-desktop') res := os.execute_or_exit('${v} install https://github.com/ttytm/emoji-mart-desktop')
assert res.output.contains('Resolving 2 dependencies') assert res.output.contains('Resolving 2 dependencies'), res.output
assert res.output.contains('Installing module `webview`') assert res.output.contains('Installing `webview`'), res.output
assert res.output.contains('Installing module `miniaudio`') assert res.output.contains('Installing `miniaudio`'), res.output
// The external dependencies should have been installed to `<vmodules_dir>/<dependency_name>` // The external dependencies should have been installed to `<vmodules_dir>/<dependency_name>`
assert get_mod_name(os.join_path(test_path, 'webview', 'v.mod')) == 'webview' assert get_mod_name(os.join_path(test_path, 'webview', 'v.mod')) == 'webview'
assert get_mod_name(os.join_path(test_path, 'miniaudio', 'v.mod')) == 'miniaudio' assert get_mod_name(os.join_path(test_path, 'miniaudio', 'v.mod')) == 'miniaudio'

View file

@ -4,6 +4,12 @@ import os
import v.vmod import v.vmod
import v.help import v.help
enum InstallResult {
installed
failed
skipped
}
fn vpm_install(query []string) { fn vpm_install(query []string) {
if settings.is_help { if settings.is_help {
help.print_and_exit('vpm') help.print_and_exit('vpm')
@ -88,8 +94,8 @@ fn vpm_install_from_vpm(modules []Module) {
last_errors := errors last_errors := errors
vcs := if m.vcs != '' { vcs := if m.vcs != '' {
supported_vcs[m.vcs] or { supported_vcs[m.vcs] or {
errors++
vpm_error('skipping `${m.name}`, since it uses an unsupported version control system `${m.vcs}`.') vpm_error('skipping `${m.name}`, since it uses an unsupported version control system `${m.vcs}`.')
errors++
continue continue
} }
} else { } else {
@ -100,18 +106,19 @@ fn vpm_install_from_vpm(modules []Module) {
errors++ errors++
continue continue
} }
if os.exists(m.install_path) { match m.install(vcs) {
vpm_update([m.name]) .installed {}
continue .failed {
} errors++
m.install(vcs) or { continue
errors++ }
vpm_error(err.msg()) .skipped {
continue continue
}
} }
increment_module_download_count(m.name) or { increment_module_download_count(m.name) or {
errors++
vpm_error('failed to increment the download count for `${m.name}`', details: err.msg()) vpm_error('failed to increment the download count for `${m.name}`', details: err.msg())
errors++
} }
if last_errors == errors { if last_errors == errors {
println('Installed `${m.name}`.') println('Installed `${m.name}`.')
@ -124,27 +131,27 @@ fn vpm_install_from_vpm(modules []Module) {
} }
fn vpm_install_from_vcs(modules []Module) { fn vpm_install_from_vcs(modules []Module) {
mut errors := 0 vpm_log(@FILE_LINE, @FN, 'modules: ${modules}')
vcs := supported_vcs[settings.vcs] vcs := supported_vcs[settings.vcs]
vcs.is_executable() or {
vpm_error(err.msg())
exit(1)
}
urls := modules.map(it.url) urls := modules.map(it.url)
mut errors := 0
for m in modules { for m in modules {
vpm_log(@FILE_LINE, @FN, 'module: ${m}') vpm_log(@FILE_LINE, @FN, 'module: ${m}')
last_errors := errors last_errors := errors
if os.exists(m.install_path) { match m.install(vcs) {
vpm_update([m.name]) .installed {}
continue .failed {
errors++
continue
}
.skipped {
continue
}
} }
vcs.is_executable() or {
vpm_error(err.msg())
errors++
continue
}
m.install(vcs) or {
errors++
vpm_error(err.msg())
continue
}
// Note: increment error count when v.mod becomes mandatory for external modules.
manifest := get_manifest(m.install_path) or { continue } manifest := get_manifest(m.install_path) or { continue }
final_path := os.real_path(os.join_path(settings.vmodules_path, manifest.name.replace('-', final_path := os.real_path(os.join_path(settings.vmodules_path, manifest.name.replace('-',
'_').to_lower())) '_').to_lower()))
@ -201,13 +208,57 @@ fn vpm_install_from_vcs(modules []Module) {
} }
} }
fn (m Module) install(vcs &VCS) ! { fn (m Module) install(vcs &VCS) InstallResult {
cmd := '${vcs.cmd} ${vcs.args.install} "${m.url}" "${m.install_path}"' if m.is_installed {
// Case: installed, but not an explicit version. Update instead of continuing the installation.
if m.version == '' && m.installed_version == '' {
vpm_update([if m.is_external { m.url } else { m.name }])
return .skipped
}
// Case: installed, but conflicting. Confirmation or -[-f]orce flag required.
if settings.is_force || m.confirm_install() {
m.remove() or {
vpm_error('failed to remove `${m.name}`.', details: err.msg())
return .failed
}
} else {
return .skipped
}
}
install_arg := if m.version != '' {
'${vcs.args.install} --single-branch -b ${m.version}'
} else {
vcs.args.install
}
cmd := '${vcs.cmd} ${install_arg} "${m.url}" "${m.install_path}"'
vpm_log(@FILE_LINE, @FN, 'command: ${cmd}') vpm_log(@FILE_LINE, @FN, 'command: ${cmd}')
println('Installing module `${m.name}` from `${m.url}` to `${m.install_path}` ...') println('Installing `${m.name}`...')
os.execute_opt(cmd) or { verbose_println(' cloning from `${m.url}` to `${m.install_path}`')
vpm_log(@FILE_LINE, @FN, 'cmd output: ${err}') res := os.execute_opt(cmd) or {
return error('failed to install module `${m.name}` to `${m.install_path}`.') vpm_error('failed to install `${m.name}`.', details: err.msg())
return .failed
}
vpm_log(@FILE_LINE, @FN, 'cmd output: ${res.output}')
return .installed
}
fn (m Module) confirm_install() bool {
if m.installed_version == m.version {
println('Module `${m.name}${at_version(m.installed_version)}` is already installed, use --force to overwrite.')
return false
} else {
install_version := at_version(if m.version == '' { 'latest' } else { m.version })
println('Module `${m.name}${at_version(m.installed_version)}` is already installed at `${m.install_path}`.')
input := os.input('Replace it with `${m.name}${install_version}`? [Y/n]: ')
match input.to_lower() {
'', 'y' {
return true
}
else {
verbose_println('Skipping `${m.name}`.')
return false
}
}
} }
} }
@ -220,3 +271,7 @@ fn (m Module) remove() ! {
} }
verbose_println('Removed `${m.name}`.') verbose_println('Removed `${m.name}`.')
} }
fn at_version(version string) string {
return if version != '' { '@${version}' } else { '' }
}

View file

@ -24,7 +24,7 @@ fn testsuite_end() {
fn test_install_from_vpm_ident() { fn test_install_from_vpm_ident() {
res := os.execute_or_exit('${v} install nedpals.args') res := os.execute_or_exit('${v} install nedpals.args')
assert res.output.contains('Skipping download count increment for `nedpals.args`.') assert res.output.contains('Skipping download count increment for `nedpals.args`.'), res.output
mod := vmod.from_file(os.join_path(test_path, 'nedpals', 'args', 'v.mod')) or { mod := vmod.from_file(os.join_path(test_path, 'nedpals', 'args', 'v.mod')) or {
assert false, err.msg() assert false, err.msg()
return return
@ -45,7 +45,7 @@ fn test_install_from_vpm_short_ident() {
fn test_install_from_git_url() { fn test_install_from_git_url() {
res := os.execute_or_exit('${v} install https://github.com/vlang/markdown') res := os.execute_or_exit('${v} install https://github.com/vlang/markdown')
assert res.output.contains('Installing module `markdown` from `https://github.com/vlang/markdown`') assert res.output.contains('Installing `markdown`'), res.output
mod := vmod.from_file(os.join_path(test_path, 'markdown', 'v.mod')) or { mod := vmod.from_file(os.join_path(test_path, 'markdown', 'v.mod')) or {
assert false, err.msg() assert false, err.msg()
return return
@ -86,7 +86,7 @@ fn test_install_once() {
install_cmd := '${@VEXE} install https://github.com/vlang/markdown https://github.com/vlang/pcre --once -v' install_cmd := '${@VEXE} install https://github.com/vlang/markdown https://github.com/vlang/pcre --once -v'
// Try installing two modules, one of which is already installed. // Try installing two modules, one of which is already installed.
mut res := os.execute_or_exit(install_cmd) mut res := os.execute_or_exit(install_cmd)
assert res.output.contains("Already installed modules: ['markdown']") assert res.output.contains("Already installed modules: ['markdown']"), res.output
mod := vmod.from_file(os.join_path(test_path, 'pcre', 'v.mod')) or { mod := vmod.from_file(os.join_path(test_path, 'pcre', 'v.mod')) or {
assert false, err.msg() assert false, err.msg()
return return
@ -99,7 +99,7 @@ fn test_install_once() {
// Try installing two modules that are both already installed. // Try installing two modules that are both already installed.
res = os.execute_or_exit(install_cmd) res = os.execute_or_exit(install_cmd)
assert res.output.contains('All modules are already installed.') assert res.output.contains('All modules are already installed.'), res.output
assert md_last_modified == os.file_last_mod_unix(os.join_path(test_path, 'markdown', assert md_last_modified == os.file_last_mod_unix(os.join_path(test_path, 'markdown',
'v.mod')) 'v.mod'))
} }
@ -108,13 +108,13 @@ fn test_missing_repo_name_in_url() {
incomplete_url := 'https://github.com/vlang' incomplete_url := 'https://github.com/vlang'
res := os.execute('${v} install ${incomplete_url}') res := os.execute('${v} install ${incomplete_url}')
assert res.exit_code == 1 assert res.exit_code == 1
assert res.output.contains('failed to retrieve module name for `${incomplete_url}`') assert res.output.contains('failed to retrieve module name for `${incomplete_url}`'), res.output
} }
fn test_missing_vmod_in_url() { fn test_missing_vmod_in_url() {
assert has_vmod('https://github.com/vlang/v') // head branch == `master`. assert has_vmod('https://github.com/vlang/v', '') // head branch == `master`.
assert has_vmod('https://github.com/v-analyzer/v-analyzer') // head branch == `main`. assert has_vmod('https://github.com/v-analyzer/v-analyzer', '') // head branch == `main`.
assert !has_vmod('https://github.com/octocat/octocat.github.io') // not a V module. assert !has_vmod('https://github.com/octocat/octocat.github.io', '') // not a V module.
res := os.execute('${v} install https://github.com/octocat/octocat.github.io') res := os.execute('${v} install https://github.com/octocat/octocat.github.io')
assert res.exit_code == 1 assert res.exit_code == 1
assert res.output.contains('failed to find `v.mod` for `https://github.com/octocat/octocat.github.io`'), res.output assert res.output.contains('failed to find `v.mod` for `https://github.com/octocat/octocat.github.io`'), res.output

View file

@ -0,0 +1,95 @@
// vtest flaky: true
// vtest retry: 3
module main
import os
import v.vmod
const (
v = os.quoted_path(@VEXE)
test_path = os.join_path(os.vtmp_dir(), 'vpm_install_version_test')
)
fn testsuite_begin() {
os.setenv('VMODULES', test_path, true)
os.setenv('VPM_DEBUG', '', true)
os.setenv('VPM_NO_INCREMENT', '1', true)
}
fn testsuite_end() {
os.rmdir_all(test_path) or {}
}
fn get_mod_name_and_version(path string) (string, string) {
mod := vmod.from_file(os.join_path(test_path, path, 'v.mod')) or {
eprintln(err)
return '', ''
}
return mod.name, mod.version
}
fn test_install_from_vpm_with_git_version_tag() {
ident := 'ttytm.webview'
mut tag := 'v0.6.0'
mut res := os.execute_or_exit('${v} install ${ident}@${tag}')
assert res.output.contains('Installing `${ident}`'), res.output
assert res.output.contains('Installed `${ident}`'), res.output
mut name, mut version := get_mod_name_and_version(os.join_path('ttytm', 'webview'))
assert name == 'webview'
assert version == '0.6.0'
// Install same version without force flag.
res = os.execute_or_exit('${v} install ${ident}@${tag}')
assert res.output.contains('Module `${ident}@${tag}` is already installed, use --force to overwrite'), res.output
// Install another version, add force flag to surpass confirmation.
tag = 'v0.5.0'
res = os.execute_or_exit('${v} install -f ${ident}@${tag}')
assert res.output.contains('Installed `${ident}`'), res.output
name, version = get_mod_name_and_version(os.join_path('ttytm', 'webview'))
assert name == 'webview'
assert version == '0.5.0'
// Install invalid version.
tag = '6.0'
res = os.execute('${v} install -f ${ident}@${tag}')
assert res.exit_code == 1
assert res.output.contains('failed to install `${ident}`'), res.output
// Install invalid version verbose.
res = os.execute('${v} install -f -v ${ident}@${tag}')
assert res.exit_code == 1
assert res.output.contains('failed to install `${ident}`'), res.output
assert res.output.contains('Remote branch 6.0 not found in upstream origin'), res.output
// Install without version tag after a version was installed
res = os.execute_or_exit('${v} install -f ${ident}')
assert res.output.contains('Installing `${ident}`'), res.output
// Re-install latest version (without a tag). Should trigger an update, force should not be required.
res = os.execute_or_exit('${v} install ${ident}')
assert res.output.contains('Updating module `${ident}`'), res.output
}
fn test_install_from_url_with_git_version_tag() {
url := 'https://github.com/vlang/vsl'
mut tag := 'v0.1.50'
mut res := os.execute_or_exit('v install ${url}@${tag}')
assert res.output.contains('Installing `vsl`'), res.output
assert res.output.contains('Installed `vsl`'), res.output
mut name, mut version := get_mod_name_and_version('vsl')
assert name == 'vsl'
assert version == '0.1.50'
// Install same version without force flag.
res = os.execute_or_exit('${v} install ${url}@${tag}')
assert res.output.contains('Module `vsl@${tag}` is already installed, use --force to overwrite'), res.output
// Install another version, add force flag to surpass confirmation.
tag = 'v0.1.47'
res = os.execute_or_exit('${v} install -f ${url}@${tag}')
assert res.output.contains('Installed `vsl`'), res.output
name, version = get_mod_name_and_version('vsl')
assert name == 'vsl'
assert version == '0.1.47'
// Install invalid version.
tag = 'abc'
res = os.execute('${v} install -f ${url}@${tag}')
assert res.exit_code == 1
// Install invalid version verbose.
res = os.execute('${v} install -f -v ${url}@${tag}')
assert res.exit_code == 1
assert res.output.contains('Remote branch abc not found in upstream origin'), res.output
}

View file

@ -9,6 +9,7 @@ mut:
is_help bool is_help bool
is_once bool is_once bool
is_verbose bool is_verbose bool
is_force bool
server_urls []string server_urls []string
vcs string vcs string
vmodules_path string vmodules_path string
@ -26,6 +27,7 @@ fn init_settings() VpmSettings {
is_help: '-h' in opts || '--help' in opts || 'help' in cmds is_help: '-h' in opts || '--help' in opts || 'help' in cmds
is_once: '--once' in opts is_once: '--once' in opts
is_verbose: '-v' in opts is_verbose: '-v' in opts
is_force: '-f' in opts || '--force' in opts
vcs: if '--hg' in opts { 'hg' } else { 'git' } vcs: if '--hg' in opts { 'hg' } else { 'git' }
server_urls: cmdline.options(args, '--server-urls') server_urls: cmdline.options(args, '--server-urls')
vmodules_path: os.vmodules_dir() vmodules_path: os.vmodules_dir()