Mercurial > hg
changeset 46667:93e9f448273c
rhg: Add support for automatic fallback to Python
`rhg` is a command-line application that can do a small subset of what
`hg` can. It is written entirely in Rust, which avoids the cost of starting
a Python interpreter and importing many Python modules.
In a script that runs many `hg` commands, this cost can add up.
However making users decide when to use `rhg` instead of `hg` is
not practical as we want the subset of supported functionality
to grow over time.
Instead we introduce "fallback" behavior where, when `rhg` encounters
something (a sub-command, a repository format, …) that is not implemented
in Rust-only, it does nothing but silently start a subprocess of
Python-based `hg` running the same command.
That way `rhg` becomes a drop-in replacement for `hg` that sometimes
goes faster. Whether Python is used should be an implementation detail
not apparent to users (other than through speed).
A new `fallback` value is added to the previously introduced
`rhg.on-unsupported` configuration key. When in this mode, the new
`rhg.fallback-executable` config is determine what command to use
to run a Python-based `hg`.
The previous `rhg.on-unsupported = abort-silent` configuration was designed
to let a wrapper script call `rhg` and then fall back to `hg` based on the
exit code. This is still available, but having fallback behavior built-in
in rhg might be easier for users instead of leaving that script "as an
exercise for the reader".
Using a subprocess like this is not idea, especially when `rhg` is to be
installed in `$PATH` as `hg`, since the other `hg.py` executable needs
to still be available… somewhere. Eventually this could be replaced
by using PyOxidizer to a have a single executable that embeds a Python
interpreter, but only starts it when needed.
Differential Revision: https://phab.mercurial-scm.org/D10093
author | Simon Sapin <simon.sapin@octobus.net> |
---|---|
date | Mon, 01 Mar 2021 20:36:06 +0100 |
parents | 33f2d56acc73 |
children | fb2368598281 |
files | rust/rhg/src/main.rs tests/test-rhg.t |
diffstat | 2 files changed, 79 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- a/rust/rhg/src/main.rs Mon Mar 01 16:18:42 2021 +0100 +++ b/rust/rhg/src/main.rs Mon Mar 01 20:36:06 2021 +0100 @@ -11,6 +11,7 @@ use hg::utils::SliceExt; use std::ffi::OsString; use std::path::PathBuf; +use std::process::Command; mod blackbox; mod error; @@ -138,9 +139,43 @@ fn exit( ui: &Ui, - on_unsupported: OnUnsupported, + mut on_unsupported: OnUnsupported, result: Result<(), CommandError>, ) -> ! { + if let ( + OnUnsupported::Fallback { executable }, + Err(CommandError::UnsupportedFeature { .. }), + ) = (&on_unsupported, &result) + { + let mut args = std::env::args_os(); + let executable_path = get_path_from_bytes(&executable); + let this_executable = args.next().expect("exepcted argv[0] to exist"); + if executable_path == &PathBuf::from(this_executable) { + // Avoid spawning infinitely many processes until resource + // exhaustion. + let _ = ui.write_stderr(&format_bytes!( + b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ + points to `rhg` itself.\n", + executable + )); + on_unsupported = OnUnsupported::Abort + } else { + // `args` is now `argv[1..]` since we’ve already consumed `argv[0]` + let result = Command::new(executable_path).args(args).status(); + match result { + Ok(status) => std::process::exit( + status.code().unwrap_or(exitcode::ABORT), + ), + Err(error) => { + let _ = ui.write_stderr(&format_bytes!( + b"tried to fall back to a '{}' sub-process but got error {}\n", + executable, format_bytes::Utf8(error) + )); + on_unsupported = OnUnsupported::Abort + } + } + } + } match &result { Ok(_) => {} Err(CommandError::Abort { message }) => { @@ -160,6 +195,7 @@ )); } OnUnsupported::AbortSilent => {} + OnUnsupported::Fallback { .. } => unreachable!(), } } } @@ -268,18 +304,32 @@ Abort, /// Silently exit with code 252. AbortSilent, + /// Try running a Python implementation + Fallback { executable: Vec<u8> }, } impl OnUnsupported { + const DEFAULT: Self = OnUnsupported::Abort; + const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg"; + fn from_config(config: &Config) -> Self { - let default = OnUnsupported::Abort; - match config.get(b"rhg", b"on-unsupported") { + match config + .get(b"rhg", b"on-unsupported") + .map(|value| value.to_ascii_lowercase()) + .as_deref() + { Some(b"abort") => OnUnsupported::Abort, Some(b"abort-silent") => OnUnsupported::AbortSilent, - None => default, + Some(b"fallback") => OnUnsupported::Fallback { + executable: config + .get(b"rhg", b"fallback-executable") + .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE) + .to_owned(), + }, + None => Self::DEFAULT, Some(_) => { // TODO: warn about unknown config value - default + Self::DEFAULT } } }
--- a/tests/test-rhg.t Mon Mar 01 16:18:42 2021 +0100 +++ b/tests/test-rhg.t Mon Mar 01 20:36:06 2021 +0100 @@ -1,9 +1,10 @@ #require rust Define an rhg function that will only run if rhg exists + $ RHG="$RUNTESTDIR/../rust/target/release/rhg" $ rhg() { - > if [ -f "$RUNTESTDIR/../rust/target/release/rhg" ]; then - > "$RUNTESTDIR/../rust/target/release/rhg" "$@" + > if [ -f "$RHG" ]; then + > "$RHG" "$@" > else > echo "skipped: Cannot find rhg. Try to run cargo build in rust/rhg." > exit 80 @@ -151,6 +152,27 @@ $ rhg cat -r 1 copy_of_original original content +Fallback to Python + $ rhg cat original + unsupported feature: `rhg cat` without `--rev` / `-r` + [252] + $ FALLBACK="--config rhg.on-unsupported=fallback" + $ rhg cat original $FALLBACK + original content + + $ rhg cat original $FALLBACK --config rhg.fallback-executable=false + [1] + + $ rhg cat original $FALLBACK --config rhg.fallback-executable=hg-non-existent + tried to fall back to a 'hg-non-existent' sub-process but got error $ENOENT$ + unsupported feature: `rhg cat` without `--rev` / `-r` + [252] + + $ rhg cat original $FALLBACK --config rhg.fallback-executable="$RHG" + Blocking recursive fallback. The 'rhg.fallback-executable = */rust/target/release/rhg' config points to `rhg` itself. (glob) + unsupported feature: `rhg cat` without `--rev` / `-r` + [252] + Requirements $ rhg debugrequirements dotencode