| 1 | module main |
| 2 | |
| 3 | import os |
| 4 | import net.http |
| 5 | import v.vmod |
| 6 | |
| 7 | struct Module { |
| 8 | mut: |
| 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 | |
| 22 | struct Parser { |
| 23 | mut: |
| 24 | modules map[string]Module |
| 25 | checked_settings_vcs bool |
| 26 | search_modules []string |
| 27 | search_loaded bool |
| 28 | errors int |
| 29 | } |
| 30 | |
| 31 | enum ModuleKind { |
| 32 | registered |
| 33 | https |
| 34 | http |
| 35 | ssh |
| 36 | local |
| 37 | } |
| 38 | |
| 39 | fn 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 | |
| 50 | fn (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 | |
| 80 | fn (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 | |
| 242 | fn 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 | |
| 266 | fn (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 | |
| 287 | fn 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 | |