1use std::path::Path;
23
24use once_cell::sync::Lazy;
25use regex::Regex;
26
27static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
28 Regex::new(r"^#!\s*(?:/usr/bin/env\s+(?:-S\s+)?(?P<vars>(?:[^ \t=]+=[^ \t=]+\s+)*))?(?P<prog>[^ \t]+)(?P<args>.*)$")
29 .unwrap()
30});
31
32static DOLLAR_EXPR_REGEX: Lazy<Regex> =
33 Lazy::new(|| Regex::new(r"\$\{?(?P<var>[^$@#?\- \t{}:]+)\}?").unwrap());
34
35pub fn shim_bin(source: &Path, to: &Path) -> std::io::Result<()> {
36 let from = pathdiff::diff_paths(source, to.parent().expect("must have parent"))
39 .expect("paths should be diffable");
40 cleanup_existing(to)?;
41 if let Ok(contents) = std::fs::read_to_string(source) {
42 let mut lines = contents.lines();
43 if let Some(first_line) = lines.next() {
44 if let Some(captures) = SHEBANG_REGEX.captures(first_line.trim_end()) {
45 let vars = captures.name("vars").map(|m| m.as_str());
46 let prog = captures.name("prog").map(|m| m.as_str());
47 let args = captures.name("args").map(|m| m.as_str());
48 return write_shim(&from, to, vars, prog, args);
49 }
50 }
51 }
52 write_shim(&from, to, None, None, None)
53}
54
55fn cleanup_existing(to: &Path) -> std::io::Result<()> {
56 if let Ok(meta) = to.metadata() {
57 if meta.is_dir() {
58 std::fs::remove_dir_all(to)?;
59 } else {
60 std::fs::remove_file(to)?;
61 }
62 }
63 let cmd = to.with_extension("cmd");
64 if let Ok(meta) = cmd.metadata() {
65 if meta.is_dir() {
66 std::fs::remove_dir_all(cmd)?;
67 } else {
68 std::fs::remove_file(cmd)?;
69 }
70 }
71 let ps1 = to.with_extension("ps1");
72 if let Ok(meta) = ps1.metadata() {
73 if meta.is_dir() {
74 std::fs::remove_dir_all(ps1)?;
75 } else {
76 std::fs::remove_file(ps1)?;
77 }
78 }
79 Ok(())
80}
81
82fn write_shim(
83 from: &Path,
84 to: &Path,
85 vars: Option<&str>,
86 prog: Option<&str>,
87 args: Option<&str>,
88) -> std::io::Result<()> {
89 write_cmd_shim(from, to, vars, prog, args)?;
90 write_sh_shim(from, to, vars, prog, args)?;
91 write_pwsh_shim(from, to, vars, prog, args)?;
92 Ok(())
93}
94
95fn write_cmd_shim(
96 from: &Path,
97 to: &Path,
98 vars: Option<&str>,
99 prog: Option<&str>,
100 args: Option<&str>,
101) -> std::io::Result<()> {
102 let mut cmd = concat!(
103 "@ECHO off\r\n",
104 "GOTO start\r\n",
105 ":find_dp0\r\n",
106 "SET dp0=%~dp0\r\n",
107 "EXIT /b\r\n",
108 ":start\r\n",
109 "SETLOCAL\r\n",
110 "CALL :find_dp0\r\n"
111 )
112 .to_string();
113
114 let target = format!(
115 "\"%dp0%\\{target}\"",
116 target = from.display().to_string().replace('/', "\\")
117 );
118 if let Some(prog) = prog {
119 let args = if let Some(args) = args {
120 args.trim()
121 } else {
122 ""
123 };
124 cmd.push_str(&convert_to_set_commands(vars.unwrap_or("")));
125 cmd.push_str("\r\n");
126 cmd.push_str(&format!("IF EXIST \"%dp0%\\{prog}.exe\" (\r\n"));
127 cmd.push_str(&format!(" SET \"_prog=%dp0%\\{prog}.exe\"\r\n"));
128 cmd.push_str(") ELSE (\r\n");
129 cmd.push_str(&format!(
130 " SET \"_prog={}\"\r\n",
131 prog.trim_start_matches('"').trim_end_matches('"')
132 ));
133 cmd.push_str(" SET PATHEXT=%PATHEXT:;.JS;=;%\r\n");
134 cmd.push_str(")\r\n");
135 cmd.push_str("\r\n");
136 cmd.push_str("endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & ");
137 cmd.push_str(&format!("\"%_prog%\" {args} {target} %*\r\n",));
138 } else {
139 cmd.push_str(&format!("{target} %*\r\n",));
140 }
141
142 std::fs::write(to.with_extension("cmd"), cmd)?;
143
144 Ok(())
145}
146
147fn write_sh_shim(
148 from: &Path,
149 to: &Path,
150 vars: Option<&str>,
151 prog: Option<&str>,
152 args: Option<&str>,
153) -> std::io::Result<()> {
154 let mut sh = concat!(
155 "#!/bin/sh\n",
156 r#"basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')")"#,
157 "\n\n",
158 "case `uname` in\n",
159 " *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w \"$basedir\"`;;\n",
160 "esac\n\n"
161 )
162 .to_string();
163
164 let args = args.unwrap_or("");
165 let vars = vars.unwrap_or("");
166 let target = from.display().to_string().replace('\\', "/");
167 if let Some(prog) = prog {
168 let long_prog = format!("\"$basedir/{prog}\"");
169 let prog = prog.replace('\\', "/");
170 sh.push_str(&format!("if [ -x {long_prog} ]; then\n"));
171 sh.push_str(&format!(
172 " exec {vars}{long_prog} {args} \"$basedir/{target}\" \"$@\"\n"
173 ));
174 sh.push_str("else \n");
175 sh.push_str(&format!(
176 " exec {vars}{prog} {args} \"$basedir/{target}\" \"$@\"\n"
177 ));
178 sh.push_str("fi\n");
179 } else {
180 sh.push_str(&format!("exec \"$basedir/{target}\" {args} \"$@\"\n"));
181 }
182
183 std::fs::write(to, sh)?;
184
185 Ok(())
186}
187
188fn write_pwsh_shim(
189 from: &Path,
190 to: &Path,
191 vars: Option<&str>,
192 prog: Option<&str>,
193 args: Option<&str>,
194) -> std::io::Result<()> {
195 let mut pwsh = concat!(
196 "#!/usr/bin/env pwsh\n",
197 "$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\n",
198 "\n",
199 "$exe=\"\"\n",
200 "if ($PSVersionTable.PSVersion -lt \"6.0\" -or $IsWindows) {\n",
201 " # Fix case when both the Windows and Linux builds of Node\n",
202 " # are installed in the same directory\n",
203 " $exe=\".exe\"\n",
204 "}\n"
205 )
206 .to_string();
207
208 let args = args.unwrap_or("");
209 let target = from.display().to_string().replace('\\', "/");
210 if let Some(prog) = prog {
211 let long_prog = format!("\"$basedir/{prog}$exe\"");
212 let prog = format!("\"{}\"$exe", prog.replace('\\', "/"));
213 pwsh.push_str(&convert_to_env_commands(vars.unwrap_or("")));
214 pwsh.push_str("$ret=0\n");
215 pwsh.push_str(&format!("if (Test-Path {long_prog}) {{\n"));
216 pwsh.push_str(" # Support pipeline input\n");
217 pwsh.push_str(" if ($MyInvocation.ExpectingInput) {\n");
218 pwsh.push_str(&format!(
219 " $input | & {long_prog} {args} \"$basedir/{target}\" $args\n"
220 ));
221 pwsh.push_str(" } else {\n");
222 pwsh.push_str(&format!(
223 " & {long_prog} {args} \"$basedir/{target}\" $args\n"
224 ));
225 pwsh.push_str(" }\n");
226 pwsh.push_str(" $ret=$LASTEXITCODE\n");
227 pwsh.push_str("} else {\n");
228 pwsh.push_str(" # Support pipeline input\n");
229 pwsh.push_str(" if ($MyInvocation.ExpectingInput) {\n");
230 pwsh.push_str(&format!(
231 " $input | & {prog} {args} \"$basedir/{target}\" $args\n"
232 ));
233 pwsh.push_str(" } else {\n");
234 pwsh.push_str(&format!(
235 " & {prog} {args} \"$basedir/{target}\" $args\n"
236 ));
237 pwsh.push_str(" }\n");
238 pwsh.push_str(" $ret=$LASTEXITCODE\n");
239 pwsh.push_str("}\n");
240 pwsh.push_str("exit $ret\n");
241 } else {
242 pwsh.push_str("# Support pipeline input\n");
243 pwsh.push_str("if ($MyInvocation.ExpectingInput) {\n");
244 pwsh.push_str(&format!(" $input | & \"$basedir/{target}\" $args\n"));
245 pwsh.push_str("} else {\n");
246 pwsh.push_str(&format!(" & \"$basedir/{target}\" $args\n"));
247 pwsh.push_str("}\n");
248 pwsh.push_str("exit $LASTEXITCODE\n");
249 }
250
251 std::fs::write(to.with_extension("ps1"), pwsh)?;
252
253 Ok(())
254}
255
256fn convert_to_set_commands(variables: &str) -> String {
257 let mut var_declarations_as_batch = String::new();
258 for var_str in variables.split_whitespace() {
259 let mut parts = var_str.splitn(2, '=');
260 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
261 var_declarations_as_batch.push_str(&convert_to_set_command(key, value));
262 }
263 }
264 var_declarations_as_batch
265}
266
267fn convert_to_set_command(key: &str, value: &str) -> String {
268 let key = key.trim();
269 let value = value.trim();
270 if key.is_empty() || value.is_empty() {
271 String::new()
272 } else {
273 format!("@SET {key}={}\r\n", replace_dollar_with_percent_pair(value))
274 }
275}
276
277fn replace_dollar_with_percent_pair(value: &str) -> String {
278 let mut result = String::new();
279 let mut start_idx = 0;
280 for capture in DOLLAR_EXPR_REGEX.captures_iter(value) {
281 let mat = capture
282 .get(0)
283 .expect("If we had a capture, there should be a 0-match");
284 result.push_str(&value[start_idx..mat.start()]);
285 result.push('%');
286 result.push_str(&capture["var"]);
287 result.push('%');
288 start_idx = mat.end();
289 }
290 result.push_str(&value[start_idx..]);
291 result
292}
293
294fn convert_to_env_commands(variables: &str) -> String {
295 let mut var_declarations_as_batch = String::new();
296 for var_str in variables.split_whitespace() {
297 let mut parts = var_str.splitn(2, '=');
298 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
299 var_declarations_as_batch.push_str(&convert_to_env_command(key, value));
300 }
301 }
302 var_declarations_as_batch
303}
304
305fn convert_to_env_command(key: &str, value: &str) -> String {
306 let key = key.trim();
307 let value = value.trim();
308 if key.is_empty() || value.is_empty() {
309 String::new()
310 } else {
311 format!(
312 "$env:{key}=\"{}\"\n",
313 replace_with_string_interpolation(value)
314 )
315 }
316}
317
318fn replace_with_string_interpolation(value: &str) -> String {
319 let mut result = String::new();
320 let mut start_idx = 0;
321 for capture in DOLLAR_EXPR_REGEX.captures_iter(value) {
322 let mat = capture
323 .get(0)
324 .expect("If we had a capture, there should be a 0-match");
325 result.push_str(&value[start_idx..mat.start()]);
326 result.push_str("${env:");
329 result.push_str(&capture["var"]);
330 result.push('}');
331 start_idx = mat.end();
332 }
333 result.push_str(&value[start_idx..]);
334 result.replace('\"', "`\"")
335}