here is a proposal for how the roc CLI could manage its own versions: https://docs.google.com/document/d/1Or0Uf6thmFMKAwE4pcqvZBW6siim5BT2pIwxaD048TU/edit?usp=sharing
any thoughts welcome!
Love this!
Would this be compatible with asdf for people who want to manage their roc version the same way they manage versions of other tools?
Could a compiler compile code with a different version? I guess future compilers can have a list of what old versions they're compatible with. But could an old compiler try to compile code with a future version label, in case there weren't breaking changes or in case any incompatible new features aren't used in that codebase? Does this system force us to be really strict about using semver for compiler versions, even if something doesn't feel like a major release?
I'm particularly thinking about an app with many dependencies, all of which have slightly different versions. That could get hard to resolve. (Though I guess this problem exists in most programming ecosystems, but just with implicit language versions instead of explicit.)
Sky Rose said:
Would this be compatible with
asdffor people who want to manage their roc version the same way they manage versions of other tools?
hm, compatible in what way? :thinking:
Like, you can specify a version in .tool-versions (or mise.toml), and then that version would be used in that project, and there's a smooth and consistent experience, without roc and asdf fighting each other over which version to use.
Yes, that kind of thing would be good for a monorepo as well. I don't think there's an easy way to define deps per monorepo like the compiler does. Not being able to coordinate package versions isn't great, but it's manageable. A .roc-version, or more generally a roc.toml, would help here for larger projects, even if it birfurcates the version setting process
I don't think hardcoding the releases URI increases security. If a process has access to install Roc binaries they will need write and execute access on the relevant paths, at which point they could patch the binary to change the URI if they are malicious. Not hardcoding the path allows affordances like not needing to patch each binary if you want to download your own version (a very common thing in enterprises/high-availability situations where you want to have patched binaries or host them yourself)
I also don't quite understand the advantage of avoiding symlinks. Is the idea resolving symlinks is too slow? Having the CLI be a wrapper is nice because then if you try to run roc on something with an incompatible version the CLI can resolve it correctly for you, and also manage the installation directory, in a single process.
It has been a pain to debug code that used execve in the past
Ayaz Hafiz said:
I don't think hardcoding the releases URI increases security. If a process has access to install Roc binaries they will need write and execute access on the relevant paths, at which point they could patch the binary to change the URI if they are malicious. Not hardcoding the path allows affordances like not needing to patch each binary if you want to download your own version (a very common thing in enterprises/high-availability situations where you want to have patched binaries or host them yourself)
I figure in situations where someone is running their own patched binaries, or is hosting themselves, they'll just change the hardcoded URL anyway as part of the patch :big_smile:
Ayaz Hafiz said:
I also don't quite understand the advantage of avoiding symlinks. Is the idea resolving symlinks is too slow?
part of the reason I don't like it is that I want it to be possible to download a single executable and have it Just Work, not have running an installer being the only possible way to get roc to work.
I don't think it's a deal-breaker that symlinks are very slightly slower, but it does bother me to think that every single run of the compiler (and scripts run directly with roc) would be slowed down for the sake of a feature that would be used super rarely
Anton said:
It has been a pain to debug code that used execve in the past
hm, do you remember what the problems were? :thinking:
Sky Rose said:
Like, you can specify a version in .tool-versions (or mise.toml), and then that version would be used in that project, and there's a smooth and consistent experience, without roc and asdf fighting each other over which version to use.
I don't think the Roc compiler code base should have a concept of other third-party tools, if that's what you mean...as in, I don't think it should recognize files with particular filenames and behave differently if they're present
that said, I think the general idea of "this is a build intended to be used in Nix or apt or something else, so here is how to switch versions using that system" seems like it could be used for asdf or something similar as well
Richard Feldman said:
Anton said:
It has been a pain to debug code that used execve in the past
hm, do you remember what the problems were? :thinking:
I could not get it to forward the correct exit code, this is the code now:
match unsafe { libc::fork() } {
0 => unsafe {
// we are the child
executable.execve(&argv, &envp);
// Display a human-friendly error message
println!("Error {:?}", std::io::Error::last_os_error());
std::process::exit(1);
},
-1 => {
// something failed
// Display a human-friendly error message
println!("Error {:?}", std::io::Error::last_os_error());
std::process::exit(1)
}
pid @ 1.. => {
let sigchld = Arc::new(AtomicBool::new(false));
signal_hook::flag::register(signal_hook::consts::SIGCHLD, Arc::clone(&sigchld))
.unwrap();
let exit_code = loop {
match memory.wait_for_child(sigchld.clone()) {
ChildProcessMsg::Terminate => {
let mut status = 0;
let options = 0;
unsafe { libc::waitpid(pid, &mut status, options) };
// if `WIFEXITED` returns false, `WEXITSTATUS` will just return junk
break if libc::WIFEXITED(status) {
libc::WEXITSTATUS(status)
} else {
// we don't have an exit code, but something went wrong if we're in this else
1
};
}
ChildProcessMsg::Expect => {
let mut writer = std::io::stdout();
roc_repl_expect::run::render_expects_in_memory(
&mut writer,
arena,
&mut expectations,
&interns,
&layout_interner,
&memory,
)
.unwrap();
memory.reset();
}
}
};
std::process::exit(exit_code)
}
_ => unreachable!(),
}
We've got the expect stuff in there as well but it seems like using execve to call Roc would have sharp edges
ah gotcha
I think in this case that wouldn't come up, because we aren't trying to mess with the exit code
just literally forward a subset of the command-line arguments and that's it
The way I understand it we're not trying to mess with the exit code either, we just want to pass it, because you obviously don't want to ignore it.
To be fair, that isn't really an issue with execve and all the fork and expect chaos is going way
So it should just be execve and on failure exit the process with the errno
Last updated: Jun 16 2026 at 16:19 UTC