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