v / cmd / tools
Raw file | 510 loc (428 sloc) | 13.25 KB | Latest commit hash 017ace6ea
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 vls_folder = os.join_path(os.home_dir(), '.vls')
53
54const vls_bin_folder = os.join_path(vls_folder, 'bin')
55
56const vls_cache_folder = os.join_path(vls_folder, '.cache')
57
58const vls_manifest_path = os.join_path(vls_folder, 'vls.config.json')
59
60const vls_src_folder = os.join_path(vls_folder, 'src')
61
62const server_not_found_err = error_with_code('Language server is not installed nor found.',
63 101)
64
65const json_enc = json2.Encoder{
66 newline: `\n`
67 newline_spaces_count: 2
68 escape_unicode: false
69}
70
71fn (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
78fn (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
84fn (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
92fn (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
119fn (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
131fn (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
140fn (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
151fn (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
235fn (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
242fn calculate_checksum(file_path string) !string {
243 data := os.read_file(file_path)!
244 return sha256.hexhash(data)
245}
246
247fn (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
291fn (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
306fn (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
381fn (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
394fn (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]
422fn (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
441fn (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
449fn (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
480fn 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}