From 33740ad9d7d5f5dae90a44e0fdcc5b175fafd327 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 11 Mar 2026 16:19:16 +0300 Subject: [PATCH] cmd/tools: allow specifying the location of the symlink (fixes #25244) --- README.md | 13 +++++- cmd/tools/vsymlink/vsymlink.v | 61 ++++++++++++++++++++----- cmd/tools/vsymlink/vsymlink_nix.c.v | 24 ++++++---- cmd/tools/vsymlink/vsymlink_test.v | 49 ++++++++++++++++++++ cmd/tools/vsymlink/vsymlink_windows.c.v | 22 ++++++--- 5 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 cmd/tools/vsymlink/vsymlink_test.v diff --git a/README.md b/README.md index 2a401c034..51ebd3e21 100644 --- a/README.md +++ b/README.md @@ -220,13 +220,19 @@ Otherwise, follow these instructions: > you the effort to type in the full path to your v executable every time. > V provides a convenience `v symlink` command to do that more easily. -On Unix systems, it creates a `/usr/local/bin/v` symlink to your -executable. To do that, run: +On Unix systems, it creates a `v` symlink in `/usr/local/bin` by +default. To do that, run: ```bash sudo ./v symlink ``` +You can also pass a different directory, for example: + +```bash +./v symlink ~/.local/bin +``` + On Windows, start a new shell with administrative privileges, for example by pressing the Windows Key, then type `cmd.exe`, right-click on its menu entry, and choose `Run as administrator`. In the new administrative shell, cd to the path where you have compiled V, then @@ -238,6 +244,9 @@ v symlink (or `.\v symlink` in PowerShell) +You can pass a different directory there too, for example +`v symlink C:\Users\you\bin`. + That will make V available everywhere, by adding it to your PATH. Please restart your shell/editor after that, so that it can pick up the new PATH variable. diff --git a/cmd/tools/vsymlink/vsymlink.v b/cmd/tools/vsymlink/vsymlink.v index 1a27450ae..3496e6a4d 100644 --- a/cmd/tools/vsymlink/vsymlink.v +++ b/cmd/tools/vsymlink/vsymlink.v @@ -1,23 +1,60 @@ import os const vexe = os.real_path(os.getenv_opt('VEXE') or { @VEXE }) +const symlink_usage = 'usage: v symlink [directory]' + +struct SymlinkOptions { + link_dir string + github_ci bool +} fn main() { at_exit(|| os.rmdir_all(os.vtmp_dir()) or {}) or {} - if os.args.len > 2 { - if '-githubci' in os.args { - // TODO: [AFTER 2024-09-31] remove `-githubci` flag and function and only print usage and exit(1) . - if os.getenv('GITHUB_JOB') != '' { - println('::warning::Use `v symlink` instead of `v symlink -githubci`') - } - setup_symlink_github() - return - } else { - println('usage: v symlink') - exit(1) + options := parse_symlink_options(os.args[1..]) or { + println(symlink_usage) + exit(1) + } + if options.github_ci { + setup_symlink_github() + return + } + setup_symlink(options.link_dir) +} + +fn parse_symlink_options(raw_args []string) !SymlinkOptions { + args := trim_symlink_command(raw_args) + if args.len == 0 { + return SymlinkOptions{} + } + if args == ['-githubci'] { + // TODO: [AFTER 2024-09-31] remove `-githubci` flag and function and only print usage and exit(1) . + if os.getenv('GITHUB_JOB') != '' { + println('::warning::Use `v symlink` instead of `v symlink -githubci`') + } + return SymlinkOptions{ + github_ci: true } } - setup_symlink() + if args.len == 1 { + return SymlinkOptions{ + link_dir: args[0] + } + } + return error(symlink_usage) +} + +fn trim_symlink_command(raw_args []string) []string { + if raw_args.len > 0 && raw_args[0] == 'symlink' { + return raw_args[1..] + } + return raw_args +} + +fn normalized_link_dir(custom_link_dir string) string { + if custom_link_dir != '' { + return os.expand_tilde_to_home(custom_link_dir) + } + return default_link_dir() } fn setup_symlink_github() { diff --git a/cmd/tools/vsymlink/vsymlink_nix.c.v b/cmd/tools/vsymlink/vsymlink_nix.c.v index 5d954bbb6..541ccfd49 100644 --- a/cmd/tools/vsymlink/vsymlink_nix.c.v +++ b/cmd/tools/vsymlink/vsymlink_nix.c.v @@ -1,14 +1,11 @@ import os -fn setup_symlink() { - mut link_path := '/data/data/com.termux/files/usr/bin/v' - if !os.is_dir('/data/data/com.termux/files') { - link_dir := '/usr/local/bin' - if !os.exists(link_dir) { - os.mkdir_all(link_dir) or { panic(err) } - } - link_path = link_dir + '/v' +fn setup_symlink(custom_link_dir string) { + link_dir := normalized_link_dir(custom_link_dir) + if !os.exists(link_dir) { + os.mkdir_all(link_dir) or { panic(err) } } + link_path := symlink_path(link_dir) os.rm(link_path) or {} os.symlink(vexe, link_path) or { // Try ~/.local/bin as a fallback when /usr/local/bin is not writable. @@ -41,3 +38,14 @@ fn setup_symlink() { } } } + +fn default_link_dir() string { + if os.is_dir('/data/data/com.termux/files') { + return '/data/data/com.termux/files/usr/bin' + } + return '/usr/local/bin' +} + +fn symlink_path(link_dir string) string { + return os.join_path(link_dir, 'v') +} diff --git a/cmd/tools/vsymlink/vsymlink_test.v b/cmd/tools/vsymlink/vsymlink_test.v new file mode 100644 index 000000000..6de77823d --- /dev/null +++ b/cmd/tools/vsymlink/vsymlink_test.v @@ -0,0 +1,49 @@ +module main + +import os + +fn test_trim_symlink_command_accepts_forwarded_subcommand() { + assert trim_symlink_command([]string{}) == []string{} + assert trim_symlink_command(['symlink']) == []string{} + assert trim_symlink_command(['symlink', '~/.local/bin']) == ['~/.local/bin'] + assert trim_symlink_command(['~/.local/bin']) == ['~/.local/bin'] +} + +fn test_parse_symlink_options_defaults_to_default_location() { + options := parse_symlink_options([]string{}) or { panic(err) } + assert options == SymlinkOptions{} +} + +fn test_parse_symlink_options_accepts_custom_directory() { + options := parse_symlink_options(['symlink', '~/.local/bin']) or { panic(err) } + assert options.github_ci == false + assert options.link_dir == '~/.local/bin' +} + +fn test_parse_symlink_options_supports_githubci() { + options := parse_symlink_options(['symlink', '-githubci']) or { panic(err) } + assert options.github_ci + assert options.link_dir == '' +} + +fn test_parse_symlink_options_rejects_multiple_arguments() { + if options := parse_symlink_options(['symlink', '/tmp/bin', 'extra']) { + assert false, 'expected an error, got ${options}' + } else { + assert err.msg() == symlink_usage + } +} + +fn test_normalized_link_dir_expands_tilde() { + path := os.join_path('~', '.local', 'bin') + expected := os.join_path(os.home_dir(), '.local', 'bin') + assert normalized_link_dir(path) == expected +} + +fn test_symlink_path_uses_platform_binary_name() { + $if windows { + assert symlink_path(r'C:\tools\vbin') == os.join_path(r'C:\tools\vbin', 'v.exe') + } $else { + assert symlink_path('/tmp/vbin') == os.join_path('/tmp/vbin', 'v') + } +} diff --git a/cmd/tools/vsymlink/vsymlink_windows.c.v b/cmd/tools/vsymlink/vsymlink_windows.c.v index ffd62f347..51f7f00f7 100644 --- a/cmd/tools/vsymlink/vsymlink_windows.c.v +++ b/cmd/tools/vsymlink/vsymlink_windows.c.v @@ -5,16 +5,15 @@ $if tinyc { #flag -luser32 } -fn setup_symlink() { - // Create a symlink in a new local folder (.\.bin\.v.exe) +fn setup_symlink(custom_link_dir string) { + // Create a symlink in a local folder (by default .\.bin\v.exe). // Puts `v` in %PATH% without polluting it with anything else (like make.bat). // This will make `v` available on cmd.exe, PowerShell, and MinGW(MSYS)/WSL/Cygwin - vdir := os.real_path(os.dir(vexe)) - vsymlinkdir := os.join_path(vdir, '.bin') - mut vsymlink := os.join_path(vsymlinkdir, 'v.exe') + vsymlinkdir := normalized_link_dir(custom_link_dir) + mut vsymlink := symlink_path(vsymlinkdir) // Remove old symlink first (v could have been moved, symlink rerun) if !os.exists(vsymlinkdir) { - os.mkdir(vsymlinkdir) or { panic(err) } + os.mkdir_all(vsymlinkdir) or { panic(err) } } else { if os.exists(vsymlink) { os.rm(vsymlink) or { panic(err) } @@ -26,7 +25,7 @@ fn setup_symlink() { vsymlink = os.join_path(vsymlinkdir, 'v.exe') } } - // First, try to create a native symlink at .\.bin\v.exe + // First, try to create a native symlink in the configured directory. os.symlink(vexe, vsymlink) or { // typically only fails if you're on a network drive (VirtualBox) // do batch file creation instead @@ -102,6 +101,15 @@ fn setup_symlink() { println('After restarting your shell/IDE, give `v version` a try in another directory!') } +fn default_link_dir() string { + vdir := os.real_path(os.dir(vexe)) + return os.join_path(vdir, '.bin') +} + +fn symlink_path(link_dir string) string { + return os.join_path(link_dir, 'v.exe') +} + fn warn_and_exit(err string) { eprintln(err) exit(1) -- 2.39.5