1 | import os |
2 | |
3 | $if windows { |
4 | $if tinyc { |
5 | #flag -ladvapi32 |
6 | #flag -luser32 |
7 | } |
8 | } |
9 | |
10 | fn main() { |
11 | C.atexit(cleanup_vtmp_folder) |
12 | |
13 | if os.args.len > 3 { |
14 | print('usage: v symlink [OPTIONS]') |
15 | exit(1) |
16 | } |
17 | vexe := os.real_path(os.getenv_opt('VEXE') or { @VEXE }) |
18 | |
19 | if '-githubci' in os.args { |
20 | setup_symlink_github() |
21 | } else { |
22 | $if windows { |
23 | setup_symlink_windows(vexe) |
24 | } $else { |
25 | setup_symlink_unix(vexe) |
26 | } |
27 | } |
28 | } |
29 | |
30 | fn cleanup_vtmp_folder() { |
31 | os.rmdir_all(os.vtmp_dir()) or {} |
32 | } |
33 | |
34 | fn setup_symlink_github() { |
35 | // We append V's install location (which should |
36 | // be the current directory) to the PATH environment variable. |
37 | |
38 | // Resources: |
39 | // 1. https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files |
40 | // 2. https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable |
41 | mut content := os.read_file(os.getenv('GITHUB_PATH')) or { |
42 | panic('Failed to read GITHUB_PATH.') |
43 | } |
44 | content += '\n${os.getwd()}\n' |
45 | os.write_file(os.getenv('GITHUB_PATH'), content) or { panic('Failed to write to GITHUB_PATH.') } |
46 | } |
47 | |
48 | fn setup_symlink_unix(vexe string) { |
49 | mut link_path := '/data/data/com.termux/files/usr/bin/v' |
50 | if !os.is_dir('/data/data/com.termux/files') { |
51 | link_dir := '/usr/local/bin' |
52 | if !os.exists(link_dir) { |
53 | os.mkdir_all(link_dir) or { panic(err) } |
54 | } |
55 | link_path = link_dir + '/v' |
56 | } |
57 | os.rm(link_path) or {} |
58 | os.symlink(vexe, link_path) or { |
59 | eprintln('Failed to create symlink "${link_path}". Try again with sudo.') |
60 | exit(1) |
61 | } |
62 | } |
63 | |
64 | fn setup_symlink_windows(vexe string) { |
65 | $if windows { |
66 | // Create a symlink in a new local folder (.\.bin\.v.exe) |
67 | // Puts `v` in %PATH% without polluting it with anything else (like make.bat). |
68 | // This will make `v` available on cmd.exe, PowerShell, and MinGW(MSYS)/WSL/Cygwin |
69 | vdir := os.real_path(os.dir(vexe)) |
70 | vsymlinkdir := os.join_path(vdir, '.bin') |
71 | mut vsymlink := os.join_path(vsymlinkdir, 'v.exe') |
72 | // Remove old symlink first (v could have been moved, symlink rerun) |
73 | if !os.exists(vsymlinkdir) { |
74 | os.mkdir(vsymlinkdir) or { panic(err) } |
75 | } else { |
76 | if os.exists(vsymlink) { |
77 | os.rm(vsymlink) or { panic(err) } |
78 | } else { |
79 | vsymlink = os.join_path(vsymlinkdir, 'v.bat') |
80 | if os.exists(vsymlink) { |
81 | os.rm(vsymlink) or { panic(err) } |
82 | } |
83 | vsymlink = os.join_path(vsymlinkdir, 'v.exe') |
84 | } |
85 | } |
86 | // First, try to create a native symlink at .\.bin\v.exe |
87 | os.symlink(vexe, vsymlink) or { |
88 | // typically only fails if you're on a network drive (VirtualBox) |
89 | // do batch file creation instead |
90 | eprintln('Could not create a native symlink: ${err}') |
91 | eprintln('Creating a batch file instead...') |
92 | vsymlink = os.join_path(vsymlinkdir, 'v.bat') |
93 | if os.exists(vsymlink) { |
94 | os.rm(vsymlink) or { panic(err) } |
95 | } |
96 | os.write_file(vsymlink, '@echo off\n"${vexe}" %*') or { panic(err) } |
97 | eprintln('${vsymlink} file written.') |
98 | } |
99 | if !os.exists(vsymlink) { |
100 | warn_and_exit('Could not create ${vsymlink}') |
101 | } |
102 | println('Symlink ${vsymlink} to ${vexe} created.') |
103 | println('Checking system %PATH%...') |
104 | reg_sys_env_handle := get_reg_sys_env_handle() or { |
105 | warn_and_exit(err.msg()) |
106 | return |
107 | } |
108 | // TODO: Fix defers inside ifs |
109 | // defer { |
110 | // C.RegCloseKey(reg_sys_env_handle) |
111 | // } |
112 | // if the above succeeded, and we cannot get the value, it may simply be empty |
113 | sys_env_path := get_reg_value(reg_sys_env_handle, 'Path') or { '' } |
114 | current_sys_paths := sys_env_path.split(os.path_delimiter).map(it.trim('/${os.path_separator}')) |
115 | mut new_paths := [vsymlinkdir] |
116 | for p in current_sys_paths { |
117 | if p == '' { |
118 | continue |
119 | } |
120 | if p !in new_paths { |
121 | new_paths << p |
122 | } |
123 | } |
124 | new_sys_env_path := new_paths.join(os.path_delimiter) |
125 | if new_sys_env_path == sys_env_path { |
126 | println('System %PATH% was already configured.') |
127 | } else { |
128 | println('System %PATH% was not configured.') |
129 | println('Adding symlink directory to system %PATH%...') |
130 | set_reg_value(reg_sys_env_handle, 'Path', new_sys_env_path) or { |
131 | C.RegCloseKey(reg_sys_env_handle) |
132 | warn_and_exit(err.msg()) |
133 | } |
134 | println('Done.') |
135 | } |
136 | println('Notifying running processes to update their Environment...') |
137 | send_setting_change_msg('Environment') or { |
138 | eprintln(err) |
139 | C.RegCloseKey(reg_sys_env_handle) |
140 | warn_and_exit('You might need to run this again to have the `v` command in your %PATH%') |
141 | } |
142 | C.RegCloseKey(reg_sys_env_handle) |
143 | println('Done.') |
144 | println('Note: Restart your shell/IDE to load the new %PATH%.') |
145 | println('After restarting your shell/IDE, give `v version` a try in another directory!') |
146 | } |
147 | } |
148 | |
149 | fn warn_and_exit(err string) { |
150 | eprintln(err) |
151 | exit(1) |
152 | } |
153 | |
154 | // get the system environment registry handle |
155 | fn get_reg_sys_env_handle() !voidptr { |
156 | $if windows { // wrap for cross-compile compat |
157 | // open the registry key |
158 | reg_key_path := 'Environment' |
159 | reg_env_key := unsafe { nil } // or HKEY (HANDLE) |
160 | if C.RegOpenKeyEx(os.hkey_current_user, reg_key_path.to_wide(), 0, 1 | 2, ®_env_key) != 0 { |
161 | return error('Could not open "${reg_key_path}" in the registry') |
162 | } |
163 | return reg_env_key |
164 | } |
165 | return error('not on windows') |
166 | } |
167 | |
168 | // get a value from a given $key |
169 | fn get_reg_value(reg_env_key voidptr, key string) !string { |
170 | $if windows { |
171 | // query the value (shortcut the sizing step) |
172 | reg_value_size := u32(4095) // this is the max length (not for the registry, but for the system %PATH%) |
173 | mut reg_value := unsafe { &u16(malloc(int(reg_value_size))) } |
174 | if C.RegQueryValueExW(reg_env_key, key.to_wide(), 0, 0, reg_value, ®_value_size) != 0 { |
175 | return error('Unable to get registry value for "${key}".') |
176 | } |
177 | return unsafe { string_from_wide(reg_value) } |
178 | } |
179 | return error('not on windows') |
180 | } |
181 | |
182 | // sets the value for the given $key to the given $value |
183 | fn set_reg_value(reg_key voidptr, key string, value string) !bool { |
184 | $if windows { |
185 | if C.RegSetValueExW(reg_key, key.to_wide(), 0, C.REG_EXPAND_SZ, value.to_wide(), |
186 | value.len * 2) != 0 { |
187 | return error('Unable to set registry value for "${key}". %PATH% may be too long.') |
188 | } |
189 | return true |
190 | } |
191 | return error('not on windows') |
192 | } |
193 | |
194 | // Broadcasts a message to all listening windows (explorer.exe in particular) |
195 | // letting them know that the system environment has changed and should be reloaded |
196 | fn send_setting_change_msg(message_data string) !bool { |
197 | $if windows { |
198 | if C.SendMessageTimeoutW(os.hwnd_broadcast, os.wm_settingchange, 0, unsafe { &u32(message_data.to_wide()) }, |
199 | os.smto_abortifhung, 5000, 0) == 0 { |
200 | return error('Could not broadcast WM_SETTINGCHANGE') |
201 | } |
202 | return true |
203 | } |
204 | return error('not on windows') |
205 | } |