Skip to content

Commit b6d57d9

Browse files
authored
Cmd API overhaul (#397)
* WIP Cmd API overhaul * overhaul progress * finished Cmd API * refactor Cmd stuff * error test coverage + tidying * misc fixes
1 parent e36f36f commit b6d57d9

16 files changed

Lines changed: 551 additions & 386 deletions

File tree

build.roc

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ roc_version! = |roc_cmd|
4747
info!("Checking provided roc; executing `${roc_cmd} version`:")?
4848

4949
Cmd.exec!(roc_cmd, ["version"])
50-
|> Result.map_err(RocVersionCheckFailed)
5150

5251
get_os_and_arch! : {} => Result OSAndArch _
5352
get_os_and_arch! = |{}|
@@ -78,7 +77,6 @@ build_stub_app_lib! = |roc_cmd, stub_lib_path|
7877
info!("Building stubbed app shared library ...")?
7978

8079
Cmd.exec!(roc_cmd, ["build", "--lib", "platform/libapp.roc", "--output", stub_lib_path, "--optimize"])
81-
|> Result.map_err(ErrBuildingAppStub)
8280

8381
stub_file_extension : OSAndArch -> Str
8482
stub_file_extension = |os_and_arch|
@@ -130,7 +128,6 @@ cargo_build_host! = |debug_mode|
130128
args = cargo_build_args!({})?
131129

132130
Cmd.exec!("cargo", args)
133-
|> Result.map_err(ErrBuildingHostBinaries)
134131

135132
copy_host_lib! : OSAndArch, Str => Result {} _
136133
copy_host_lib! = |os_and_arch, rust_target_folder|
@@ -142,7 +139,6 @@ copy_host_lib! = |os_and_arch, rust_target_folder|
142139
info!("Moving the prebuilt binary from ${host_build_path} to ${host_dest_path} ...")?
143140

144141
Cmd.exec!("cp", [host_build_path, host_dest_path])
145-
|> Result.map_err(ErrMovingPrebuiltLegacyBinary)
146142

147143
preprocess_host! : Str, Str, Str => Result {} _
148144
preprocess_host! = |roc_cmd, stub_lib_path, rust_target_folder|
@@ -152,7 +148,6 @@ preprocess_host! = |roc_cmd, stub_lib_path, rust_target_folder|
152148
surgical_build_path = "${rust_target_folder}host"
153149

154150
Cmd.exec!(roc_cmd, ["preprocess-host", surgical_build_path, "platform/main.roc", stub_lib_path])
155-
|> Result.map_err(ErrPreprocessingSurgicalBinary)
156151

157152
info! : Str => Result {} _
158153
info! = |msg|

ci/check_all_exposed_funs_tested.roc

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -106,39 +106,29 @@ is_function_unused! = |module_name, function_name|
106106
)?
107107

108108
# Check if ripgrep is installed
109-
rg_check_cmd = Cmd.new("rg") |> Cmd.arg("--version")
110-
rg_check_output = Cmd.output!(rg_check_cmd)
111-
112-
when rg_check_output.status is
113-
Ok(0) ->
114-
unused_in_dir =
115-
search_dirs
116-
|> List.map_try!( |search_dir|
117-
# Skip searching if directory doesn't exist
118-
dir_exists = File.is_dir!(search_dir)?
119-
if !dir_exists then
120-
Ok(Bool.true) # Consider unused if we can't search
121-
else
122-
# Use ripgrep to search for the function pattern
123-
cmd =
124-
Cmd.new("rg")
125-
|> Cmd.arg("-q") # Quiet mode - we only care about exit code
126-
|> Cmd.arg(function_pattern)
127-
|> Cmd.arg(search_dir)
128-
129-
status_res = Cmd.status!(cmd)
130-
131-
# ripgrep returns status 0 if matches were found, 1 if no matches
132-
when status_res is
133-
Ok(0) -> Ok(Bool.false) # Function is used (not unused)
134-
_ -> Ok(Bool.true)
135-
)?
136-
137-
unused_in_dir
138-
|> List.walk!(Bool.true, |state, is_unused_res| state && is_unused_res)
139-
|> Ok
140-
_ ->
141-
err_s("Error: ripgrep (rg) is not installed or not available in PATH. Please install ripgrep to use this script. Full output: ${Inspect.to_str(rg_check_output)}")
109+
_ = Cmd.exec!("rg", ["--version"]) ? |err| RipgrepNotInstalled(err)
110+
111+
112+
unused_in_dir =
113+
search_dirs
114+
|> List.map_try!( |search_dir|
115+
# Skip searching if directory doesn't exist
116+
dir_exists = File.is_dir!(search_dir)?
117+
if !dir_exists then
118+
Ok(Bool.true) # Consider unused if we can't search
119+
else
120+
# Use ripgrep to search for the function pattern
121+
cmd_res =
122+
Cmd.exec!("rg", ["-q", function_pattern, search_dir])
123+
124+
when cmd_res is
125+
Ok(_) -> Ok(Bool.false) # Function is used (not unused)
126+
_ -> Ok(Bool.true)
127+
)?
128+
129+
unused_in_dir
130+
|> List.walk!(Bool.true, |state, is_unused_res| state && is_unused_res)
131+
|> Ok
142132

