Set argv[0] to .roc file passed to 'roc run'

When we run `roc run <file>` or `roc <file>` then Roc will compile a
binary and run it. Before this commit we would set the path to the
compiled binary as argv[0]. This commit changes the behavior to make
argv[0] in the binary correspond to the roc file being ran.

This benefits the use of roc scripts that make use of a shebang:

    #!/usr/bin/env roc

With this change such scripts will be able to read the path to
themselves out of ARGV. This trick is commonly used for instance by bash
scripts in order to access files relative to the script itself.
This commit is contained in:
Jasper Woudenberg 2024-10-18 19:37:00 +02:00
parent 573d4337e7
commit ad555297cf
No known key found for this signature in database
3 changed files with 69 additions and 15 deletions

View file

@ -986,7 +986,15 @@ pub fn build(
// ManuallyDrop will leak the bytes because we don't drop manually // ManuallyDrop will leak the bytes because we don't drop manually
let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap()); let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap());
roc_run(&arena, opt_level, target, args, bytes, expect_metadata) roc_run(
&arena,
path,
opt_level,
target,
args,
bytes,
expect_metadata,
)
} }
BuildAndRunIfNoErrors => { BuildAndRunIfNoErrors => {
if problems.fatally_errored { if problems.fatally_errored {
@ -1021,7 +1029,15 @@ pub fn build(
// ManuallyDrop will leak the bytes because we don't drop manually // ManuallyDrop will leak the bytes because we don't drop manually
let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap()); let bytes = &ManuallyDrop::new(std::fs::read(&binary_path).unwrap());
roc_run(&arena, opt_level, target, args, bytes, expect_metadata) roc_run(
&arena,
path,
opt_level,
target,
args,
bytes,
expect_metadata,
)
} }
} }
} }
@ -1034,6 +1050,7 @@ pub fn build(
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>( fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: &Bump, arena: &Bump,
script_path: &Path,
opt_level: OptLevel, opt_level: OptLevel,
target: Target, target: Target,
args: I, args: I,
@ -1073,7 +1090,14 @@ fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
Ok(0) Ok(0)
} }
_ => roc_run_native(arena, opt_level, args, binary_bytes, expect_metadata), _ => roc_run_native(
arena,
script_path,
opt_level,
args,
binary_bytes,
expect_metadata,
),
} }
} }
@ -1090,7 +1114,7 @@ fn os_str_as_utf8_bytes(os_str: &OsStr) -> &[u8] {
fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>( fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &'a Bump, arena: &'a Bump,
executable: &ExecutableFile, script_path: &Path,
args: I, args: I,
) -> ( ) -> (
bumpalo::collections::Vec<'a, CString>, bumpalo::collections::Vec<'a, CString>,
@ -1098,8 +1122,7 @@ fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
) { ) {
use bumpalo::collections::CollectIn; use bumpalo::collections::CollectIn;
let path = executable.as_path(); let path_cstring = CString::new(os_str_as_utf8_bytes(script_path.as_os_str())).unwrap();
let path_cstring = CString::new(os_str_as_utf8_bytes(path.as_os_str())).unwrap();
// argv is an array of pointers to strings passed to the new program // argv is an array of pointers to strings passed to the new program
// as its command-line arguments. By convention, the first of these // as its command-line arguments. By convention, the first of these
@ -1137,6 +1160,7 @@ fn make_argv_envp<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>( fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &Bump, arena: &Bump,
script_path: &Path,
opt_level: OptLevel, opt_level: OptLevel,
args: I, args: I,
binary_bytes: &[u8], binary_bytes: &[u8],
@ -1145,7 +1169,7 @@ fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
use bumpalo::collections::CollectIn; use bumpalo::collections::CollectIn;
let executable = roc_run_executable_file_path(binary_bytes)?; let executable = roc_run_executable_file_path(binary_bytes)?;
let (argv_cstrings, envp_cstrings) = make_argv_envp(arena, &executable, args); let (argv_cstrings, envp_cstrings) = make_argv_envp(arena, script_path, args);
let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings
.iter() .iter()
@ -1400,6 +1424,7 @@ fn roc_run_executable_file_path(binary_bytes: &[u8]) -> std::io::Result<Executab
#[cfg(not(target_family = "unix"))] #[cfg(not(target_family = "unix"))]
fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>( fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
arena: &Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it! arena: &Bump, // This should be passed an owned value, not a reference, so we can usefully mem::forget it!
script_path: &Path,
opt_level: OptLevel, opt_level: OptLevel,
args: I, args: I,
binary_bytes: &[u8], binary_bytes: &[u8],
@ -1411,7 +1436,7 @@ fn roc_run_native<I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
let executable = roc_run_executable_file_path(binary_bytes)?; let executable = roc_run_executable_file_path(binary_bytes)?;
// TODO forward the arguments // TODO forward the arguments
let (argv_cstrings, envp_cstrings) = make_argv_envp(&arena, &executable, args); let (argv_cstrings, envp_cstrings) = make_argv_envp(&arena, script_path, args);
let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings let argv: bumpalo::collections::Vec<*const c_char> = argv_cstrings
.iter() .iter()

View file

@ -0,0 +1,12 @@
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
}
import pf.Stdout
import pf.Arg
main =
args = Arg.list! {}
when List.first args is
Ok argv0 -> Stdout.line argv0
Err ListWasEmpty -> Stdout.line "Failed: argv was empty"

View file

@ -305,7 +305,7 @@ mod cli_run {
} }
}; };
let self_path = file.display().to_string(); let self_path = file.parent().unwrap().display().to_string();
let actual_cmd_stdout = ignore_test_timings(&strip_colors(&cmd_output.stdout)) let actual_cmd_stdout = ignore_test_timings(&strip_colors(&cmd_output.stdout))
.replace(&self_path, "<ignored for tests>"); .replace(&self_path, "<ignored for tests>");
@ -573,12 +573,12 @@ mod cli_run {
words : List Str words : List Str
words = ["this", "will", "for", "sure", "be", "a", "large", "string", "so", "when", "we", "split", "it", "it", "will", "use", "seamless", "slices", "which", "affect", "printing"] words = ["this", "will", "for", "sure", "be", "a", "large", "string", "so", "when", "we", "split", "it", "it", "will", "use", "seamless", "slices", "which", "affect", "printing"]
[<ignored for tests>:31] x = 42 [<ignored for tests>/expects.roc:31] x = 42
[<ignored for tests>:33] "Fjoer en ferdjer frieten oan dyn geve lea" = "Fjoer en ferdjer frieten oan dyn geve lea" [<ignored for tests>/expects.roc:33] "Fjoer en ferdjer frieten oan dyn geve lea" = "Fjoer en ferdjer frieten oan dyn geve lea"
[<ignored for tests>:35] "this is line 24" = "this is line 24" [<ignored for tests>/expects.roc:35] "this is line 24" = "this is line 24"
[<ignored for tests>:21] x = "abc" [<ignored for tests>/expects.roc:21] x = "abc"
[<ignored for tests>:21] x = 10 [<ignored for tests>/expects.roc:21] x = 10
[<ignored for tests>:21] x = (A (B C)) [<ignored for tests>/expects.roc:21] x = (A (B C))
Program finished! Program finished!
"# "#
), ),
@ -1203,6 +1203,23 @@ mod cli_run {
) )
} }
#[test]
#[serial(cli_platform)]
#[cfg_attr(windows, ignore)]
#[ignore = "broken because of a bug in basic-cli: https://github.com/roc-lang/basic-cli/issues/82"]
fn argv0() {
test_roc_app(
"crates/cli/tests/cli",
"argv0.roc",
&[],
&[],
&[],
"<ignored for tests>/argv0.roc\n",
UseValgrind::No,
TestCliCommands::Run,
)
}
#[test] #[test]
#[serial(cli_platform)] #[serial(cli_platform)]
#[cfg_attr(windows, ignore)] #[cfg_attr(windows, ignore)]