Skip to content

Commit 19bc165

Browse files
authored
tools: extend vpm to support specifying git version tags when installing modules (#19835)
1 parent 44cf145 commit 19bc165

6 files changed

Lines changed: 209 additions & 52 deletions

File tree

cmd/tools/vpm/common.v

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mut:
1919
version string // specifies the requested version.
2020
install_path string
2121
is_installed bool
22+
is_external bool
2223
installed_version string
2324
}
2425

@@ -48,22 +49,22 @@ fn parse_query(query []string) ([]Module, []Module) {
4849
mut errors := 0
4950
for m in query {
5051
ident, version := m.rsplit_once('@') or { m, '' }
51-
mut is_external := false
5252
mut mod := if ident.starts_with('https://') {
53-
is_external = true
5453
name := get_name_from_url(ident) or {
5554
vpm_error(err.msg())
5655
errors++
5756
continue
5857
}
59-
if !has_vmod(ident) {
58+
install_path := os.real_path(os.join_path(settings.vmodules_path, name))
59+
if !has_vmod(ident, install_path) {
6060
errors++
6161
continue
6262
}
6363
Module{
6464
name: name
6565
url: ident
66-
install_path: os.real_path(os.join_path(settings.vmodules_path, name))
66+
install_path: install_path
67+
is_external: true
6768
}
6869
} else {
6970
info := get_mod_vpm_info(ident) or {
@@ -85,7 +86,7 @@ fn parse_query(query []string) ([]Module, []Module) {
8586
mod.is_installed = true
8687
mod.installed_version = v.output.all_after_last('/').trim_space()
8788
}
88-
if is_external {
89+
if mod.is_external {
8990
external_modules << mod
9091
} else {
9192
vpm_modules << mod
@@ -97,7 +98,11 @@ fn parse_query(query []string) ([]Module, []Module) {
9798
return vpm_modules, external_modules
9899
}
99100

100-
fn has_vmod(url string) bool {
101+
fn has_vmod(url string, install_path string) bool {
102+
if os.exists((os.join_path(install_path, 'v.mod'))) {
103+
// Safe time fetchting the repo when the module is already installed and has a `v.mod`.
104+
return true
105+
}
101106
head_branch := os.execute_opt('git ls-remote --symref ${url} HEAD') or {
102107
vpm_error('failed to find git HEAD for `${url}`.', details: err.msg())
103108
return false

cmd/tools/vpm/dependency_test.v

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,20 @@ fn test_install_dependencies_in_module_dir() {
5050
assert v_mod.dependencies == ['markdown', 'pcre', 'https://github.com/spytheman/vtray']
5151
// Run `v install`
5252
res := os.execute_or_exit('${v} install')
53-
assert res.output.contains('Detected v.mod file inside the project directory. Using it...')
54-
assert res.output.contains('Installing module `markdown`')
55-
assert res.output.contains('Installing module `pcre`')
56-
assert res.output.contains('Installing module `vtray`')
53+
assert res.output.contains('Detected v.mod file inside the project directory. Using it...'), res.output
54+
assert res.output.contains('Installing `markdown`'), res.output
55+
assert res.output.contains('Installing `pcre`'), res.output
56+
assert res.output.contains('Installing `vtray`'), res.output
5757
assert get_mod_name(os.join_path(test_path, 'markdown', 'v.mod')) == 'markdown'
5858
assert get_mod_name(os.join_path(test_path, 'pcre', 'v.mod')) == 'pcre'
5959
assert get_mod_name(os.join_path(test_path, 'vtray', 'v.mod')) == 'vtray'
6060
}
6161

6262
fn test_resolve_external_dependencies_during_module_install() {
6363
res := os.execute_or_exit('${v} install https://github.com/ttytm/emoji-mart-desktop')
64-
assert res.output.contains('Resolving 2 dependencies')
65-
assert res.output.contains('Installing module `webview`')
66-
assert res.output.contains('Installing module `miniaudio`')
64+
assert res.output.contains('Resolving 2 dependencies'), res.output
65+
assert res.output.contains('Installing `webview`'), res.output
66+
assert res.output.contains('Installing `miniaudio`'), res.output
6767
// The external dependencies should have been installed to `<vmodules_dir>/<dependency_name>`
6868
assert get_mod_name(os.join_path(test_path, 'webview', 'v.mod')) == 'webview'
6969
assert get_mod_name(os.join_path(test_path, 'miniaudio', 'v.mod')) == 'miniaudio'

cmd/tools/vpm/install.v

Lines changed: 86 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import os
44
import v.vmod
55
import v.help
66

7+
enum InstallResult {
8+
installed
9+
failed
10+
skipped
11+
}
12+
713
fn vpm_install(query []string) {
814
if settings.is_help {
915
help.print_and_exit('vpm')
@@ -88,8 +94,8 @@ fn vpm_install_from_vpm(modules []Module) {
8894
last_errors := errors
8995
vcs := if m.vcs != '' {
9096
supported_vcs[m.vcs] or {
91-
errors++
9297
vpm_error('skipping `${m.name}`, since it uses an unsupported version control system `${m.vcs}`.')
98+
errors++
9399
continue
94100
}
95101
} else {
@@ -100,18 +106,19 @@ fn vpm_install_from_vpm(modules []Module) {
100106
errors++
101107
continue
102108
}
103-
if os.exists(m.install_path) {
104-
vpm_update([m.name])
105-
continue
106-
}
107-
m.install(vcs) or {
108-
errors++
109-
vpm_error(err.msg())
110-
continue
109+
match m.install(vcs) {
110+
.installed {}
111+
.failed {
112+
errors++
113+
continue
114+
}
115+
.skipped {
116+
continue
117+
}
111118
}
112119
increment_module_download_count(m.name) or {
113-
errors++
114120
vpm_error('failed to increment the download count for `${m.name}`', details: err.msg())
121+
errors++
115122
}
116123
if last_errors == errors {
117124
println('Installed `${m.name}`.')
@@ -124,27 +131,27 @@ fn vpm_install_from_vpm(modules []Module) {
124131
}
125132

126133
fn vpm_install_from_vcs(modules []Module) {
127-
mut errors := 0
134+
vpm_log(@FILE_LINE, @FN, 'modules: ${modules}')
128135
vcs := supported_vcs[settings.vcs]
136+
vcs.is_executable() or {
137+
vpm_error(err.msg())
138+
exit(1)
139+
}
129140
urls := modules.map(it.url)
141+
mut errors := 0
130142
for m in modules {
131143
vpm_log(@FILE_LINE, @FN, 'module: ${m}')
132144
last_errors := errors
133-
if os.exists(m.install_path) {
134-
vpm_update([m.name])
135-
continue
136-
}
137-
vcs.is_executable() or {
138-
vpm_error(err.msg())
139-
errors++
140-
continue
141-
}
142-
m.install(vcs) or {
143-
errors++
144-
vpm_error(err.msg())
145-
continue
145+
match m.install(vcs) {
146+
.installed {}
147+
.failed {
148+
errors++
149+
continue
150+
}
151+
.skipped {
152+
continue
153+
}
146154
}
147-
// Note: increment error count when v.mod becomes mandatory for external modules.
148155
manifest := get_manifest(m.install_path) or { continue }
149156
final_path := os.real_path(os.join_path(settings.vmodules_path, manifest.name.replace('-',
150157
'_').to_lower()))
@@ -201,13 +208,57 @@ fn vpm_install_from_vcs(modules []Module) {
201208
}
202209
}
203210

204-
fn (m Module) install(vcs &VCS) ! {
205-
cmd := '${vcs.cmd} ${vcs.args.install} "${m.url}" "${m.install_path}"'
211+
fn (m Module) install(vcs &VCS) InstallResult {
212+
if m.is_installed {
213+
// Case: installed, but not an explicit version. Update instead of continuing the installation.
214+
if m.version == '' && m.installed_version == '' {
215+
vpm_update([if m.is_external { m.url } else { m.name }])
216+
return .skipped
217+
}
218+
// Case: installed, but conflicting. Confirmation or -[-f]orce flag required.
219+
if settings.is_force || m.confirm_install() {
220+
m.remove() or {
221+
vpm_error('failed to remove `${m.name}`.', details: err.msg())
222+
return .failed
223+
}
224+
} else {
225+
return .skipped
226+
}
227+
}
228+
install_arg := if m.version != '' {
229+
'${vcs.args.install} --single-branch -b ${m.version}'
230+
} else {
231+
vcs.args.install
232+
}
233+
cmd := '${vcs.cmd} ${install_arg} "${m.url}" "${m.install_path}"'
206234
vpm_log(@FILE_LINE, @FN, 'command: ${cmd}')
207-
println('Installing module `${m.name}` from `${m.url}` to `${m.install_path}` ...')
208-
os.execute_opt(cmd) or {
209-
vpm_log(@FILE_LINE, @FN, 'cmd output: ${err}')
210-
return error('failed to install module `${m.name}` to `${m.install_path}`.')
235+
println('Installing `${m.name}`...')
236+
verbose_println(' cloning from `${m.url}` to `${m.install_path}`')
237+
res := os.execute_opt(cmd) or {
238+
vpm_error('failed to install `${m.name}`.', details: err.msg())
239+
return .failed
240+
}
241+
vpm_log(@FILE_LINE, @FN, 'cmd output: ${res.output}')
242+
return .installed
243+
}
244+
245+
fn (m Module) confirm_install() bool {
246+
if m.installed_version == m.version {
247+
println('Module `${m.name}${at_version(m.installed_version)}` is already installed, use --force to overwrite.')
248+
return false
249+
} else {
250+
install_version := at_version(if m.version == '' { 'latest' } else { m.version })
251+
println('Module `${m.name}${at_version(m.installed_version)}` is already installed at `${m.install_path}`.')
252+
input := os.input('Replace it with `${m.name}${install_version}`? [Y/n]: ')
253+
match input.to_lower() {
254+
'', 'y' {
255+
return true
256+
}
257+
else {
258+
verbose_println('Skipping `${m.name}`.')
259+
return false
260+
}
261+
}
211262
}
212263
}
213264

@@ -220,3 +271,7 @@ fn (m Module) remove() ! {
220271
}
221272
verbose_println('Removed `${m.name}`.')
222273
}
274+
275+
fn at_version(version string) string {
276+
return if version != '' { '@${version}' } else { '' }
277+
}

cmd/tools/vpm/install_test.v

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fn testsuite_end() {
2424

2525
fn test_install_from_vpm_ident() {
2626
res := os.execute_or_exit('${v} install nedpals.args')
27-
assert res.output.contains('Skipping download count increment for `nedpals.args`.')
27+
assert res.output.contains('Skipping download count increment for `nedpals.args`.'), res.output
2828
mod := vmod.from_file(os.join_path(test_path, 'nedpals', 'args', 'v.mod')) or {
2929
assert false, err.msg()
3030
return
@@ -45,7 +45,7 @@ fn test_install_from_vpm_short_ident() {
4545

4646
fn test_install_from_git_url() {
4747
res := os.execute_or_exit('${v} install https://github.com/vlang/markdown')
48-
assert res.output.contains('Installing module `markdown` from `https://github.com/vlang/markdown`')
48+
assert res.output.contains('Installing `markdown`'), res.output
4949
mod := vmod.from_file(os.join_path(test_path, 'markdown', 'v.mod')) or {
5050
assert false, err.msg()
5151
return
@@ -86,7 +86,7 @@ fn test_install_once() {
8686
install_cmd := '${@VEXE} install https://github.com/vlang/markdown https://github.com/vlang/pcre --once -v'
8787
// Try installing two modules, one of which is already installed.
8888
mut res := os.execute_or_exit(install_cmd)
89-
assert res.output.contains("Already installed modules: ['markdown']")
89+
assert res.output.contains("Already installed modules: ['markdown']"), res.output
9090
mod := vmod.from_file(os.join_path(test_path, 'pcre', 'v.mod')) or {
9191
assert false, err.msg()
9292
return
@@ -99,7 +99,7 @@ fn test_install_once() {
9999

100100
// Try installing two modules that are both already installed.
101101
res = os.execute_or_exit(install_cmd)
102-
assert res.output.contains('All modules are already installed.')
102+
assert res.output.contains('All modules are already installed.'), res.output
103103
assert md_last_modified == os.file_last_mod_unix(os.join_path(test_path, 'markdown',
104104
'v.mod'))
105105
}
@@ -108,13 +108,13 @@ fn test_missing_repo_name_in_url() {
108108
incomplete_url := 'https://github.com/vlang'
109109
res := os.execute('${v} install ${incomplete_url}')
110110
assert res.exit_code == 1
111-
assert res.output.contains('failed to retrieve module name for `${incomplete_url}`')
111+
assert res.output.contains('failed to retrieve module name for `${incomplete_url}`'), res.output
112112
}
113113

114114
fn test_missing_vmod_in_url() {
115-
assert has_vmod('https://github.com/vlang/v') // head branch == `master`.
116-
assert has_vmod('https://github.com/v-analyzer/v-analyzer') // head branch == `main`.
117-
assert !has_vmod('https://github.com/octocat/octocat.github.io') // not a V module.
115+
assert has_vmod('https://github.com/vlang/v', '') // head branch == `master`.
116+
assert has_vmod('https://github.com/v-analyzer/v-analyzer', '') // head branch == `main`.
117+
assert !has_vmod('https://github.com/octocat/octocat.github.io', '') // not a V module.
118118
res := os.execute('${v} install https://github.com/octocat/octocat.github.io')
119119
assert res.exit_code == 1
120120
assert res.output.contains('failed to find `v.mod` for `https://github.com/octocat/octocat.github.io`'), res.output

0 commit comments

Comments
 (0)