mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
tools: add v ls
- a VLS launcher and updater utility (#15009)
This commit is contained in:
parent
58c5d387c4
commit
235ef23588
@ -10,6 +10,7 @@
|
||||
- DOOM is now translated/compiled and launched on CI servers. A screenshot of the running game
|
||||
is made via `vgret` and is compared to the expected result.
|
||||
- VLS performance improvements, especially on Windows.
|
||||
- Add `v ls` tool for installing, for updating, and for launching VLS (V Language Server)
|
||||
|
||||
## V 0.3
|
||||
*30 Jun 2022*
|
||||
|
@ -95,6 +95,7 @@ const (
|
||||
'doctor',
|
||||
'fmt',
|
||||
'gret',
|
||||
'ls',
|
||||
'repl',
|
||||
'self',
|
||||
'setup-freetype',
|
||||
|
442
cmd/tools/vls.v
Normal file
442
cmd/tools/vls.v
Normal file
@ -0,0 +1,442 @@
|
||||
// Copyright (c) 2022 Ned Palacios. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
//
|
||||
// The V language server launcher and updater utility is
|
||||
// a program responsible for installing, updating, and
|
||||
// executing the V language server program with the primary
|
||||
// goal of simplifying the installation process across
|
||||
// all different platforms, text editors, and IDEs.
|
||||
module main
|
||||
|
||||
import os
|
||||
import flag
|
||||
import x.json2
|
||||
import net.http
|
||||
import runtime
|
||||
import crypto.sha256
|
||||
import time
|
||||
|
||||
enum UpdateSource {
|
||||
github_releases
|
||||
git_repo
|
||||
}
|
||||
|
||||
enum SetupKind {
|
||||
none_
|
||||
install
|
||||
update
|
||||
}
|
||||
|
||||
enum OutputMode {
|
||||
silent
|
||||
text
|
||||
json
|
||||
}
|
||||
|
||||
struct VlsUpdater {
|
||||
mut:
|
||||
output OutputMode = .text
|
||||
setup_kind SetupKind = .none_
|
||||
update_source UpdateSource = .github_releases
|
||||
ls_path string // --path
|
||||
pass_to_ls bool // --ls
|
||||
is_check bool // --check
|
||||
is_force bool // --force
|
||||
is_help bool // --help
|
||||
args []string
|
||||
}
|
||||
|
||||
const vls_folder = os.join_path(os.home_dir(), '.vls')
|
||||
|
||||
const vls_bin_folder = os.join_path(vls_folder, 'bin')
|
||||
|
||||
const vls_cache_folder = os.join_path(vls_folder, '.cache')
|
||||
|
||||
const vls_manifest_path = os.join_path(vls_folder, 'vls.config.json')
|
||||
|
||||
const vls_src_folder = os.join_path(vls_folder, 'src')
|
||||
|
||||
const json_enc = json2.Encoder{
|
||||
newline: `\n`
|
||||
newline_spaces_count: 2
|
||||
escape_unicode: false
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) check_or_create_vls_folder() ? {
|
||||
if !os.exists(vls_folder) {
|
||||
upd.log('Creating .vls folder...')
|
||||
os.mkdir(vls_folder)?
|
||||
}
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) manifest_config() ?map[string]json2.Any {
|
||||
manifest_buf := os.read_file(vls_manifest_path) or { '{}' }
|
||||
manifest_contents := json2.raw_decode(manifest_buf)?.as_map()
|
||||
return manifest_contents
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) exec_asset_file_name() string {
|
||||
// TODO: support for Arm and other archs
|
||||
os_name := os.user_os()
|
||||
arch := if runtime.is_64bit() { 'x64' } else { 'x86' }
|
||||
ext := if os_name == 'windows' { '.exe' } else { '' }
|
||||
return 'vls_${os_name}_${arch + ext}'
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) update_manifest(new_path string, from_source bool, timestamp time.Time) ? {
|
||||
upd.log('Updating permissions...')
|
||||
os.chmod(new_path, 755)?
|
||||
|
||||
upd.log('Updating vls.config.json...')
|
||||
mut manifest := upd.manifest_config() or {
|
||||
map[string]json2.Any{}
|
||||
}
|
||||
|
||||
$if macos {
|
||||
if os.exists(vls_manifest_path) {
|
||||
os.rm(vls_manifest_path) or {}
|
||||
}
|
||||
}
|
||||
|
||||
mut manifest_file := os.open_file(vls_manifest_path, 'w+')?
|
||||
defer {
|
||||
manifest_file.close()
|
||||
}
|
||||
|
||||
manifest['server_path'] = json2.Any(new_path)
|
||||
manifest['last_updated'] = json2.Any(timestamp.format_ss())
|
||||
manifest['from_source'] = json2.Any(from_source)
|
||||
|
||||
json_enc.encode_value(manifest, mut manifest_file)?
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) init_download_prebuilt() ? {
|
||||
if !os.exists(vls_cache_folder) {
|
||||
os.mkdir(vls_cache_folder)?
|
||||
}
|
||||
|
||||
if os.exists(vls_bin_folder) {
|
||||
os.rmdir_all(vls_bin_folder)?
|
||||
}
|
||||
|
||||
os.mkdir(vls_bin_folder)?
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) get_last_updated_at() ?time.Time {
|
||||
if manifest := upd.manifest_config() {
|
||||
if 'last_updated' in manifest {
|
||||
return time.parse(manifest['last_updated'] or { '' }.str()) or { return none }
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) download_prebuilt() ? {
|
||||
last_updated_at := upd.get_last_updated_at() or { time.now() }
|
||||
defer {
|
||||
os.rmdir_all(vls_cache_folder) or {}
|
||||
}
|
||||
|
||||
upd.log('Finding prebuilt executables from GitHub release..')
|
||||
resp := http.get('https://api.github.com/repos/vlang/vls/releases')?
|
||||
releases_json := json2.raw_decode(resp.body)?.arr()
|
||||
if releases_json.len == 0 {
|
||||
return error('Unable to fetch latest VLS release data: No releases found.')
|
||||
}
|
||||
|
||||
latest_release := releases_json[0].as_map()
|
||||
assets := latest_release['assets']?.arr()
|
||||
|
||||
mut checksum_asset_idx := -1
|
||||
mut exec_asset_idx := -1
|
||||
|
||||
exp_asset_name := upd.exec_asset_file_name()
|
||||
exec_asset_file_path := os.join_path(vls_cache_folder, exp_asset_name)
|
||||
|
||||
for asset_idx, raw_asset in assets {
|
||||
asset := raw_asset.as_map()
|
||||
match asset['name']?.str() {
|
||||
exp_asset_name {
|
||||
exec_asset_idx = asset_idx
|
||||
|
||||
// check timestamp here
|
||||
}
|
||||
'checksums.txt' {
|
||||
checksum_asset_idx = asset_idx
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
|
||||
if exec_asset_idx == -1 {
|
||||
return error_with_code('No executable found for this system.', 100)
|
||||
} else if checksum_asset_idx == -1 {
|
||||
return error('Unable to download executable: missing checksum')
|
||||
}
|
||||
|
||||
exec_asset := assets[exec_asset_idx].as_map()
|
||||
|
||||
mut asset_last_updated_at := time.now()
|
||||
if created_at := exec_asset['created_at'] {
|
||||
asset_last_updated_at = time.parse_rfc3339(created_at.str()) or { asset_last_updated_at }
|
||||
}
|
||||
|
||||
if !upd.is_force && asset_last_updated_at <= last_updated_at {
|
||||
upd.log("VLS was already updated to it's latest version.")
|
||||
return
|
||||
}
|
||||
|
||||
upd.log('Executable found for this system. Downloading...')
|
||||
upd.init_download_prebuilt()?
|
||||
http.download_file(exec_asset['browser_download_url']?.str(), exec_asset_file_path)?
|
||||
|
||||
checksum_file_path := os.join_path(vls_cache_folder, 'checksums.txt')
|
||||
checksum_file_asset := assets[checksum_asset_idx].as_map()
|
||||
http.download_file(checksum_file_asset['browser_download_url']?.str(), checksum_file_path)?
|
||||
checksums := os.read_file(checksum_file_path)?.split_into_lines()
|
||||
|
||||
upd.log('Verifying checksum...')
|
||||
for checksum_result in checksums {
|
||||
if checksum_result.ends_with(exp_asset_name) {
|
||||
checksum := checksum_result.split(' ')[0]
|
||||
actual := calculate_checksum(exec_asset_file_path) or { '' }
|
||||
if checksum != actual {
|
||||
return error('Downloaded executable is corrupted. Exiting...')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
new_exec_path := os.join_path(vls_bin_folder, exp_asset_name)
|
||||
os.cp(exec_asset_file_path, new_exec_path)?
|
||||
upd.update_manifest(new_exec_path, false, asset_last_updated_at) or {
|
||||
upd.log('Unable to update config but the executable was updated successfully.')
|
||||
}
|
||||
upd.print_new_vls_version(new_exec_path)
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) print_new_vls_version(new_vls_exec_path string) {
|
||||
exec_version := os.execute('$new_vls_exec_path --version')
|
||||
if exec_version.exit_code == 0 {
|
||||
upd.log('VLS was updated to version: ${exec_version.output.all_after('vls version ').trim_space()}')
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_checksum(file_path string) ?string {
|
||||
data := os.read_file(file_path)?
|
||||
return sha256.hexhash(data)
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) compile_from_source() ? {
|
||||
git := os.find_abs_path_of_executable('git') or { return error('Git not found.') }
|
||||
|
||||
if !os.exists(vls_src_folder) {
|
||||
upd.log('Cloning VLS repo...')
|
||||
clone_result := os.execute('$git clone https://github.com/nedpals/vls $vls_src_folder')
|
||||
if clone_result.exit_code != 0 {
|
||||
return error('Failed to build VLS from source. Reason: $clone_result.output')
|
||||
}
|
||||
} else {
|
||||
upd.log('Updating VLS repo...')
|
||||
pull_result := os.execute('$git -C $vls_src_folder pull')
|
||||
if !upd.is_force && pull_result.output.trim_space() == 'Already up to date.' {
|
||||
upd.log("VLS was already updated to it's latest version.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
upd.log('Compiling VLS from source...')
|
||||
possible_compilers := ['cc', 'gcc', 'clang', 'msvc']
|
||||
mut selected_compiler_idx := -1
|
||||
|
||||
for i, cname in possible_compilers {
|
||||
os.find_abs_path_of_executable(cname) or { continue }
|
||||
selected_compiler_idx = i
|
||||
break
|
||||
}
|
||||
|
||||
if selected_compiler_idx == -1 {
|
||||
return error('Cannot compile VLS from source: no appropriate C compiler found.')
|
||||
}
|
||||
|
||||
compile_result := os.execute('v run ${os.join_path(vls_src_folder, 'build.vsh')} ${possible_compilers[selected_compiler_idx]}')
|
||||
if compile_result.exit_code != 0 {
|
||||
return error('Cannot compile VLS from source: $compile_result.output')
|
||||
}
|
||||
|
||||
exec_path := os.join_path(vls_src_folder, 'bin', 'vls')
|
||||
upd.update_manifest(exec_path, true, time.now()) or {
|
||||
upd.log('Unable to update config but the executable was updated successfully.')
|
||||
}
|
||||
upd.print_new_vls_version(exec_path)
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) find_ls_path() ?string {
|
||||
manifest := upd.manifest_config()?
|
||||
if 'server_path' in manifest {
|
||||
server_path := manifest['server_path']?
|
||||
if server_path is string {
|
||||
return server_path
|
||||
}
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
fn (mut upd VlsUpdater) parse(mut fp flag.FlagParser) ? {
|
||||
is_json := fp.bool('json', ` `, false, 'Print the output as JSON.')
|
||||
if is_json {
|
||||
upd.output = .json
|
||||
}
|
||||
|
||||
is_silent := fp.bool('silent', ` `, false, 'Disables output printing.')
|
||||
if is_silent && is_json {
|
||||
return error('Cannot use --json and --silent at the same time.')
|
||||
} else if is_silent {
|
||||
upd.output = .silent
|
||||
}
|
||||
|
||||
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.')
|
||||
is_update := fp.bool('update', ` `, false, 'Updates the installed language server.')
|
||||
upd.is_check = fp.bool('check', ` `, false, 'Checks if the language server is installed.')
|
||||
upd.is_force = fp.bool('force', ` `, false, 'Force install or update the language server.')
|
||||
is_source := fp.bool('source', ` `, false, 'Clone and build the language server from source.')
|
||||
|
||||
if is_install && is_update {
|
||||
return error('Cannot use --install and --update at the same time.')
|
||||
} else if is_install {
|
||||
upd.setup_kind = .install
|
||||
} else if is_update {
|
||||
upd.setup_kind = .update
|
||||
}
|
||||
|
||||
if is_source {
|
||||
upd.update_source = .git_repo
|
||||
}
|
||||
|
||||
upd.pass_to_ls = fp.bool('ls', ` `, false, 'Pass the arguments to the language server.')
|
||||
if ls_path := fp.string_opt('path', `p`, 'Path to the language server executable.') {
|
||||
if upd.setup_kind != .none_ {
|
||||
return error('Cannot use --install or --update when --path is supplied.')
|
||||
} else if !os.is_executable(ls_path) {
|
||||
return error('Provided executable is not valid.')
|
||||
}
|
||||
|
||||
upd.ls_path = ls_path
|
||||
}
|
||||
|
||||
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.")
|
||||
|
||||
if !upd.is_help && !upd.pass_to_ls {
|
||||
// automatically set the cli launcher to language server mode
|
||||
upd.pass_to_ls = true
|
||||
}
|
||||
|
||||
if upd.pass_to_ls {
|
||||
if upd.ls_path.len == 0 {
|
||||
if ls_path := upd.find_ls_path() {
|
||||
if !upd.is_force && upd.setup_kind == .install {
|
||||
return error_with_code('VLS was already installed.', 102)
|
||||
}
|
||||
|
||||
upd.ls_path = ls_path
|
||||
} else if upd.setup_kind == .none_ {
|
||||
return error('Language server is not installed nor found.')
|
||||
}
|
||||
}
|
||||
|
||||
if upd.is_help {
|
||||
upd.args << '--help'
|
||||
}
|
||||
|
||||
fp.allow_unknown_args()
|
||||
upd.args << fp.finalize() or { fp.remaining_parameters() }
|
||||
} else {
|
||||
fp.finalize()?
|
||||
}
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) log(msg string) {
|
||||
match upd.output {
|
||||
.text {
|
||||
println('> $msg')
|
||||
}
|
||||
.json {
|
||||
print('{"message":"$msg"}')
|
||||
flush_stdout()
|
||||
}
|
||||
.silent {}
|
||||
}
|
||||
}
|
||||
|
||||
[noreturn]
|
||||
fn (upd VlsUpdater) cli_error(err IError) {
|
||||
match upd.output {
|
||||
.text {
|
||||
eprintln('v ls error: $err.msg() ($err.code())')
|
||||
print_backtrace()
|
||||
}
|
||||
.json {
|
||||
print('{"error":{"message":"$err.msg()","code":"$err.code()"}}')
|
||||
flush_stdout()
|
||||
}
|
||||
.silent {}
|
||||
}
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) check_installation() {
|
||||
if upd.ls_path.len == 0 {
|
||||
upd.log('Language server is not installed')
|
||||
} else {
|
||||
upd.log('Language server is installed at: $upd.ls_path')
|
||||
}
|
||||
}
|
||||
|
||||
fn (upd VlsUpdater) run(fp flag.FlagParser) ? {
|
||||
if upd.is_check {
|
||||
upd.check_installation()
|
||||
} else if upd.setup_kind != .none_ {
|
||||
upd.check_or_create_vls_folder()?
|
||||
|
||||
match upd.update_source {
|
||||
.github_releases {
|
||||
upd.download_prebuilt() or {
|
||||
if err.code() == 100 {
|
||||
upd.compile_from_source()?
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
.git_repo {
|
||||
upd.compile_from_source()?
|
||||
}
|
||||
}
|
||||
} else if upd.pass_to_ls {
|
||||
exit(os.system('$upd.ls_path ${upd.args.join(' ')}'))
|
||||
} else if upd.is_help {
|
||||
println(fp.usage())
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut fp := flag.new_flag_parser(os.args)
|
||||
mut upd := VlsUpdater{}
|
||||
|
||||
fp.application('v ls')
|
||||
fp.description('Installs, updates, and executes the V language server program')
|
||||
fp.version('0.1')
|
||||
fp.skip_executable()
|
||||
|
||||
upd.parse(mut fp) or {
|
||||
if err.code() == 102 {
|
||||
upd.log(err.msg())
|
||||
exit(0)
|
||||
} else {
|
||||
upd.cli_error(err)
|
||||
}
|
||||
}
|
||||
|
||||
upd.run(fp) or { upd.cli_error(err) }
|
||||
}
|
18
cmd/v/help/ls.txt
Normal file
18
cmd/v/help/ls.txt
Normal file
@ -0,0 +1,18 @@
|
||||
Usage:
|
||||
v ls [options] [ARGS]
|
||||
|
||||
Description:
|
||||
Installs, updates, and executes the V language server program
|
||||
|
||||
Options:
|
||||
--json Print the output as JSON.
|
||||
--silent Disables output printing.
|
||||
--install Installs the language server. You may also use this flag to re-download or force update your existing installation.
|
||||
--update Updates the installed language server.
|
||||
--check Checks if the language server is installed.
|
||||
--force Force install or update the language server.
|
||||
--source Clone and build the language server from source.
|
||||
--ls Pass the arguments to the language server.
|
||||
-p, --path <string> Path to the language server executable.
|
||||
-h, --help Show this updater's help text. To show the help text for the language server's, pass the `--ls` flag before it.
|
||||
--version output version information and exit
|
Loading…
Reference in New Issue
Block a user