1 | // Copyright (c) 2022 Ned Palacios. 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 | // |
5 | // The V language server launcher and updater utility is |
6 | // a program responsible for installing, updating, and |
7 | // executing the V language server program with the primary |
8 | // goal of simplifying the installation process across |
9 | // all different platforms, text editors, and IDEs. |
10 | module main |
11 | |
12 | import os |
13 | import flag |
14 | import x.json2 |
15 | import net.http |
16 | import runtime |
17 | import crypto.sha256 |
18 | import time |
19 | import json |
20 | |
21 | enum UpdateSource { |
22 | github_releases |
23 | git_repo |
24 | local_file |
25 | } |
26 | |
27 | enum SetupKind { |
28 | none_ |
29 | install |
30 | update |
31 | } |
32 | |
33 | enum OutputMode { |
34 | silent |
35 | text |
36 | json |
37 | } |
38 | |
39 | struct VlsUpdater { |
40 | mut: |
41 | output OutputMode = .text |
42 | setup_kind SetupKind = .none_ |
43 | update_source UpdateSource = .github_releases |
44 | ls_path string // --path |
45 | pass_to_ls bool // --ls |
46 | is_check bool // --check |
47 | is_force bool // --force |
48 | is_help bool // --help |
49 | args []string |
50 | } |
51 | |
52 | const vls_folder = os.join_path(os.home_dir(), '.vls') |
53 | |
54 | const vls_bin_folder = os.join_path(vls_folder, 'bin') |
55 | |
56 | const vls_cache_folder = os.join_path(vls_folder, '.cache') |
57 | |
58 | const vls_manifest_path = os.join_path(vls_folder, 'vls.config.json') |
59 | |
60 | const vls_src_folder = os.join_path(vls_folder, 'src') |
61 | |
62 | const server_not_found_err = error_with_code('Language server is not installed nor found.', |
63 | 101) |
64 | |
65 | const json_enc = json2.Encoder{ |
66 | newline: `\n` |
67 | newline_spaces_count: 2 |
68 | escape_unicode: false |
69 | } |
70 | |
71 | fn (upd VlsUpdater) check_or_create_vls_folder() ! { |
72 | if !os.exists(vls_folder) { |
73 | upd.log('Creating .vls folder...') |
74 | os.mkdir(vls_folder)! |
75 | } |
76 | } |
77 | |
78 | fn (upd VlsUpdater) manifest_config() !map[string]json2.Any { |
79 | manifest_buf := os.read_file(vls_manifest_path) or { '{}' } |
80 | manifest_contents := json2.raw_decode(manifest_buf)!.as_map() |
81 | return manifest_contents |
82 | } |
83 | |
84 | fn (upd VlsUpdater) exec_asset_file_name() string { |
85 | // TODO: support for Arm and other archs |
86 | os_name := os.user_os() |
87 | arch := if runtime.is_64bit() { 'x64' } else { 'x86' } |
88 | ext := if os_name == 'windows' { '.exe' } else { '' } |
89 | return 'vls_${os_name}_${arch + ext}' |
90 | } |
91 | |
92 | fn (upd VlsUpdater) update_manifest(new_path string, from_source bool, timestamp time.Time) ! { |
93 | upd.log('Updating permissions...') |
94 | os.chmod(new_path, 0o755)! |
95 | |
96 | upd.log('Updating vls.config.json...') |
97 | mut manifest := upd.manifest_config() or { |
98 | map[string]json2.Any{} |
99 | } |
100 | |
101 | $if macos { |
102 | if os.exists(vls_manifest_path) { |
103 | os.rm(vls_manifest_path) or {} |
104 | } |
105 | } |
106 | |
107 | mut manifest_file := os.open_file(vls_manifest_path, 'w+')! |
108 | defer { |
109 | manifest_file.close() |
110 | } |
111 | |
112 | manifest['server_path'] = json2.Any(new_path) |
113 | manifest['last_updated'] = json2.Any(timestamp.format_ss()) |
114 | manifest['from_source'] = json2.Any(from_source) |
115 | |
116 | json_enc.encode_value(manifest, mut manifest_file)! |
117 | } |
118 | |
119 | fn (upd VlsUpdater) init_download_prebuilt() ! { |
120 | if !os.exists(vls_cache_folder) { |
121 | os.mkdir(vls_cache_folder)! |
122 | } |
123 | |
124 | if os.exists(vls_bin_folder) { |
125 | os.rmdir_all(vls_bin_folder)! |
126 | } |
127 | |
128 | os.mkdir(vls_bin_folder)! |
129 | } |
130 | |
131 | fn (upd VlsUpdater) get_last_updated_at() !time.Time { |
132 | if manifest := upd.manifest_config() { |
133 | if 'last_updated' in manifest { |
134 | return time.parse(manifest['last_updated'] or { '' }.str()) or { return error('none') } |
135 | } |
136 | } |
137 | return error('none') |
138 | } |
139 | |
140 | fn (upd VlsUpdater) copy_local_file(exec_asset_file_path string, timestamp time.Time) ! { |
141 | exp_asset_name := upd.exec_asset_file_name() |
142 | |
143 | new_exec_path := os.join_path(vls_bin_folder, exp_asset_name) |
144 | os.cp(exec_asset_file_path, new_exec_path)! |
145 | upd.update_manifest(new_exec_path, false, timestamp) or { |
146 | upd.log('Unable to update config but the executable was updated successfully.') |
147 | } |
148 | upd.print_new_vls_version(new_exec_path) |
149 | } |
150 | |
151 | fn (upd VlsUpdater) download_prebuilt() ! { |
152 | mut has_last_updated_at := true |
153 | last_updated_at := upd.get_last_updated_at() or { |
154 | has_last_updated_at = false |
155 | time.now() |
156 | } |
157 | defer { |
158 | os.rmdir_all(vls_cache_folder) or {} |
159 | } |
160 | |
161 | upd.log('Finding prebuilt executables from GitHub release..') |
162 | resp := http.get('https://api.github.com/repos/vlang/vls/releases')! |
163 | releases_json := json2.raw_decode(resp.body)!.arr() |
164 | if releases_json.len == 0 { |
165 | return error('Unable to fetch latest VLS release data: No releases found.') |
166 | } |
167 | |
168 | latest_release := releases_json[0].as_map() |
169 | assets := latest_release['assets']!.arr() |
170 | |
171 | mut checksum_asset_idx := -1 |
172 | mut exec_asset_idx := -1 |
173 | |
174 | exp_asset_name := upd.exec_asset_file_name() |
175 | exec_asset_file_path := os.join_path(vls_cache_folder, exp_asset_name) |
176 | |
177 | for asset_idx, raw_asset in assets { |
178 | asset := raw_asset.as_map() |
179 | t_asset := asset['name'] or { return } |
180 | match t_asset.str() { |
181 | exp_asset_name { |
182 | exec_asset_idx = asset_idx |
183 | |
184 | // check timestamp here |
185 | } |
186 | 'checksums.txt' { |
187 | checksum_asset_idx = asset_idx |
188 | } |
189 | else {} |
190 | } |
191 | } |
192 | |
193 | if exec_asset_idx == -1 { |
194 | return error_with_code('No executable found for this system.', 100) |
195 | } else if checksum_asset_idx == -1 { |
196 | return error('Unable to download executable: missing checksum') |
197 | } |
198 | |
199 | exec_asset := assets[exec_asset_idx].as_map() |
200 | |
201 | mut asset_last_updated_at := time.now() |
202 | if created_at := exec_asset['created_at'] { |
203 | asset_last_updated_at = time.parse_rfc3339(created_at.str()) or { asset_last_updated_at } |
204 | } |
205 | |
206 | if has_last_updated_at && !upd.is_force && asset_last_updated_at <= last_updated_at { |
207 | upd.log("VLS was already updated to it's latest version.") |
208 | return |
209 | } |
210 | |
211 | upd.log('Executable found for this system. Downloading...') |
212 | upd.init_download_prebuilt()! |
213 | http.download_file(exec_asset['browser_download_url']!.str(), exec_asset_file_path)! |
214 | |
215 | checksum_file_path := os.join_path(vls_cache_folder, 'checksums.txt') |
216 | checksum_file_asset := assets[checksum_asset_idx].as_map() |
217 | http.download_file(checksum_file_asset['browser_download_url']!.str(), checksum_file_path)! |
218 | checksums := os.read_file(checksum_file_path)!.split_into_lines() |
219 | |
220 | upd.log('Verifying checksum...') |
221 | for checksum_result in checksums { |
222 | if checksum_result.ends_with(exp_asset_name) { |
223 | checksum := checksum_result.split(' ')[0] |
224 | actual := calculate_checksum(exec_asset_file_path) or { '' } |
225 | if checksum != actual { |
226 | return error('Downloaded executable is corrupted. Exiting...') |
227 | } |
228 | break |
229 | } |
230 | } |
231 | |
232 | upd.copy_local_file(exec_asset_file_path, asset_last_updated_at)! |
233 | } |
234 | |
235 | fn (upd VlsUpdater) print_new_vls_version(new_vls_exec_path string) { |
236 | exec_version := os.execute('${new_vls_exec_path} --version') |
237 | if exec_version.exit_code == 0 { |
238 | upd.log('VLS was updated to version: ${exec_version.output.all_after('vls version ').trim_space()}') |
239 | } |
240 | } |
241 | |
242 | fn calculate_checksum(file_path string) !string { |
243 | data := os.read_file(file_path)! |
244 | return sha256.hexhash(data) |
245 | } |
246 | |
247 | fn (upd VlsUpdater) compile_from_source() ! { |
248 | git := os.find_abs_path_of_executable('git') or { return error('Git not found.') } |
249 | |
250 | if !os.exists(vls_src_folder) { |
251 | upd.log('Cloning VLS repo...') |
252 | clone_result := os.execute('${git} clone --filter=blob:none https://github.com/vlang/vls ${vls_src_folder}') |
253 | if clone_result.exit_code != 0 { |
254 | return error('Failed to build VLS from source. Reason: ${clone_result.output}') |
255 | } |
256 | } else { |
257 | upd.log('Updating VLS repo...') |
258 | pull_result := os.execute('${git} -C ${vls_src_folder} pull') |
259 | if !upd.is_force && pull_result.output.trim_space() == 'Already up to date.' { |
260 | upd.log("VLS was already updated to it's latest version.") |
261 | return |
262 | } |
263 | } |
264 | |
265 | upd.log('Compiling VLS from source...') |
266 | possible_compilers := ['cc', 'gcc', 'clang', 'msvc'] |
267 | mut selected_compiler_idx := -1 |
268 | |
269 | for i, cname in possible_compilers { |
270 | os.find_abs_path_of_executable(cname) or { continue } |
271 | selected_compiler_idx = i |
272 | break |
273 | } |
274 | |
275 | if selected_compiler_idx == -1 { |
276 | return error('Cannot compile VLS from source: no appropriate C compiler found.') |
277 | } |
278 | |
279 | compile_result := os.execute('v run ${os.join_path(vls_src_folder, 'build.vsh')} ${possible_compilers[selected_compiler_idx]}') |
280 | if compile_result.exit_code != 0 { |
281 | return error('Cannot compile VLS from source: ${compile_result.output}') |
282 | } |
283 | |
284 | exec_path := os.join_path(vls_src_folder, 'bin', 'vls') |
285 | upd.update_manifest(exec_path, true, time.now()) or { |
286 | upd.log('Unable to update config but the executable was updated successfully.') |
287 | } |
288 | upd.print_new_vls_version(exec_path) |
289 | } |
290 | |
291 | fn (upd VlsUpdater) find_ls_path() !string { |
292 | manifest := upd.manifest_config()! |
293 | if 'server_path' in manifest { |
294 | server_path := manifest['server_path'] or { return error('none') } |
295 | if server_path is string { |
296 | if server_path.len == 0 { |
297 | return error('none') |
298 | } |
299 | |
300 | return server_path |
301 | } |
302 | } |
303 | return error('none') |
304 | } |
305 | |
306 | fn (mut upd VlsUpdater) parse(mut fp flag.FlagParser) ! { |
307 | is_json := fp.bool('json', ` `, false, 'Print the output as JSON.') |
308 | if is_json { |
309 | upd.output = .json |
310 | } |
311 | |
312 | is_silent := fp.bool('silent', ` `, false, 'Disables output printing.') |
313 | if is_silent && is_json { |
314 | return error('Cannot use --json and --silent at the same time.') |
315 | } else if is_silent { |
316 | upd.output = .silent |
317 | } |
318 | |
319 | is_install := fp.bool('install', ` `, false, 'Installs the language server. You may also use this flag to re-download or force update your existing installation.') |
320 | is_update := fp.bool('update', ` `, false, 'Updates the installed language server.') |
321 | upd.is_check = fp.bool('check', ` `, false, 'Checks if the language server is installed.') |
322 | upd.is_force = fp.bool('force', ` `, false, 'Force install or update the language server.') |
323 | is_source := fp.bool('source', ` `, false, 'Clone and build the language server from source.') |
324 | |
325 | if is_install && is_update { |
326 | return error('Cannot use --install and --update at the same time.') |
327 | } else if is_install { |
328 | upd.setup_kind = .install |
329 | } else if is_update { |
330 | upd.setup_kind = .update |
331 | } |
332 | |
333 | if is_source { |
334 | upd.update_source = .git_repo |
335 | } |
336 | |
337 | upd.pass_to_ls = fp.bool('ls', ` `, false, 'Pass the arguments to the language server.') |
338 | if ls_path := fp.string_opt('path', `p`, 'Path to the language server executable.') { |
339 | if !os.is_executable(ls_path) { |
340 | return server_not_found_err |
341 | } |
342 | |
343 | upd.ls_path = ls_path |
344 | |
345 | if upd.setup_kind != .none_ { |
346 | upd.update_source = .local_file // use local path if both -p and --source are used |
347 | } |
348 | } |
349 | |
350 | upd.is_help = fp.bool('help', `h`, false, "Show this updater's help text. To show the help text for the language server, pass the `--ls` flag before it.") |
351 | |
352 | if !upd.is_help && !upd.pass_to_ls { |
353 | // automatically set the cli launcher to language server mode |
354 | upd.pass_to_ls = true |
355 | } |
356 | |
357 | if upd.pass_to_ls { |
358 | if upd.ls_path.len == 0 { |
359 | if ls_path := upd.find_ls_path() { |
360 | if !upd.is_force && upd.setup_kind == .install { |
361 | return error_with_code('VLS was already installed.', 102) |
362 | } |
363 | |
364 | upd.ls_path = ls_path |
365 | } else if upd.setup_kind == .none_ { |
366 | return server_not_found_err |
367 | } |
368 | } |
369 | |
370 | if upd.is_help { |
371 | upd.args << '--help' |
372 | } |
373 | |
374 | fp.allow_unknown_args() |
375 | upd.args << fp.finalize() or { fp.remaining_parameters() } |
376 | } else { |
377 | fp.finalize()! |
378 | } |
379 | } |
380 | |
381 | fn (upd VlsUpdater) log(msg string) { |
382 | match upd.output { |
383 | .text { |
384 | println('> ${msg}') |
385 | } |
386 | .json { |
387 | print('{"message":"${msg}"}') |
388 | flush_stdout() |
389 | } |
390 | .silent {} |
391 | } |
392 | } |
393 | |
394 | fn (upd VlsUpdater) error_details(err IError) string { |
395 | match err.code() { |
396 | 101 { |
397 | mut vls_dir_shortened := '\$HOME/.vls' |
398 | $if windows { |
399 | vls_dir_shortened = '%USERPROFILE%\\.vls' |
400 | } |
401 | |
402 | return ' |
403 | - If you are using this for the first time, please run |
404 | `v ls --install` first to download and install VLS. |
405 | - If you are using a custom version of VLS, check if |
406 | the specified path exists and is a valid executable. |
407 | - If you have an existing installation of VLS, be sure |
408 | to remove "vls.config.json" and "bin" located inside |
409 | "${vls_dir_shortened}" and re-install. |
410 | |
411 | If none of the options listed have solved your issue, |
412 | please report it at https://github.com/vlang/v/issues |
413 | ' |
414 | } |
415 | else { |
416 | return '' |
417 | } |
418 | } |
419 | } |
420 | |
421 | [noreturn] |
422 | fn (upd VlsUpdater) cli_error(err IError) { |
423 | match upd.output { |
424 | .text { |
425 | eprintln('v ls error: ${err.msg()} (${err.code()})') |
426 | if err !is none { |
427 | eprintln(upd.error_details(err)) |
428 | } |
429 | |
430 | print_backtrace() |
431 | } |
432 | .json { |
433 | print('{"error":{"message":${json.encode(err.msg())},"code":"${err.code()}","details":${json.encode(upd.error_details(err).trim_space())}}}') |
434 | flush_stdout() |
435 | } |
436 | .silent {} |
437 | } |
438 | exit(1) |
439 | } |
440 | |
441 | fn (upd VlsUpdater) check_installation() { |
442 | if upd.ls_path.len == 0 { |
443 | upd.log('Language server is not installed') |
444 | } else { |
445 | upd.log('Language server is installed at: ${upd.ls_path}'.split(r'\').join(r'\\')) |
446 | } |
447 | } |
448 | |
449 | fn (upd VlsUpdater) run(fp flag.FlagParser) ! { |
450 | if upd.is_check { |
451 | upd.check_installation() |
452 | } else if upd.setup_kind != .none_ { |
453 | upd.check_or_create_vls_folder()! |
454 | |
455 | match upd.update_source { |
456 | .github_releases { |
457 | upd.download_prebuilt() or { |
458 | if err.code() == 100 { |
459 | upd.compile_from_source()! |
460 | } |
461 | return err |
462 | } |
463 | } |
464 | .git_repo { |
465 | upd.compile_from_source()! |
466 | } |
467 | .local_file { |
468 | upd.log('Using local vls file to install or update..') |
469 | upd.copy_local_file(upd.ls_path, time.now())! |
470 | } |
471 | } |
472 | } else if upd.pass_to_ls { |
473 | exit(os.system('${upd.ls_path} ${upd.args.join(' ')}')) |
474 | } else if upd.is_help { |
475 | println(fp.usage()) |
476 | exit(0) |
477 | } |
478 | } |
479 | |
480 | fn main() { |
481 | mut fp := flag.new_flag_parser(os.args) |
482 | mut upd := VlsUpdater{} |
483 | |
484 | fp.application('v ls') |
485 | fp.description('Installs, updates, and executes the V language server program') |
486 | fp.version('0.1.1') |
487 | |
488 | // just to make sure whenever user wants to |
489 | // interact directly with the executable |
490 | // instead of the usual `v ls` command |
491 | if fp.args.len >= 2 && fp.args[0..2] == [os.executable(), 'ls'] { |
492 | // skip the executable here, the next skip_executable |
493 | // outside the if statement will skip the `ls` part |
494 | fp.skip_executable() |
495 | } |
496 | |
497 | // skip the executable or the `ls` part |
498 | fp.skip_executable() |
499 | |
500 | upd.parse(mut fp) or { |
501 | if err.code() == 102 { |
502 | upd.log(err.msg()) |
503 | exit(0) |
504 | } else { |
505 | upd.cli_error(err) |
506 | } |
507 | } |
508 | |
509 | upd.run(fp) or { upd.cli_error(err) } |
510 | } |