v2 / cmd / tools / vpm / parse.v
295 lines · 283 sloc · 8.54 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1module main
2
3import os
4import net.http
5import v.vmod
6
7struct Module {
8mut:
9 name string
10 url string
11 version string // specifies the requested version.
12 tmp_path string
13 install_path string
14 install_path_fmted string
15 installed_version string
16 is_installed bool
17 is_external bool
18 vcs ?VCS
19 manifest vmod.Manifest
20}
21
22struct Parser {
23mut:
24 modules map[string]Module
25 checked_settings_vcs bool
26 search_modules []string
27 search_loaded bool
28 errors int
29}
30
31enum ModuleKind {
32 registered
33 https
34 http
35 ssh
36 local
37}
38
39fn parse_query(query []string, mut selector VpmInstallServerSelector) []Module {
40 mut p := Parser{}
41 for m in query {
42 p.parse_module(m, mut selector)
43 }
44 if p.errors > 0 && p.errors == query.len {
45 exit(1)
46 }
47 return p.modules.values()
48}
49
50fn (mut p Parser) lookup_registered_name_for_url(manifest_name string, ident string, mut selector VpmInstallServerSelector) ?string {
51 if manifest_name == '' || manifest_name.contains('.') {
52 return none
53 }
54 expected_url := normalize_repo_lookup_url(ident) or { return none }
55 if !p.search_loaded {
56 p.search_modules = get_all_modules_for_search_with_selector(mut selector) or {
57 vpm_log(@FILE_LINE, @FN, 'failed to load the VPM search index for `${ident}`: ${err}')
58 p.search_loaded = true
59 return none
60 }
61 p.search_loaded = true
62 }
63 target_name := normalize_mod_path(manifest_name)
64 for registered_name in p.search_modules {
65 if normalize_mod_path(registered_name.all_after_last('.')) != target_name {
66 continue
67 }
68 info := get_mod_vpm_info_with_selector(registered_name, mut selector) or {
69 vpm_log(@FILE_LINE, @FN, 'failed to retrieve metadata for `${registered_name}`: ${err}')
70 continue
71 }
72 registered_url := normalize_repo_lookup_url(info.url) or { continue }
73 if registered_url == expected_url {
74 return info.name
75 }
76 }
77 return none
78}
79
80fn (mut p Parser) parse_module(m string, mut selector VpmInstallServerSelector) {
81 kind := match true {
82 m.starts_with('https://') { ModuleKind.https }
83 m.starts_with('git@') { ModuleKind.ssh }
84 m.starts_with('http://') { ModuleKind.http }
85 is_local_repository(m) { ModuleKind.local }
86 else { ModuleKind.registered }
87 }
88
89 ident, version := if kind == .ssh {
90 if m.count('@') > 1 {
91 m.all_before_last('@'), m.all_after_last('@')
92 } else {
93 m, ''
94 }
95 } else {
96 m.rsplit_once('@') or { m, '' }
97 }
98 key := match kind {
99 .registered { m }
100 .ssh { ident.replace(':', '/') + at_version(version) }
101 else { ident.all_after('//').trim_string_right('.git') + at_version(version) }
102 }
103
104 if key in p.modules {
105 return
106 }
107 println('Scanning `${m}`...')
108 mut mod := if kind != ModuleKind.registered {
109 // External module. The identifier is an URL.
110 if kind == .http {
111 vpm_warn('installing `${ident}` via http.',
112 details: 'Support for `http` is deprecated, use `https` to ensure future compatibility.'
113 )
114 }
115 publisher, name := get_ident_from_url(if kind == .ssh {
116 'https://' + ident['git@'.len..].replace(':', '/')
117 } else {
118 ident
119 }) or {
120 vpm_error(err.msg())
121 p.errors++
122 return
123 }
124 // Verify VCS. Only needed once for external modules.
125 if !p.checked_settings_vcs {
126 p.checked_settings_vcs = true
127 settings.vcs.is_executable() or {
128 vpm_error(err.msg())
129 exit(1)
130 }
131 }
132 tmp_path := get_tmp_path(os.join_path(publisher, name, version)) or {
133 vpm_error('failed to get temporary directory for `${ident}`.', details: err.msg())
134 p.errors++
135 return
136 }
137 settings.vcs.clone(ident, version, tmp_path) or {
138 vpm_error('failed to install `${ident}`.', details: err.msg())
139 p.errors++
140 return
141 }
142 manifest := get_manifest(tmp_path) or {
143 vpm_error('failed to find `v.mod` for `${ident}${at_version(version)}`.',
144 details: err.msg()
145 )
146 rmdir_all(tmp_path) or {}
147 p.errors++
148 return
149 }
150 // Reuse the registered VPM name when a direct VCS URL points to the same repository.
151 registered_name := if kind in [.https, .ssh] {
152 p.lookup_registered_name_for_url(manifest.name, ident, mut selector) or { '' }
153 } else {
154 ''
155 }
156 final_name := if registered_name != '' { registered_name } else { manifest.name }
157 mod_path := if registered_name != '' {
158 normalize_mod_path(final_name.replace('.', os.path_separator))
159 } else {
160 normalize_mod_path(os.join_path(if kind == .http { publisher } else { '' },
161 manifest.name))
162 }
163 Module{
164 name: final_name
165 url: ident
166 version: version
167 install_path: os.real_path(os.join_path(settings.vmodules_path, mod_path))
168 is_external: true
169 tmp_path: tmp_path
170 manifest: manifest
171 }
172 } else {
173 // VPM registered module.
174 info := get_mod_vpm_info_with_selector(ident, mut selector) or {
175 vpm_error('failed to retrieve metadata for `${ident}`.', details: err.msg())
176 p.errors++
177 return
178 }
179 // Verify VCS.
180 vcs := if info.vcs != '' {
181 info_vcs := vcs_from_str(info.vcs) or {
182 vpm_error('skipping `${info.name}`, since it uses an unsupported version control system `${info.vcs}`.')
183 p.errors++
184 return
185 }
186 info_vcs
187 } else {
188 VCS.git
189 }
190 vcs.is_executable() or {
191 vpm_error(err.msg())
192 p.errors++
193 return
194 }
195 mod_path := normalize_mod_path(info.name.replace('.', os.path_separator))
196 tmp_path := get_tmp_path(os.join_path(mod_path, version)) or {
197 vpm_error('failed to get temporary directory for `${ident}`.', details: err.msg())
198 p.errors++
199 return
200 }
201 vcs.clone(info.url, version, tmp_path) or {
202 vpm_error('failed to install `${ident}`.', details: err.msg())
203 p.errors++
204 return
205 }
206 manifest := get_manifest(tmp_path) or {
207 // Add link with issue template requesting to add a manifest.
208 mut details := ''
209 new_issue_url := '${info.url}/issues/new'
210 verbose_println_more(@FILE_LINE, @FN, 'making a HEAD request to: ${new_issue_url} ...')
211 if resp := http.head(new_issue_url) {
212 if resp.status_code == 200 {
213 issue_tmpl_url := '${info.url}/issues/new?title=Missing%20Manifest&body=${info.name}%20is%20missing%20a%20manifest,%20please%20consider%20adding%20a%20v.mod%20file%20with%20the%20modules%20metadata.'
214 details = 'Please help us ensure future-compatibility, by adding a `v.mod` file or opening an issue at:\n`${issue_tmpl_url}`'
215 }
216 }
217 vpm_warn('`${info.name}` is missing a manifest file.', details: details)
218 vpm_log(@FILE_LINE, @FN, 'vpm manifest detection error: ${err}')
219 vmod.Manifest{}
220 }
221 Module{
222 name: info.name
223 url: info.url
224 version: version
225 vcs: vcs
226 install_path: os.real_path(os.join_path(settings.vmodules_path, mod_path))
227 tmp_path: tmp_path
228 manifest: manifest
229 }
230 }
231 mod.install_path_fmted = fmt_mod_path(mod.install_path)
232 mod.get_installed()
233 p.modules[key] = mod
234 if mod.manifest.dependencies.len > 0 {
235 verbose_println('Found ${mod.manifest.dependencies.len} dependencies for `${mod.name}`: ${mod.manifest.dependencies}.')
236 for d in mod.manifest.dependencies {
237 p.parse_module(d, mut selector)
238 }
239 }
240}
241
242fn is_local_repository(query string) bool {
243 if query.starts_with('file://') {
244 return true
245 }
246 mut path_candidates := [query]
247 if !query.starts_with('git@') {
248 ident, _ := query.rsplit_once('@') or { query, '' }
249 if ident != query {
250 path_candidates << ident
251 }
252 }
253 for candidate in path_candidates {
254 path := os.expand_tilde_to_home(candidate)
255 if os.is_abs_path(path) || path.starts_with('./') || path.starts_with('../')
256 || path.starts_with('~/') {
257 return true
258 }
259 if os.exists(path) {
260 return true
261 }
262 }
263 return false
264}
265
266fn (mut m Module) get_installed() {
267 refs := os.execute_opt('git ls-remote --refs ${m.install_path}') or { return }
268 vpm_log(@FILE_LINE, @FN, 'refs: ${refs}')
269 m.is_installed = true
270 // In case the head just temporarily matches a tag, make sure that there
271 // really is a version installation before adding it as `installed_version`.
272 // NOTE: can be refined for branch installations. E.g., for `sdl`.
273 if refs.output.contains('refs/tags/') {
274 tag := refs.output.all_after_last('refs/tags/').all_before('\n').trim_space()
275 head := if refs.output.contains('refs/heads/') {
276 refs.output.all_after_last('refs/heads/').all_before('\n').trim_space()
277 } else {
278 tag
279 }
280 vpm_log(@FILE_LINE, @FN, 'head: ${head}, tag: ${tag}')
281 if tag == head {
282 m.installed_version = tag
283 }
284 }
285}
286
287fn get_tmp_path(relative_path string) !string {
288 tmp_path := os.real_path(os.join_path(settings.tmp_path, relative_path))
289 if os.exists(tmp_path) {
290 // It's unlikely that the tmp_path already exists, but it might
291 // occur if vpm was canceled during an installation or update.
292 rmdir_all(tmp_path)!
293 }
294 return tmp_path
295}
296