1 | // Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved. |
2 | // Use of this source code is governed by an MIT license |
3 | // that can be found in the LICENSE file. |
4 | module main |
5 | |
6 | import os |
7 | import rand |
8 | import os.cmdline |
9 | import net.http |
10 | import net.urllib |
11 | import json |
12 | import v.help |
13 | import v.vmod |
14 | import sync.pool |
15 | |
16 | const ( |
17 | default_vpm_server_urls = ['https://vpm.vlang.io', 'https://vpm.url4e.com'] |
18 | vpm_server_urls = rand.shuffle_clone(default_vpm_server_urls) or { [] } // ensure that all queries are distributed fairly |
19 | valid_vpm_commands = ['help', 'search', 'install', 'update', 'upgrade', 'outdated', |
20 | 'list', 'remove', 'show'] |
21 | excluded_dirs = ['cache', 'vlib'] |
22 | supported_vcs_systems = ['git', 'hg'] |
23 | supported_vcs_folders = ['.git', '.hg'] |
24 | supported_vcs_update_cmds = { |
25 | 'git': 'pull --recurse-submodules' // pulling with `--depth=1` leads to conflicts, when the upstream is more than 1 commit newer |
26 | 'hg': 'pull --update' |
27 | } |
28 | supported_vcs_install_cmds = { |
29 | 'git': 'clone --depth=1 --recursive --shallow-submodules' |
30 | 'hg': 'clone' |
31 | } |
32 | supported_vcs_outdated_steps = { |
33 | 'git': ['fetch', 'rev-parse @', 'rev-parse @{u}'] |
34 | 'hg': ['incoming'] |
35 | } |
36 | supported_vcs_version_cmds = { |
37 | 'git': 'version' |
38 | 'hg': 'version' |
39 | } |
40 | ) |
41 | |
42 | struct Mod { |
43 | id int |
44 | name string |
45 | url string |
46 | nr_downloads int |
47 | vcs string |
48 | } |
49 | |
50 | struct Vmod { |
51 | mut: |
52 | name string |
53 | version string |
54 | deps []string |
55 | } |
56 | |
57 | enum Source { |
58 | git |
59 | hg |
60 | vpm |
61 | } |
62 | |
63 | fn main() { |
64 | init_settings() |
65 | // This tool is intended to be launched by the v frontend, |
66 | // which provides the path to V inside os.getenv('VEXE') |
67 | // args are: vpm [options] SUBCOMMAND module names |
68 | params := cmdline.only_non_options(os.args[1..]) |
69 | options := cmdline.only_options(os.args[1..]) |
70 | verbose_println('cli params: ${params}') |
71 | if params.len < 1 { |
72 | vpm_help() |
73 | exit(5) |
74 | } |
75 | vpm_command := params[0] |
76 | mut module_names := params[1..].clone() |
77 | ensure_vmodules_dir_exist() |
78 | // println('module names: ') println(module_names) |
79 | match vpm_command { |
80 | 'help' { |
81 | vpm_help() |
82 | } |
83 | 'search' { |
84 | vpm_search(module_names) |
85 | } |
86 | 'install' { |
87 | if module_names.len == 0 && os.exists('./v.mod') { |
88 | println('Detected v.mod file inside the project directory. Using it...') |
89 | manifest := vmod.from_file('./v.mod') or { panic(err) } |
90 | module_names = manifest.dependencies.clone() |
91 | } |
92 | |
93 | if '--once' in options { |
94 | module_names = vpm_once_filter(module_names) |
95 | |
96 | if module_names.len == 0 { |
97 | return |
98 | } |
99 | } |
100 | |
101 | external_module_names := module_names.filter(it.starts_with('https://')) |
102 | vpm_module_names := module_names.filter(it !in external_module_names) |
103 | |
104 | if vpm_module_names.len > 0 { |
105 | vpm_install(vpm_module_names, Source.vpm) |
106 | } |
107 | |
108 | if external_module_names.len > 0 { |
109 | mut external_source := Source.git |
110 | |
111 | if '--hg' in options { |
112 | external_source = Source.hg |
113 | } |
114 | |
115 | vpm_install(external_module_names, external_source) |
116 | } |
117 | } |
118 | 'update' { |
119 | vpm_update(module_names) |
120 | } |
121 | 'upgrade' { |
122 | vpm_upgrade() |
123 | } |
124 | 'outdated' { |
125 | vpm_outdated() |
126 | } |
127 | 'list' { |
128 | vpm_list() |
129 | } |
130 | 'remove' { |
131 | vpm_remove(module_names) |
132 | } |
133 | 'show' { |
134 | vpm_show(module_names) |
135 | } |
136 | else { |
137 | eprintln('Error: you tried to run "v ${vpm_command}"') |
138 | eprintln('... but the v package management tool vpm only knows about these commands:') |
139 | for validcmd in valid_vpm_commands { |
140 | eprintln(' v ${validcmd}') |
141 | } |
142 | exit(3) |
143 | } |
144 | } |
145 | } |
146 | |
147 | fn vpm_search(keywords []string) { |
148 | search_keys := keywords.map(it.replace('_', '-')) |
149 | if settings.is_help { |
150 | help.print_and_exit('search') |
151 | exit(0) |
152 | } |
153 | if search_keys.len == 0 { |
154 | eprintln('´v search´ requires *at least one* keyword.') |
155 | exit(2) |
156 | } |
157 | modules := get_all_modules() |
158 | installed_modules := get_installed_modules() |
159 | joined := search_keys.join(', ') |
160 | mut index := 0 |
161 | for mod in modules { |
162 | for k in search_keys { |
163 | if !mod.contains(k) { |
164 | continue |
165 | } |
166 | if index == 0 { |
167 | println('Search results for "${joined}":\n') |
168 | } |
169 | index++ |
170 | mut parts := mod.split('.') |
171 | // in case the author isn't present |
172 | if parts.len == 1 { |
173 | parts << parts[0] |
174 | parts[0] = ' ' |
175 | } else { |
176 | parts[0] = ' by ${parts[0]} ' |
177 | } |
178 | installed := if mod in installed_modules { ' (installed)' } else { '' } |
179 | println('${index}. ${parts[1]}${parts[0]}[${mod}]${installed}') |
180 | break |
181 | } |
182 | } |
183 | if index == 0 { |
184 | vexe := os.getenv('VEXE') |
185 | vroot := os.real_path(os.dir(vexe)) |
186 | mut messages := ['No module(s) found for `${joined}` .'] |
187 | for vlibmod in search_keys { |
188 | if os.is_dir(os.join_path(vroot, 'vlib', vlibmod)) { |
189 | messages << 'There is already an existing "${vlibmod}" module in vlib, so you can just `import ${vlibmod}` .' |
190 | } |
191 | } |
192 | for m in messages { |
193 | println(m) |
194 | } |
195 | } else { |
196 | eprintln('\nUse "v install author_name.module_name" to install the module.') |
197 | } |
198 | } |
199 | |
200 | fn vpm_install_from_vpm(module_names []string) { |
201 | mut errors := 0 |
202 | for n in module_names { |
203 | name := n.trim_space().replace('_', '-') |
204 | mod := get_module_meta_info(name) or { |
205 | errors++ |
206 | eprintln('Errors while retrieving meta data for module ${name}:') |
207 | eprintln(err) |
208 | continue |
209 | } |
210 | mut vcs := mod.vcs |
211 | if vcs == '' { |
212 | vcs = supported_vcs_systems[0] |
213 | } |
214 | if vcs !in supported_vcs_systems { |
215 | errors++ |
216 | eprintln('Skipping module "${name}", since it uses an unsupported VCS {${vcs}} .') |
217 | continue |
218 | } |
219 | if !ensure_vcs_is_installed(vcs) { |
220 | errors++ |
221 | eprintln('VPM needs `${vcs}` to be installed.') |
222 | continue |
223 | } |
224 | // |
225 | minfo := mod_name_info(mod.name) |
226 | if os.exists(minfo.final_module_path) { |
227 | vpm_update([name]) |
228 | continue |
229 | } |
230 | println('Installing module "${name}" from "${mod.url}" to "${minfo.final_module_path}" ...') |
231 | increment_module_download_count(name) or { |
232 | errors++ |
233 | eprintln('Errors while incrementing the download count for ${name}:') |
234 | } |
235 | vcs_install_cmd := '${vcs} ${supported_vcs_install_cmds[vcs]}' |
236 | cmd := '${vcs_install_cmd} "${mod.url}" "${minfo.final_module_path}"' |
237 | verbose_println(' command: ${cmd}') |
238 | cmdres := os.execute(cmd) |
239 | if cmdres.exit_code != 0 { |
240 | errors++ |
241 | eprintln('Failed installing module "${name}" to "${minfo.final_module_path}" .') |
242 | print_failed_cmd(cmd, cmdres) |
243 | continue |
244 | } |
245 | resolve_dependencies(name, minfo.final_module_path, module_names) |
246 | } |
247 | if errors > 0 { |
248 | exit(1) |
249 | } |
250 | } |
251 | |
252 | fn print_failed_cmd(cmd string, cmdres os.Result) { |
253 | verbose_println('Failed command: ${cmd}') |
254 | verbose_println('Failed command output:\n${cmdres.output}') |
255 | } |
256 | |
257 | fn ensure_vcs_is_installed(vcs string) bool { |
258 | mut res := true |
259 | cmd := '${vcs} ${supported_vcs_version_cmds[vcs]}' |
260 | cmdres := os.execute(cmd) |
261 | if cmdres.exit_code != 0 { |
262 | print_failed_cmd(cmd, cmdres) |
263 | res = false |
264 | } |
265 | return res |
266 | } |
267 | |
268 | fn vpm_install_from_vcs(module_names []string, vcs_key string) { |
269 | mut errors := 0 |
270 | for n in module_names { |
271 | url := n.trim_space() |
272 | |
273 | first_cut_pos := url.last_index('/') or { |
274 | errors++ |
275 | eprintln('Errors while retrieving name for module "${url}" :') |
276 | eprintln(err) |
277 | continue |
278 | } |
279 | |
280 | mod_name := url.substr(first_cut_pos + 1, url.len) |
281 | |
282 | second_cut_pos := url.substr(0, first_cut_pos).last_index('/') or { |
283 | errors++ |
284 | eprintln('Errors while retrieving name for module "${url}" :') |
285 | eprintln(err) |
286 | continue |
287 | } |
288 | |
289 | repo_name := url.substr(second_cut_pos + 1, first_cut_pos) |
290 | mut name := os.join_path(repo_name, mod_name) |
291 | mod_name_as_path := name.replace('-', '_').to_lower() |
292 | mut final_module_path := os.real_path(os.join_path(settings.vmodules_path, mod_name_as_path)) |
293 | if os.exists(final_module_path) { |
294 | vpm_update([name.replace('-', '_')]) |
295 | continue |
296 | } |
297 | if !ensure_vcs_is_installed(vcs_key) { |
298 | errors++ |
299 | eprintln('VPM needs `${vcs_key}` to be installed.') |
300 | continue |
301 | } |
302 | println('Installing module "${name}" from "${url}" to "${final_module_path}" ...') |
303 | vcs_install_cmd := '${vcs_key} ${supported_vcs_install_cmds[vcs_key]}' |
304 | cmd := '${vcs_install_cmd} "${url}" "${final_module_path}"' |
305 | verbose_println(' command: ${cmd}') |
306 | cmdres := os.execute(cmd) |
307 | if cmdres.exit_code != 0 { |
308 | errors++ |
309 | eprintln('Failed installing module "${name}" to "${final_module_path}" .') |
310 | print_failed_cmd(cmd, cmdres) |
311 | continue |
312 | } |
313 | vmod_path := os.join_path(final_module_path, 'v.mod') |
314 | if os.exists(vmod_path) { |
315 | data := os.read_file(vmod_path) or { return } |
316 | vmod_ := parse_vmod(data) or { |
317 | eprintln(err) |
318 | return |
319 | } |
320 | minfo := mod_name_info(vmod_.name) |
321 | if final_module_path != minfo.final_module_path { |
322 | println('Relocating module from "${name}" to "${vmod_.name}" ( "${minfo.final_module_path}" ) ...') |
323 | if os.exists(minfo.final_module_path) { |
324 | eprintln('Warning module "${minfo.final_module_path}" already exsits!') |
325 | eprintln('Removing module "${minfo.final_module_path}" ...') |
326 | os.rmdir_all(minfo.final_module_path) or { |
327 | errors++ |
328 | eprintln('Errors while removing "${minfo.final_module_path}" :') |
329 | eprintln(err) |
330 | continue |
331 | } |
332 | } |
333 | os.mv(final_module_path, minfo.final_module_path) or { |
334 | errors++ |
335 | eprintln('Errors while relocating module "${name}" :') |
336 | eprintln(err) |
337 | os.rmdir_all(final_module_path) or { |
338 | errors++ |
339 | eprintln('Errors while removing "${final_module_path}" :') |
340 | eprintln(err) |
341 | continue |
342 | } |
343 | continue |
344 | } |
345 | println('Module "${name}" relocated to "${vmod_.name}" successfully.') |
346 | publisher_dir := final_module_path.all_before_last(os.path_separator) |
347 | if os.is_dir_empty(publisher_dir) { |
348 | os.rmdir(publisher_dir) or { |
349 | errors++ |
350 | eprintln('Errors while removing "${publisher_dir}" :') |
351 | eprintln(err) |
352 | } |
353 | } |
354 | final_module_path = minfo.final_module_path |
355 | } |
356 | name = vmod_.name |
357 | } |
358 | resolve_dependencies(name, final_module_path, module_names) |
359 | } |
360 | if errors > 0 { |
361 | exit(1) |
362 | } |
363 | } |
364 | |
365 | fn vpm_once_filter(module_names []string) []string { |
366 | installed_modules := get_installed_modules() |
367 | mut toinstall := []string{} |
368 | for mn in module_names { |
369 | if mn !in installed_modules { |
370 | toinstall << mn |
371 | } |
372 | } |
373 | return toinstall |
374 | } |
375 | |
376 | fn vpm_install(module_names []string, source Source) { |
377 | if settings.is_help { |
378 | help.print_and_exit('install') |
379 | exit(0) |
380 | } |
381 | if module_names.len == 0 { |
382 | eprintln('´v install´ requires *at least one* module name.') |
383 | exit(2) |
384 | } |
385 | match source { |
386 | .vpm { |
387 | vpm_install_from_vpm(module_names) |
388 | } |
389 | .git { |
390 | vpm_install_from_vcs(module_names, 'git') |
391 | } |
392 | .hg { |
393 | vpm_install_from_vcs(module_names, 'hg') |
394 | } |
395 | } |
396 | } |
397 | |
398 | pub struct ModUpdateInfo { |
399 | mut: |
400 | name string |
401 | final_path string |
402 | has_err bool |
403 | } |
404 | |
405 | fn update_module(mut pp pool.PoolProcessor, idx int, wid int) &ModUpdateInfo { |
406 | mut result := &ModUpdateInfo{ |
407 | name: pp.get_item[string](idx) |
408 | } |
409 | zname := url_to_module_name(result.name) |
410 | result.final_path = valid_final_path_of_existing_module(result.name) or { return result } |
411 | println('Updating module "${zname}" in "${result.final_path}" ...') |
412 | vcs := vcs_used_in_dir(result.final_path) or { return result } |
413 | if !ensure_vcs_is_installed(vcs[0]) { |
414 | result.has_err = true |
415 | println('VPM needs `${vcs}` to be installed.') |
416 | return result |
417 | } |
418 | path_flag := if vcs[0] == 'hg' { '-R' } else { '-C' } |
419 | vcs_cmd := '${vcs[0]} ${path_flag} "${result.final_path}" ${supported_vcs_update_cmds[vcs[0]]}' |
420 | verbose_println(' command: ${vcs_cmd}') |
421 | vcs_res := os.execute('${vcs_cmd}') |
422 | if vcs_res.exit_code != 0 { |
423 | result.has_err = true |
424 | println('Failed updating module "${zname}" in "${result.final_path}".') |
425 | print_failed_cmd(vcs_cmd, vcs_res) |
426 | return result |
427 | } else { |
428 | verbose_println(' ${vcs_res.output.trim_space()}') |
429 | increment_module_download_count(zname) or { |
430 | result.has_err = true |
431 | eprintln('Errors while incrementing the download count for ${zname}:') |
432 | } |
433 | } |
434 | return result |
435 | } |
436 | |
437 | fn vpm_update(m []string) { |
438 | mut module_names := m.clone() |
439 | if settings.is_help { |
440 | help.print_and_exit('update') |
441 | exit(0) |
442 | } |
443 | if module_names.len == 0 { |
444 | module_names = get_installed_modules() |
445 | } |
446 | if settings.is_verbose { |
447 | vpm_update_verbose(module_names) |
448 | return |
449 | } |
450 | mut pp := pool.new_pool_processor(callback: update_module) |
451 | pp.work_on_items(module_names) |
452 | for res in pp.get_results[ModUpdateInfo]() { |
453 | if res.has_err { |
454 | exit(1) |
455 | } |
456 | resolve_dependencies(res.name, res.final_path, module_names) |
457 | } |
458 | } |
459 | |
460 | fn vpm_update_verbose(module_names []string) { |
461 | mut errors := 0 |
462 | for name in module_names { |
463 | zname := url_to_module_name(name) |
464 | final_module_path := valid_final_path_of_existing_module(name) or { continue } |
465 | println('Updating module "${zname}" in "${final_module_path}" ...') |
466 | vcs := vcs_used_in_dir(final_module_path) or { continue } |
467 | if !ensure_vcs_is_installed(vcs[0]) { |
468 | errors++ |
469 | println('VPM needs `${vcs}` to be installed.') |
470 | continue |
471 | } |
472 | path_flag := if vcs[0] == 'hg' { '-R' } else { '-C' } |
473 | vcs_cmd := '${vcs[0]} ${path_flag} "${final_module_path}" ${supported_vcs_update_cmds[vcs[0]]}' |
474 | verbose_println(' command: ${vcs_cmd}') |
475 | vcs_res := os.execute('${vcs_cmd}') |
476 | if vcs_res.exit_code != 0 { |
477 | errors++ |
478 | println('Failed updating module "${zname}" in "${final_module_path}" .') |
479 | print_failed_cmd(vcs_cmd, vcs_res) |
480 | continue |
481 | } else { |
482 | verbose_println(' ${vcs_res.output.trim_space()}') |
483 | increment_module_download_count(zname) or { |
484 | errors++ |
485 | eprintln('Errors while incrementing the download count for ${zname}:') |
486 | } |
487 | } |
488 | resolve_dependencies(name, final_module_path, module_names) |
489 | } |
490 | if errors > 0 { |
491 | exit(1) |
492 | } |
493 | } |
494 | |
495 | pub struct ModDateInfo { |
496 | name string |
497 | mut: |
498 | outdated bool |
499 | exec_err bool |
500 | } |
501 | |
502 | fn get_mod_date_info(mut pp pool.PoolProcessor, idx int, wid int) &ModDateInfo { |
503 | mut result := &ModDateInfo{ |
504 | name: pp.get_item[string](idx) |
505 | } |
506 | final_module_path := valid_final_path_of_existing_module(result.name) or { return result } |
507 | vcs := vcs_used_in_dir(final_module_path) or { return result } |
508 | is_hg := vcs[0] == 'hg' |
509 | vcs_cmd_steps := supported_vcs_outdated_steps[vcs[0]] |
510 | mut outputs := []string{} |
511 | for step in vcs_cmd_steps { |
512 | path_flag := if is_hg { '-R' } else { '-C' } |
513 | cmd := '${vcs[0]} ${path_flag} "${final_module_path}" ${step}' |
514 | res := os.execute('${cmd}') |
515 | if res.exit_code < 0 { |
516 | verbose_println('Error command: ${cmd}') |
517 | verbose_println('Error details:\n${res.output}') |
518 | result.exec_err = true |
519 | return result |
520 | } |
521 | if is_hg { |
522 | if res.exit_code == 1 { |
523 | result.outdated = true |
524 | return result |
525 | } |
526 | } else { |
527 | outputs << res.output |
528 | } |
529 | } |
530 | // vcs[0] == 'git' |
531 | if !is_hg && outputs[1] != outputs[2] { |
532 | result.outdated = true |
533 | } |
534 | return result |
535 | } |
536 | |
537 | fn get_outdated() ![]string { |
538 | module_names := get_installed_modules() |
539 | mut outdated := []string{} |
540 | mut pp := pool.new_pool_processor(callback: get_mod_date_info) |
541 | pp.work_on_items(module_names) |
542 | for res in pp.get_results[ModDateInfo]() { |
543 | if res.exec_err { |
544 | return error('Error while checking latest commits for "${res.name}" .') |
545 | } |
546 | if res.outdated { |
547 | outdated << res.name |
548 | } |
549 | } |
550 | return outdated |
551 | } |
552 | |
553 | fn vpm_upgrade() { |
554 | outdated := get_outdated() or { exit(1) } |
555 | if outdated.len > 0 { |
556 | vpm_update(outdated) |
557 | } else { |
558 | println('Modules are up to date.') |
559 | } |
560 | } |
561 | |
562 | fn vpm_outdated() { |
563 | outdated := get_outdated() or { exit(1) } |
564 | if outdated.len > 0 { |
565 | eprintln('Outdated modules:') |
566 | for m in outdated { |
567 | eprintln(' ${m}') |
568 | } |
569 | } else { |
570 | println('Modules are up to date.') |
571 | } |
572 | } |
573 | |
574 | fn vpm_list() { |
575 | module_names := get_installed_modules() |
576 | if module_names.len == 0 { |
577 | eprintln('You have no modules installed.') |
578 | exit(0) |
579 | } |
580 | for mod in module_names { |
581 | println(mod) |
582 | } |
583 | } |
584 | |
585 | fn vpm_remove(module_names []string) { |
586 | if settings.is_help { |
587 | help.print_and_exit('remove') |
588 | exit(0) |
589 | } |
590 | if module_names.len == 0 { |
591 | eprintln('´v remove´ requires *at least one* module name.') |
592 | exit(2) |
593 | } |
594 | for name in module_names { |
595 | final_module_path := valid_final_path_of_existing_module(name) or { continue } |
596 | eprintln('Removing module "${name}" ...') |
597 | verbose_println('removing folder ${final_module_path}') |
598 | os.rmdir_all(final_module_path) or { |
599 | verbose_println('error while removing "${final_module_path}": ${err.msg()}') |
600 | } |
601 | // delete author directory if it is empty |
602 | author := name.split('.')[0] |
603 | author_dir := os.real_path(os.join_path(settings.vmodules_path, author)) |
604 | if !os.exists(author_dir) { |
605 | continue |
606 | } |
607 | if os.is_dir_empty(author_dir) { |
608 | verbose_println('removing author folder ${author_dir}') |
609 | os.rmdir(author_dir) or { |
610 | verbose_println('error while removing "${author_dir}": ${err.msg()}') |
611 | } |
612 | } |
613 | } |
614 | } |
615 | |
616 | fn valid_final_path_of_existing_module(modulename string) ?string { |
617 | name := if mod := get_mod_by_url(modulename) { mod.name } else { modulename } |
618 | minfo := mod_name_info(name) |
619 | if !os.exists(minfo.final_module_path) { |
620 | eprintln('No module with name "${minfo.mname_normalised}" exists at ${minfo.final_module_path}') |
621 | return none |
622 | } |
623 | if !os.is_dir(minfo.final_module_path) { |
624 | eprintln('Skipping "${minfo.final_module_path}", since it is not a folder.') |
625 | return none |
626 | } |
627 | vcs_used_in_dir(minfo.final_module_path) or { |
628 | eprintln('Skipping "${minfo.final_module_path}", since it does not use a supported vcs.') |
629 | return none |
630 | } |
631 | return minfo.final_module_path |
632 | } |
633 | |
634 | fn ensure_vmodules_dir_exist() { |
635 | if !os.is_dir(settings.vmodules_path) { |
636 | println('Creating "${settings.vmodules_path}/" ...') |
637 | os.mkdir(settings.vmodules_path) or { panic(err) } |
638 | } |
639 | } |
640 | |
641 | fn vpm_help() { |
642 | help.print_and_exit('vpm') |
643 | } |
644 | |
645 | fn vcs_used_in_dir(dir string) ?[]string { |
646 | mut vcs := []string{} |
647 | for repo_subfolder in supported_vcs_folders { |
648 | checked_folder := os.real_path(os.join_path(dir, repo_subfolder)) |
649 | if os.is_dir(checked_folder) { |
650 | vcs << repo_subfolder.replace('.', '') |
651 | } |
652 | } |
653 | if vcs.len == 0 { |
654 | return none |
655 | } |
656 | return vcs |
657 | } |
658 | |
659 | fn get_installed_modules() []string { |
660 | dirs := os.ls(settings.vmodules_path) or { return [] } |
661 | mut modules := []string{} |
662 | for dir in dirs { |
663 | adir := os.join_path(settings.vmodules_path, dir) |
664 | if dir in excluded_dirs || !os.is_dir(adir) { |
665 | continue |
666 | } |
667 | if os.exists(os.join_path(adir, 'v.mod')) && os.exists(os.join_path(adir, '.git', 'config')) { |
668 | // an official vlang module with a short module name, like `vsl`, `ui` or `markdown` |
669 | modules << dir |
670 | continue |
671 | } |
672 | author := dir |
673 | mods := os.ls(adir) or { continue } |
674 | for m in mods { |
675 | vcs_used_in_dir(os.join_path(adir, m)) or { continue } |
676 | modules << '${author}.${m}' |
677 | } |
678 | } |
679 | return modules |
680 | } |
681 | |
682 | struct ModNameInfo { |
683 | mut: |
684 | mname string // The-user.The-mod , *never* The-user.The-mod.git |
685 | mname_normalised string // the_user.the_mod |
686 | mname_as_path string // the_user/the_mod |
687 | final_module_path string // ~/.vmodules/the_user/the_mod |
688 | } |
689 | |
690 | fn mod_name_info(mod_name string) ModNameInfo { |
691 | mut info := ModNameInfo{} |
692 | info.mname = if mod_name.ends_with('.git') { mod_name.replace('.git', '') } else { mod_name } |
693 | info.mname_normalised = info.mname.replace('-', '_').to_lower() |
694 | info.mname_as_path = info.mname_normalised.replace('.', os.path_separator) |
695 | info.final_module_path = os.real_path(os.join_path(settings.vmodules_path, info.mname_as_path)) |
696 | return info |
697 | } |
698 | |
699 | fn url_to_module_name(modulename string) string { |
700 | mut res := if mod := get_mod_by_url(modulename) { mod.name } else { modulename } |
701 | if res.ends_with('.git') { |
702 | res = res.replace('.git', '') |
703 | } |
704 | return res |
705 | } |
706 | |
707 | fn get_all_modules() []string { |
708 | url := get_working_server_url() |
709 | r := http.get(url) or { panic(err) } |
710 | if r.status_code != 200 { |
711 | eprintln('Failed to search vpm.vlang.io. Status code: ${r.status_code}') |
712 | exit(1) |
713 | } |
714 | s := r.body |
715 | mut read_len := 0 |
716 | mut modules := []string{} |
717 | for read_len < s.len { |
718 | mut start_token := "<a href='/mod" |
719 | end_token := '</a>' |
720 | // get the start index of the module entry |
721 | mut start_index := s.index_after(start_token, read_len) |
722 | if start_index == -1 { |
723 | start_token = '<a href="/mod' |
724 | start_index = s.index_after(start_token, read_len) |
725 | if start_index == -1 { |
726 | break |
727 | } |
728 | } |
729 | // get the index of the end of anchor (a) opening tag |
730 | // we use the previous start_index to make sure we are getting a module and not just a random 'a' tag |
731 | start_token = '>' |
732 | start_index = s.index_after(start_token, start_index) + start_token.len |
733 | |
734 | // get the index of the end of module entry |
735 | end_index := s.index_after(end_token, start_index) |
736 | if end_index == -1 { |
737 | break |
738 | } |
739 | modules << s[start_index..end_index] |
740 | read_len = end_index |
741 | if read_len >= s.len { |
742 | break |
743 | } |
744 | } |
745 | return modules |
746 | } |
747 | |
748 | fn resolve_dependencies(name string, module_path string, module_names []string) { |
749 | vmod_path := os.join_path(module_path, 'v.mod') |
750 | if !os.exists(vmod_path) { |
751 | return |
752 | } |
753 | data := os.read_file(vmod_path) or { return } |
754 | vmod_ := parse_vmod(data) or { |
755 | eprintln(err) |
756 | return |
757 | } |
758 | mut deps := []string{} |
759 | // filter out dependencies that were already specified by the user |
760 | for d in vmod_.deps { |
761 | if d !in module_names { |
762 | deps << d |
763 | } |
764 | } |
765 | if deps.len > 0 { |
766 | println('Resolving ${deps.len} dependencies for module "${name}" ...') |
767 | verbose_println('Found dependencies: ${deps}') |
768 | vpm_install(deps, Source.vpm) |
769 | } |
770 | } |
771 | |
772 | fn parse_vmod(data string) !Vmod { |
773 | manifest := vmod.decode(data) or { return error('Parsing v.mod file failed, ${err}') } |
774 | mut vmod_ := Vmod{} |
775 | vmod_.name = manifest.name |
776 | vmod_.version = manifest.version |
777 | vmod_.deps = manifest.dependencies |
778 | return vmod_ |
779 | } |
780 | |
781 | fn get_working_server_url() string { |
782 | server_urls := if settings.server_urls.len > 0 { |
783 | settings.server_urls |
784 | } else { |
785 | vpm_server_urls |
786 | } |
787 | for url in server_urls { |
788 | verbose_println('Trying server url: ${url}') |
789 | http.head(url) or { |
790 | verbose_println(' ${url} failed.') |
791 | continue |
792 | } |
793 | return url |
794 | } |
795 | panic('No responding vpm server found. Please check your network connectivity and try again later.') |
796 | } |
797 | |
798 | // settings context: |
799 | struct VpmSettings { |
800 | mut: |
801 | is_help bool |
802 | is_verbose bool |
803 | server_urls []string |
804 | vmodules_path string |
805 | } |
806 | |
807 | const ( |
808 | settings = &VpmSettings{} |
809 | ) |
810 | |
811 | fn init_settings() { |
812 | mut s := &VpmSettings(unsafe { nil }) |
813 | unsafe { |
814 | s = settings |
815 | } |
816 | s.is_help = '-h' in os.args || '--help' in os.args || 'help' in os.args |
817 | s.is_verbose = '-v' in os.args |
818 | s.server_urls = cmdline.options(os.args, '-server-url') |
819 | s.vmodules_path = os.vmodules_dir() |
820 | } |
821 | |
822 | fn verbose_println(s string) { |
823 | if settings.is_verbose { |
824 | println(s) |
825 | } |
826 | } |
827 | |
828 | fn get_mod_by_url(name string) !Mod { |
829 | if purl := urllib.parse(name) { |
830 | verbose_println('purl: ${purl}') |
831 | mod := Mod{ |
832 | name: purl.path.trim_left('/').trim_right('/').replace('/', '.') |
833 | url: name |
834 | } |
835 | verbose_println(mod.str()) |
836 | return mod |
837 | } |
838 | return error('invalid url: ${name}') |
839 | } |
840 | |
841 | fn get_module_meta_info(name string) !Mod { |
842 | if mod := get_mod_by_url(name) { |
843 | return mod |
844 | } |
845 | mut errors := []string{} |
846 | |
847 | for server_url in vpm_server_urls { |
848 | modurl := server_url + '/api/packages/${name}' |
849 | verbose_println('Retrieving module metadata from: "${modurl}" ...') |
850 | r := http.get(modurl) or { |
851 | errors << 'Http server did not respond to our request for "${modurl}" .' |
852 | errors << 'Error details: ${err}' |
853 | continue |
854 | } |
855 | if r.status_code == 404 || r.body.trim_space() == '404' { |
856 | errors << 'Skipping module "${name}", since "${server_url}" reported that "${name}" does not exist.' |
857 | continue |
858 | } |
859 | if r.status_code != 200 { |
860 | errors << 'Skipping module "${name}", since "${server_url}" responded with ${r.status_code} http status code. Please try again later.' |
861 | continue |
862 | } |
863 | s := r.body |
864 | if s.len > 0 && s[0] != `{` { |
865 | errors << 'Invalid json data' |
866 | errors << s.trim_space().limit(100) + ' ...' |
867 | continue |
868 | } |
869 | mod := json.decode(Mod, s) or { |
870 | errors << 'Skipping module "${name}", since its information is not in json format.' |
871 | continue |
872 | } |
873 | if '' == mod.url || '' == mod.name { |
874 | errors << 'Skipping module "${name}", since it is missing name or url information.' |
875 | continue |
876 | } |
877 | return mod |
878 | } |
879 | return error(errors.join_lines()) |
880 | } |
881 | |
882 | fn increment_module_download_count(name string) ! { |
883 | mut errors := []string{} |
884 | |
885 | for server_url in vpm_server_urls { |
886 | modurl := server_url + '/api/packages/${name}/incr_downloads' |
887 | r := http.post(modurl, '') or { |
888 | errors << 'Http server did not respond to our request for "${modurl}" .' |
889 | errors << 'Error details: ${err}' |
890 | continue |
891 | } |
892 | if r.status_code != 200 { |
893 | errors << 'Failed to increment the download count for module "${name}", since "${server_url}" responded with ${r.status_code} http status code. Please try again later.' |
894 | continue |
895 | } |
896 | return |
897 | } |
898 | return error(errors.join_lines()) |
899 | } |
900 | |
901 | fn vpm_show(module_names []string) { |
902 | installed_modules := get_installed_modules() |
903 | for module_name in module_names { |
904 | if module_name !in installed_modules { |
905 | module_meta_info := get_module_meta_info(module_name) or { continue } |
906 | print(' |
907 | Name: ${module_meta_info.name} |
908 | Homepage: ${module_meta_info.url} |
909 | Downloads: ${module_meta_info.nr_downloads} |
910 | Installed: False |
911 | -------- |
912 | ') |
913 | continue |
914 | } |
915 | path := os.join_path(os.vmodules_dir(), module_name.replace('.', os.path_separator)) |
916 | mod := vmod.from_file(os.join_path(path, 'v.mod')) or { continue } |
917 | print('Name: ${mod.name} |
918 | Version: ${mod.version} |
919 | Description: ${mod.description} |
920 | Homepage: ${mod.repo_url} |
921 | Author: ${mod.author} |
922 | License: ${mod.license} |
923 | Location: ${path} |
924 | Requires: ${mod.dependencies.join(', ')} |
925 | -------- |
926 | ') |
927 | } |
928 | } |