v2 / cmd / tools / vpm / install.v
211 lines · 197 sloc · 6.29 KB · 184e60d0db63ea9d77fac3832471b88b1c7f5be6
Raw
1module main
2
3import os
4import v.vmod
5import v.help
6
7enum InstallResult {
8 installed
9 failed
10 skipped
11}
12
13fn vpm_install(query []string) {
14 if settings.is_help {
15 help.print_and_exit('vpm')
16 }
17
18 mut selector := new_install_server_selector()
19 mut modules := parse_query(if query.len == 0 {
20 if os.exists('./v.mod') {
21 // Case: `v install` was run in a directory of another V-module to install its dependencies
22 // - without additional module arguments.
23 println('Detected v.mod file inside the project directory. Using it...')
24 manifest := vmod.from_file('./v.mod') or { panic(err) }
25 if manifest.dependencies.len == 0 {
26 println('Nothing to install.')
27 exit(0)
28 }
29 manifest.dependencies
30 } else {
31 vpm_error('specify at least one module for installation.',
32 details: 'example: `v install publisher.package` or `v install https://github.com/owner/repository`'
33 )
34 exit(2)
35 }
36 } else {
37 query
38 }, mut selector)
39
40 installed_modules := get_installed_modules()
41
42 vpm_log(@FILE_LINE, @FN, 'Queried Modules: ${modules}')
43 vpm_log(@FILE_LINE, @FN, 'Installed modules: ${installed_modules}')
44
45 if installed_modules.len > 0 && settings.is_once {
46 num_to_install := modules.len
47 mut already_installed := []string{}
48 if modules.len > 0 {
49 mut i_deleted := []int{}
50 for i, m in modules {
51 if m.name in installed_modules {
52 already_installed << m.name
53 i_deleted << i
54 }
55 }
56 for i in i_deleted.reverse() {
57 modules.delete(i)
58 }
59 }
60 if already_installed.len > 0 {
61 verbose_println('Already installed modules: ${already_installed}')
62 if already_installed.len == num_to_install {
63 println('All modules are already installed.')
64 exit(0)
65 }
66 }
67 }
68
69 install_modules(modules, selector.selected_url)
70}
71
72fn install_modules(modules []Module, selected_server_url string) {
73 vpm_log(@FILE_LINE, @FN, 'modules: ${modules}')
74 mut errors := 0
75 for m in modules {
76 vpm_log(@FILE_LINE, @FN, 'module: ${m}')
77 match m.install() {
78 .installed {}
79 .failed {
80 errors++
81 continue
82 }
83 .skipped {
84 continue
85 }
86 }
87
88 if !m.is_external {
89 increment_module_download_count(m.name, selected_server_url) or {
90 vpm_error('failed to increment the download count for `${m.name}`',
91 details: err.msg()
92 )
93 errors++
94 }
95 }
96 println('Installed `${m.name}` in ${m.install_path_fmted} .')
97 }
98 if errors > 0 {
99 exit(1)
100 }
101}
102
103fn (m Module) install() InstallResult {
104 defer {
105 os.rmdir_all(m.tmp_path) or {}
106 }
107 // Run this check unconditionally — `m.is_installed` is computed via
108 // `git ls-remote`, which itself fails when `.git` is corrupted or
109 // inaccessible, so relying on it here would skip the guard in exactly
110 // the cases we most need to fail closed.
111 reason := local_git_changes_reason(m.install_path)
112 if reason != '' {
113 vpm_error('refusing to install `${m.name}`: `${m.install_path_fmted}` has local git work that would be lost (${reason}). Commit and push your changes, or remove the directory manually before retrying.')
114 exit(1)
115 }
116 if m.is_installed {
117 // Case: installed, but not an explicit version. Update instead of continuing the installation.
118 if m.version == '' && m.installed_version == '' {
119 if m.is_external && m.url.starts_with('http://') {
120 vpm_update([m.install_path.all_after(settings.vmodules_path).trim_left(os.path_separator).replace(os.path_separator,
121 '.')])
122 } else {
123 vpm_update([m.name])
124 }
125 return .skipped
126 }
127 // Case: installed, but conflicting. Confirmation or -[-f]orce flag required.
128 if settings.is_force || m.confirm_install() {
129 m.remove() or {
130 vpm_error('failed to remove `${m.name}`.', details: err.msg())
131 return .failed
132 }
133 } else {
134 return .skipped
135 }
136 }
137 println('Installing `${m.name}`...')
138 // When the module should be relocated into a subdirectory we need to make sure
139 // it exists to not run into permission errors.
140 parent_dir := m.install_path.all_before_last(os.path_separator)
141 if !os.exists(parent_dir) {
142 os.mkdir_all(parent_dir) or {
143 vpm_error('failed to create module directory for `${m.name}`.', details: err.msg())
144 return .failed
145 }
146 }
147 os.mv(m.tmp_path, m.install_path) or {
148 vpm_error('failed to install `${m.name}`.', details: err.msg())
149 return .failed
150 }
151 return .installed
152}
153
154fn (m Module) confirm_install() bool {
155 if m.installed_version == m.version {
156 println('Module `${m.name}${at_version(m.installed_version)}` is already installed, use --force to overwrite.')
157 return false
158 } else {
159 println('Module `${m.name}${at_version(m.installed_version)}` is already installed at `${m.install_path_fmted}`.')
160 if settings.fail_on_prompt {
161 vpm_error('VPM should not have entered a confirmation prompt.')
162 exit(1)
163 }
164 install_version := at_version(if m.version == '' { 'latest' } else { m.version })
165 input := os.input('Replace it with `${m.name}${install_version}`? [Y/n]: ')
166 match input.trim_space().to_lower() {
167 '', 'y' {
168 return true
169 }
170 else {
171 verbose_println('Skipping `${m.name}`.')
172 return false
173 }
174 }
175 }
176}
177
178// local_git_changes_reason returns a non-empty reason string if `path` is a
179// git repository whose contents should not be silently overwritten — either
180// because it has uncommitted/unpushed work, or because git could not be
181// queried at all (in which case we fail closed rather than risk data loss).
182// Returns '' when the path is safe to overwrite (not a git repo, or a clean
183// repo fully in sync with its remote).
184fn local_git_changes_reason(path string) string {
185 if !os.exists(os.join_path(path, '.git')) {
186 return ''
187 }
188 quoted := os.quoted_path(path)
189 status := os.execute_opt('git -C ${quoted} status --porcelain') or {
190 return 'failed to run `git status`: ${err.msg()}'
191 }
192 if status.output.trim_space() != '' {
193 return 'uncommitted changes detected'
194 }
195 // Include `HEAD` so commits made on a detached HEAD (e.g. after
196 // `git clone -b <tag>`, the layout vpm uses for versioned installs) are
197 // also detected. `--branches` alone only walks local branch refs.
198 unpushed := os.execute_opt('git -C ${quoted} rev-list HEAD --branches --not --remotes') or {
199 return 'failed to run `git rev-list`: ${err.msg()}'
200 }
201 if unpushed.output.trim_space() != '' {
202 return 'unpushed local commits detected'
203 }
204 return ''
205}
206
207fn (m Module) remove() ! {
208 verbose_println('Removing `${m.name}` from `${m.install_path_fmted}`...')
209 rmdir_all(m.install_path)!
210 verbose_println('Removed `${m.name}`.')
211}
212