143133

144134

ci/expect_scripts/cmd-test.exp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/expect
2+
3+
# uncomment line below for debugging
4+
# exp_internal 1
5+
6+
set timeout 7
7+
8+
source ./ci/expect_scripts/shared-code.exp
9+
10+
spawn $env(TESTS_DIR)cmd-test
11+
12+
13+
set expected_output [normalize_output {
14+
cat: non_existent.txt: No such file or directory
15+
cat: non_existent.txt: No such file or directory
16+
cat: non_existent.txt: No such file or directory
17+
All tests passed.
18+
}]
19+
20+
expect $expected_output {
21+
expect eof {
22+
check_exit_and_segfault
23+
}
24+
}
25+
26+
puts stderr "\nExpect script failed: output was not as expected. Diff the output with expected_output in this script. Alternatively, uncomment `exp_internal 1` to debug."
27+
exit 1

ci/expect_scripts/command.exp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ spawn $env(EXAMPLES_DIR)command
1212

1313
set expected_output [normalize_output {
1414
Hello
15-
Command output: Hi
16-
17-
Command output: BAZ=DUCK
15+
\{stderr_utf8_lossy: "", stdout_utf8: "Hi
16+
"\}
17+
BAZ=DUCK
1818
FOO=BAR
1919
XYZ=ABC
20-
21-
Yo
20+
cat: non_existent.txt: No such file or directory
21+
Exit code: 1
22+
\{stderr_bytes: \[\], stdout_bytes: \[72, 105, 10\]\}
2223
}]
2324

2425
expect $expected_output {

ci/expect_scripts/path-test.exp

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ Path with replaced extension: test_file.new
2424
Extension replaced: Bool.true
2525

2626
Testing Path file operations:
27-
test_path_bytes.txt exists: Bool.true
2827
Bytes written: \\\[72, 101, 108, 108, 111, 44, 32, 80, 97, 116, 104, 33\\\]
2928
Bytes read: \\\[72, 101, 108, 108, 111, 44, 32, 80, 97, 116, 104, 33\\\]
3029
Bytes match: Bool.true
@@ -35,13 +34,9 @@ UTF-8 content matches: Bool.true
3534
JSON content: {\"message\":\"Path test\",\"numbers\":\\\[1,2,3\\\]}
3635
JSON contains 'message' field: Bool.true
3736
JSON contains 'numbers' field: Bool.true
38-
File exists before delete: Bool.true
39-
File exists after delete: Bool.false
40-
41-
Testing Path directory operations:
42-
Created directory: drwxr-xr-x \\d+ \\w+ (\\w+ )? *\\d+ \\w+ +\\d+ \\d+:\\d+ test_single_dir
43-
Is a directory: Bool.true
37+
File no longer exists: Bool.true
4438

39+
Testing Path directory operations...
4540
Nested directory structure:
4641
test_parent
4742
test_parent/test_child
@@ -56,11 +51,10 @@ dr\[-rwx\]+ +\\d+ \\w+ (\\w+ )? *\\d+ \\w+ +\\d+ \\d+:\\d+ \\.\\.
5651
-\[-rwx\]+ +\\d+ \\w+ (\\w+ )? *\\d+ \\w+ +\\d+ \\d+:\\d+ file2\\.txt
5752
dr\[-rwx\]+ +\\d+ \\w+ (\\w+ )? *\\d+ \\w+ +\\d+ \\d+:\\d+ subdir
5853

59-
Empty dir exists before delete: Bool.true
60-
Empty dir exists after delete: Bool.false
54+
Empty dir was deleted: Bool.true
6155
Size before delete_all: \\d+\\w*\\s*test_parent
6256

63-
Parent dir exists after delete_all: Bool.false
57+
Parent dir no longer exists: Bool.true
6458

6559
Testing Path.hard_link!:
6660
Hard link count before: 1
@@ -96,7 +90,13 @@ Files to clean up:
9690
-rw-r--r-- \\d+ \\w+ \\w+ \\d+ \\w+ +\\d+ \\d+:\\d+ test_path_rename_new\\.txt
9791
-rw-r--r-- \\d+ \\w+ \\w+ \\d+ \\w+ +\\d+ \\d+:\\d+ test_path_utf8\\.txt
9892

99-
Files remaining after cleanup: Bool.false
93+
ls: cannot access .*
94+
ls: cannot access .*
95+
ls: cannot access .*
96+
ls: cannot access .*
97+
ls: cannot access .*
98+
ls: cannot access .*
99+
Files deleted successfully: Bool.true
100100
"]
101101

102102
expect -re $expected_output {

crates/roc_command/src/lib.rs

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! This crate provides common functionality for Roc to interface with `std::process::Command`
2+
23
use roc_std::{RocList, RocResult, RocStr};
34

45
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -61,29 +62,50 @@ impl From<&Command> for std::process::Command {
6162

6263
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
6364
#[repr(C)]
64-
pub struct OutputFromHost {
65-
pub status: roc_std::RocResult<i32, roc_io_error::IOErr>,
66-
pub stderr: roc_std::RocList<u8>,
67-
pub stdout: roc_std::RocList<u8>,
65+
pub struct OutputFromHostSuccess {
66+
pub stderr_bytes: roc_std::RocList<u8>,
67+
pub stdout_bytes: roc_std::RocList<u8>,
68+
}
69+
70+
#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
71+
#[repr(C)]
72+
pub struct OutputFromHostFailure {
73+
pub stderr_bytes: roc_std::RocList<u8>,
74+
pub stdout_bytes: roc_std::RocList<u8>,
75+
pub exit_code: i32,
6876
}
6977

70-
impl roc_std::RocRefcounted for OutputFromHost {
78+
impl roc_std::RocRefcounted for OutputFromHostSuccess {
7179
fn inc(&mut self) {
72-
self.status.inc();
73-
self.stderr.inc();
74-
self.stdout.inc();
80+
self.stdout_bytes.inc();
81+
self.stderr_bytes.inc();
7582
}
7683
fn dec(&mut self) {
77-
self.status.dec();
78-
self.stderr.dec();
79-
self.stdout.dec();
84+
self.stdout_bytes.dec();
85+
self.stderr_bytes.dec();
8086
}
8187
fn is_refcounted() -> bool {
8288
true
8389
}
8490
}
8591

86-
pub fn command_status(roc_cmd: &Command) -> RocResult<i32, roc_io_error::IOErr> {
92+
impl roc_std::RocRefcounted for OutputFromHostFailure {
93+
fn inc(&mut self) {
94+
self.exit_code.inc();
95+
self.stdout_bytes.inc();
96+
self.stderr_bytes.inc();
97+
}
98+
fn dec(&mut self) {
99+
self.exit_code.dec();
100+
self.stdout_bytes.dec();
101+
self.stderr_bytes.dec();
102+
}
103+
fn is_refcounted() -> bool {
104+
true
105+
}
106+
}
107+
108+
pub fn command_exec_exit_code(roc_cmd: &Command) -> RocResult<i32, roc_io_error::IOErr> {
87109
match std::process::Command::from(roc_cmd).status() {
88110
Ok(status) => from_exit_status(status),
89111
Err(err) => RocResult::err(err.into()),
@@ -94,29 +116,44 @@ pub fn command_status(roc_cmd: &Command) -> RocResult<i32, roc_io_error::IOErr>
94116
fn from_exit_status(status: std::process::ExitStatus) -> RocResult<i32, roc_io_error::IOErr> {
95117
match status.code() {
96118
Some(code) => RocResult::ok(code),
97-
None => killed_by_signal(),
119+
None => RocResult::err(killed_by_signal_err()),
98120
}
99121
}
100122

101-
// If no exit code is returned, the process was terminated by a signal.
102-
fn killed_by_signal() -> RocResult<i32, roc_io_error::IOErr> {
103-
RocResult::err(roc_io_error::IOErr {
123+
fn killed_by_signal_err() -> roc_io_error::IOErr {
124+
roc_io_error::IOErr {
104125
tag: roc_io_error::IOErrTag::Other,
105-
msg: "Killed by signal".into(),
106-
})
126+
msg: "Process was killed by operating system signal.".into(),
127+
}
107128
}
108129

109-
pub fn command_output(roc_cmd: &Command) -> OutputFromHost {
130+
// TODO Can we make this return a tag union (with three variants) ?
131+
pub fn command_exec_output(roc_cmd: &Command) -> RocResult<OutputFromHostSuccess, RocResult<OutputFromHostFailure, roc_io_error::IOErr>> {
110132
match std::process::Command::from(roc_cmd).output() {
111-
Ok(output) => OutputFromHost {
112-
status: from_exit_status(output.status),
113-
stdout: RocList::from(&output.stdout[..]),
114-
stderr: RocList::from(&output.stderr[..]),
115-
},
116-
Err(err) => OutputFromHost {
117-
status: RocResult::err(err.into()),
118-
stdout: RocList::empty(),
119-
stderr: RocList::empty(),
120-
},
133+
Ok(output) =>
134+
match output.status.code() {
135+
Some(status) => {
136+
137+
let stdout_bytes = RocList::from(&output.stdout[..]);
138+
let stderr_bytes = RocList::from(&output.stderr[..]);
139+
140+
if status == 0 {
141+
// Success case
142+
RocResult::ok(OutputFromHostSuccess {
143+
stderr_bytes,
144+
stdout_bytes,
145+
})
146+
} else {
147+
// Failure case
148+
RocResult::err(RocResult::ok(OutputFromHostFailure {
149+
stderr_bytes,
150+
stdout_bytes,
151+
exit_code: status,
152+
}))
153+
}
154+
},
155+
None => RocResult::err(RocResult::err(killed_by_signal_err()))
156+
}
157+
Err(err) => RocResult::err(RocResult::err(err.into()))
121158
}
122159
}

crates/roc_host/src/lib.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,8 +339,8 @@ pub fn init() {
339339
roc_fx_tcp_read_exactly as _,
340340
roc_fx_tcp_read_until as _,
341341
roc_fx_tcp_write as _,
342-
roc_fx_command_status as _,
343-
roc_fx_command_output as _,
342+
roc_fx_command_exec_exit_code as _,
343+
roc_fx_command_exec_output as _,
344344
roc_fx_dir_create as _,
345345
roc_fx_dir_create_all as _,
346346
roc_fx_dir_delete_empty as _,
@@ -742,17 +742,17 @@ pub extern "C" fn roc_fx_tcp_write(stream: RocBox<()>, msg: &RocList<u8>) -> Roc
742742
}
743743

744744
#[no_mangle]
745-
pub extern "C" fn roc_fx_command_status(
745+
pub extern "C" fn roc_fx_command_exec_exit_code(
746746
roc_cmd: &roc_command::Command,
747747
) -> RocResult<i32, roc_io_error::IOErr> {
748-
roc_command::command_status(roc_cmd)
748+
roc_command::command_exec_exit_code(roc_cmd)
749749
}
750750

751751
#[no_mangle]
752-
pub extern "C" fn roc_fx_command_output(
752+
pub extern "C" fn roc_fx_command_exec_output(
753753
roc_cmd: &roc_command::Command,
754-
) -> roc_command::OutputFromHost {
755-
roc_command::command_output(roc_cmd)
754+
) -> RocResult<roc_command::OutputFromHostSuccess, RocResult<roc_command::OutputFromHostFailure, roc_io_error::IOErr>> {
755+
roc_command::command_exec_output(roc_cmd)
756756
}
757757

758758
#[no_mangle]

0 commit comments

Comments
 (0)