Mercurial > hg
comparison rust/rhg/src/main.rs @ 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 | 6cd9f53aaed8 |
comparison
equal
deleted
inserted
replaced
46666:33f2d56acc73 | 46667:93e9f448273c |
---|---|
9 use hg::repo::{Repo, RepoError}; | 9 use hg::repo::{Repo, RepoError}; |
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; | 10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; |
11 use hg::utils::SliceExt; | 11 use hg::utils::SliceExt; |
12 use std::ffi::OsString; | 12 use std::ffi::OsString; |
13 use std::path::PathBuf; | 13 use std::path::PathBuf; |
14 use std::process::Command; | |
14 | 15 |
15 mod blackbox; | 16 mod blackbox; |
16 mod error; | 17 mod error; |
17 mod exitcode; | 18 mod exitcode; |
18 mod ui; | 19 mod ui; |
136 } | 137 } |
137 } | 138 } |
138 | 139 |
139 fn exit( | 140 fn exit( |
140 ui: &Ui, | 141 ui: &Ui, |
141 on_unsupported: OnUnsupported, | 142 mut on_unsupported: OnUnsupported, |
142 result: Result<(), CommandError>, | 143 result: Result<(), CommandError>, |
143 ) -> ! { | 144 ) -> ! { |
145 if let ( | |
146 OnUnsupported::Fallback { executable }, | |
147 Err(CommandError::UnsupportedFeature { .. }), | |
148 ) = (&on_unsupported, &result) | |
149 { | |
150 let mut args = std::env::args_os(); | |
151 let executable_path = get_path_from_bytes(&executable); | |
152 let this_executable = args.next().expect("exepcted argv[0] to exist"); | |
153 if executable_path == &PathBuf::from(this_executable) { | |
154 // Avoid spawning infinitely many processes until resource | |
155 // exhaustion. | |
156 let _ = ui.write_stderr(&format_bytes!( | |
157 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \ | |
158 points to `rhg` itself.\n", | |
159 executable | |
160 )); | |
161 on_unsupported = OnUnsupported::Abort | |
162 } else { | |
163 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]` | |
164 let result = Command::new(executable_path).args(args).status(); | |
165 match result { | |
166 Ok(status) => std::process::exit( | |
167 status.code().unwrap_or(exitcode::ABORT), | |
168 ), | |
169 Err(error) => { | |
170 let _ = ui.write_stderr(&format_bytes!( | |
171 b"tried to fall back to a '{}' sub-process but got error {}\n", | |
172 executable, format_bytes::Utf8(error) | |
173 )); | |
174 on_unsupported = OnUnsupported::Abort | |
175 } | |
176 } | |
177 } | |
178 } | |
144 match &result { | 179 match &result { |
145 Ok(_) => {} | 180 Ok(_) => {} |
146 Err(CommandError::Abort { message }) => { | 181 Err(CommandError::Abort { message }) => { |
147 if !message.is_empty() { | 182 if !message.is_empty() { |
148 // Ignore errors when writing to stderr, we’re already exiting | 183 // Ignore errors when writing to stderr, we’re already exiting |
158 b"unsupported feature: {}\n", | 193 b"unsupported feature: {}\n", |
159 message | 194 message |
160 )); | 195 )); |
161 } | 196 } |
162 OnUnsupported::AbortSilent => {} | 197 OnUnsupported::AbortSilent => {} |
198 OnUnsupported::Fallback { .. } => unreachable!(), | |
163 } | 199 } |
164 } | 200 } |
165 } | 201 } |
166 std::process::exit(exit_code(&result)) | 202 std::process::exit(exit_code(&result)) |
167 } | 203 } |
266 /// Print an error message describing what feature is not supported, | 302 /// Print an error message describing what feature is not supported, |
267 /// and exit with code 252. | 303 /// and exit with code 252. |
268 Abort, | 304 Abort, |
269 /// Silently exit with code 252. | 305 /// Silently exit with code 252. |
270 AbortSilent, | 306 AbortSilent, |
307 /// Try running a Python implementation | |
308 Fallback { executable: Vec<u8> }, | |
271 } | 309 } |
272 | 310 |
273 impl OnUnsupported { | 311 impl OnUnsupported { |
312 const DEFAULT: Self = OnUnsupported::Abort; | |
313 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg"; | |
314 | |
274 fn from_config(config: &Config) -> Self { | 315 fn from_config(config: &Config) -> Self { |
275 let default = OnUnsupported::Abort; | 316 match config |
276 match config.get(b"rhg", b"on-unsupported") { | 317 .get(b"rhg", b"on-unsupported") |
318 .map(|value| value.to_ascii_lowercase()) | |
319 .as_deref() | |
320 { | |
277 Some(b"abort") => OnUnsupported::Abort, | 321 Some(b"abort") => OnUnsupported::Abort, |
278 Some(b"abort-silent") => OnUnsupported::AbortSilent, | 322 Some(b"abort-silent") => OnUnsupported::AbortSilent, |
279 None => default, | 323 Some(b"fallback") => OnUnsupported::Fallback { |
324 executable: config | |
325 .get(b"rhg", b"fallback-executable") | |
326 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE) | |
327 .to_owned(), | |
328 }, | |
329 None => Self::DEFAULT, | |
280 Some(_) => { | 330 Some(_) => { |
281 // TODO: warn about unknown config value | 331 // TODO: warn about unknown config value |
282 default | 332 Self::DEFAULT |
283 } | 333 } |
284 } | 334 } |
285 } | 335 } |
286 } | 336 } |