Skip to main content

rustpython/
lib.rs

1//! This is the `rustpython` binary. If you're looking to embed RustPython into your application,
2//! you're likely looking for the [`rustpython_vm`] crate.
3//!
4//! You can install `rustpython` with `cargo install rustpython`. If you'd like to inject your
5//! own native modules, you can make a binary crate that depends on the `rustpython` crate (and
6//! probably [`rustpython_vm`], too), and make a `main.rs` that looks like:
7//!
8//! ```no_run
9//! use rustpython::{InterpreterBuilder, InterpreterBuilderExt};
10//! use rustpython_vm::{pymodule, py_freeze};
11//!
12//! fn main() -> std::process::ExitCode {
13//!     let builder = InterpreterBuilder::new().init_stdlib();
14//!     // Add a native module using builder.ctx
15//!     let my_mod_def = my_mod::module_def(&builder.ctx);
16//!     let builder = builder
17//!         .add_native_module(my_mod_def)
18//!         // Add a frozen module
19//!         .add_frozen_modules(py_freeze!(source = "def foo(): pass", module_name = "other_thing"));
20//!
21//!     rustpython::run(builder)
22//! }
23//!
24//! #[pymodule]
25//! mod my_mod {
26//!     use rustpython_vm::builtins::PyStrRef;
27//!
28//!     #[pyfunction]
29//!     fn do_thing(x: i32) -> i32 {
30//!         x + 1
31//!     }
32//!
33//!     #[pyfunction]
34//!     fn other_thing(s: PyStrRef) -> (String, usize) {
35//!         let new_string = format!("hello from rust, {}!", s);
36//!         let prev_len = s.byte_len();
37//!         (new_string, prev_len)
38//!     }
39//! }
40//! ```
41//!
42//! The binary will have all the standard arguments of a python interpreter (including a REPL!) but
43//! it will have your modules loaded into the vm.
44//!
45//! See [`rustpython_derive`](../rustpython_derive/index.html) crate for documentation on macros used in the example above.
46
47#![allow(clippy::needless_doctest_main)]
48
49#[macro_use]
50extern crate log;
51
52#[cfg(feature = "flame-it")]
53use vm::Settings;
54
55mod interpreter;
56mod settings;
57mod shell;
58
59#[cfg(feature = "rustpython-pylib")]
60pub use rustpython_pylib as pylib;
61
62use rustpython_vm::{AsObject, PyObjectRef, PyResult, VirtualMachine, scope::Scope};
63use std::env;
64use std::io::IsTerminal;
65use std::process::ExitCode;
66
67pub use interpreter::InterpreterBuilderExt;
68pub use rustpython_vm::{self as vm, Interpreter, InterpreterBuilder};
69pub use settings::{InstallPipMode, RunMode, parse_opts};
70pub use shell::run_shell;
71
72#[cfg(all(
73    feature = "ssl",
74    not(any(feature = "ssl-rustls", feature = "ssl-openssl"))
75))]
76compile_error!(
77    "Feature \"ssl\" is now enabled by either \"ssl-rustls\" or \"ssl-openssl\" to be enabled. Do not manually pass \"ssl\" feature. To enable ssl-openssl, use --no-default-features to disable ssl-rustls"
78);
79
80/// The main cli of the `rustpython` interpreter. This function will return `std::process::ExitCode`
81/// based on the return code of the python code ran through the cli.
82///
83/// **Note**: This function provides no way to further initialize the VM after the builder is applied.
84/// All VM initialization (adding native modules, init hooks, etc.) must be done through the
85/// [`InterpreterBuilder`] parameter before calling this function.
86pub fn run(mut builder: InterpreterBuilder) -> ExitCode {
87    env_logger::init();
88
89    // NOTE: This is not a WASI convention. But it will be convenient since POSIX shell always defines it.
90    #[cfg(target_os = "wasi")]
91    {
92        if let Ok(pwd) = env::var("PWD") {
93            let _ = env::set_current_dir(pwd);
94        };
95    }
96
97    let (settings, run_mode) = match parse_opts() {
98        Ok(x) => x,
99        Err(e) => {
100            println!("{e}");
101            return ExitCode::FAILURE;
102        }
103    };
104
105    // don't translate newlines (\r\n <=> \n)
106    #[cfg(windows)]
107    {
108        unsafe extern "C" {
109            fn _setmode(fd: i32, flags: i32) -> i32;
110        }
111        unsafe {
112            _setmode(0, libc::O_BINARY);
113            _setmode(1, libc::O_BINARY);
114            _setmode(2, libc::O_BINARY);
115        }
116    }
117
118    builder = builder.settings(settings);
119
120    let interp = builder.interpreter();
121    let exitcode = interp.run(move |vm| run_rustpython(vm, run_mode));
122
123    rustpython_vm::common::os::exit_code(exitcode)
124}
125
126fn get_pip(scope: Scope, vm: &VirtualMachine) -> PyResult<()> {
127    let get_getpip = rustpython_vm::py_compile!(
128        source = r#"\
129__import__("io").TextIOWrapper(
130    __import__("urllib.request").request.urlopen("https://bootstrap.pypa.io/get-pip.py")
131).read()
132"#,
133        mode = "eval"
134    );
135    eprintln!("downloading get-pip.py...");
136    let getpip_code = vm.run_code_obj(vm.ctx.new_code(get_getpip), vm.new_scope_with_builtins())?;
137    let getpip_code: rustpython_vm::builtins::PyStrRef = getpip_code
138        .downcast()
139        .expect("TextIOWrapper.read() should return str");
140    eprintln!("running get-pip.py...");
141    vm.run_string(scope, getpip_code.expect_str(), "get-pip.py".to_owned())?;
142    Ok(())
143}
144
145fn install_pip(installer: InstallPipMode, scope: Scope, vm: &VirtualMachine) -> PyResult<()> {
146    if !cfg!(feature = "ssl") {
147        return Err(vm.new_exception_msg(
148            vm.ctx.exceptions.system_error.to_owned(),
149            "install-pip requires rustpython be build with '--features=ssl'".into(),
150        ));
151    }
152
153    match installer {
154        InstallPipMode::Ensurepip => vm.run_module("ensurepip"),
155        InstallPipMode::GetPip => get_pip(scope, vm),
156    }
157}
158
159// pymain_run_file_obj in Modules/main.c
160fn run_file(vm: &VirtualMachine, scope: Scope, path: &str) -> PyResult<()> {
161    // Check if path is a package/directory with __main__.py
162    if let Some(_importer) = get_importer(path, vm)? {
163        vm.insert_sys_path(vm.new_pyobj(path))?;
164        let runpy = vm.import("runpy", 0)?;
165        let run_module_as_main = runpy.get_attr("_run_module_as_main", vm)?;
166        run_module_as_main.call((vm::identifier!(vm, __main__).to_owned(), false), vm)?;
167        return Ok(());
168    }
169
170    // Add script directory to sys.path[0]
171    if !vm.state.config.settings.safe_path {
172        let dir = std::path::Path::new(path)
173            .parent()
174            .and_then(|p| p.to_str())
175            .unwrap_or("");
176        vm.insert_sys_path(vm.new_pyobj(dir))?;
177    }
178
179    #[cfg(feature = "host_env")]
180    {
181        vm.run_any_file(scope, path)
182    }
183    #[cfg(not(feature = "host_env"))]
184    {
185        // In sandbox mode, the binary reads the file and feeds source to the VM.
186        // The VM itself has no filesystem access.
187        let path = if path.is_empty() { "???" } else { path };
188        match std::fs::read_to_string(path) {
189            Ok(source) => vm.run_string(scope, &source, path.to_owned()).map(drop),
190            Err(err) => Err(vm.new_os_error(err.to_string())),
191        }
192    }
193}
194
195fn get_importer(path: &str, vm: &VirtualMachine) -> PyResult<Option<PyObjectRef>> {
196    use rustpython_vm::builtins::PyDictRef;
197    use rustpython_vm::convert::TryFromObject;
198
199    let path_importer_cache = vm.sys_module.get_attr("path_importer_cache", vm)?;
200    let path_importer_cache = PyDictRef::try_from_object(vm, path_importer_cache)?;
201    if let Some(importer) = path_importer_cache.get_item_opt(path, vm)? {
202        return Ok(Some(importer));
203    }
204    let path_obj = vm.ctx.new_str(path);
205    let path_hooks = vm.sys_module.get_attr("path_hooks", vm)?;
206    let mut importer = None;
207    let path_hooks: Vec<PyObjectRef> = path_hooks.try_into_value(vm)?;
208    for path_hook in path_hooks {
209        match path_hook.call((path_obj.clone(),), vm) {
210            Ok(imp) => {
211                importer = Some(imp);
212                break;
213            }
214            Err(e) if e.fast_isinstance(vm.ctx.exceptions.import_error) => continue,
215            Err(e) => return Err(e),
216        }
217    }
218    Ok(if let Some(imp) = importer {
219        let imp = path_importer_cache.get_or_insert(vm, path_obj.into(), || imp.clone())?;
220        Some(imp)
221    } else {
222        None
223    })
224}
225
226// pymain_run_python
227fn run_rustpython(vm: &VirtualMachine, run_mode: RunMode) -> PyResult<()> {
228    #[cfg(feature = "flame-it")]
229    let main_guard = flame::start_guard("RustPython main");
230
231    let scope = vm.new_scope_with_main()?;
232
233    // Initialize warnings module to process sys.warnoptions
234    // _PyWarnings_Init()
235    if vm.import("warnings", 0).is_err() {
236        warn!("Failed to import warnings module");
237    }
238
239    // Import site first, before setting sys.path[0]
240    // This matches CPython's behavior where site.removeduppaths() runs
241    // before sys.path[0] is set, preventing '' from being converted to cwd
242    let site_result = vm.import("site", 0);
243    if site_result.is_err() {
244        warn!(
245            "Failed to import site, consider adding the Lib directory to your RUSTPYTHONPATH \
246             environment variable",
247        );
248    }
249
250    // _PyPathConfig_ComputeSysPath0 - set sys.path[0] after site import
251    if !vm.state.config.settings.safe_path {
252        let path0: Option<String> = match &run_mode {
253            RunMode::Command(_) => Some(String::new()),
254            RunMode::Module(_) => env::current_dir()
255                .ok()
256                .and_then(|p| p.to_str().map(|s| s.to_owned())),
257            RunMode::Script(_) | RunMode::InstallPip(_) => None, // handled by run_script
258            RunMode::Repl => Some(String::new()),
259        };
260
261        if let Some(path) = path0 {
262            vm.insert_sys_path(vm.new_pyobj(path))?;
263        }
264    }
265
266    // Enable faulthandler if -X faulthandler, PYTHONFAULTHANDLER or -X dev is set
267    // _PyFaulthandler_Init()
268    if vm.state.config.settings.faulthandler {
269        let _ = vm.run_simple_string("import faulthandler; faulthandler.enable()");
270    }
271
272    let is_repl = matches!(run_mode, RunMode::Repl);
273    if !vm.state.config.settings.quiet
274        && (vm.state.config.settings.verbose > 0 || (is_repl && std::io::stdin().is_terminal()))
275    {
276        eprintln!(
277            "Welcome to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}",
278            env!("CARGO_PKG_VERSION")
279        );
280        eprintln!(
281            "RustPython {}.{}.{}",
282            vm::version::MAJOR,
283            vm::version::MINOR,
284            vm::version::MICRO,
285        );
286
287        eprintln!("Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.");
288    }
289    let res = match run_mode {
290        RunMode::Command(command) => {
291            debug!("Running command {command}");
292            vm.run_string(scope.clone(), &command, "<string>".to_owned())
293                .map(drop)
294        }
295        RunMode::Module(module) => {
296            debug!("Running module {module}");
297            vm.run_module(&module)
298        }
299        RunMode::InstallPip(installer) => install_pip(installer, scope.clone(), vm),
300        RunMode::Script(script_path) => {
301            // pymain_run_file_obj
302            debug!("Running script {}", &script_path);
303            run_file(vm, scope.clone(), &script_path)
304        }
305        RunMode::Repl => Ok(()),
306    };
307    let result = if is_repl || vm.state.config.settings.inspect {
308        shell::run_shell(vm, scope)
309    } else {
310        res
311    };
312
313    #[cfg(feature = "flame-it")]
314    {
315        main_guard.end();
316        if let Err(e) = write_profile(&vm.state.as_ref().config.settings) {
317            error!("Error writing profile information: {}", e);
318        }
319    }
320
321    result
322}
323
324#[cfg(feature = "flame-it")]
325fn write_profile(settings: &Settings) -> Result<(), Box<dyn core::error::Error>> {
326    use std::{fs, io};
327
328    enum ProfileFormat {
329        Html,
330        Text,
331        SpeedScope,
332    }
333    let profile_output = settings.profile_output.as_deref();
334    let profile_format = match settings.profile_format.as_deref() {
335        Some("html") => ProfileFormat::Html,
336        Some("text") => ProfileFormat::Text,
337        None if profile_output == Some("-".as_ref()) => ProfileFormat::Text,
338        // spell-checker:ignore speedscope
339        Some("speedscope") | None => ProfileFormat::SpeedScope,
340        Some(other) => {
341            error!("Unknown profile format {}", other);
342            // TODO: Need to change to ExitCode or Termination
343            std::process::exit(1);
344        }
345    };
346
347    let profile_output = profile_output.unwrap_or_else(|| match profile_format {
348        ProfileFormat::Html => "flame-graph.html".as_ref(),
349        ProfileFormat::Text => "flame.txt".as_ref(),
350        ProfileFormat::SpeedScope => "flamescope.json".as_ref(),
351    });
352
353    let profile_output: Box<dyn io::Write> = if profile_output == "-" {
354        Box::new(io::stdout())
355    } else {
356        Box::new(fs::File::create(profile_output)?)
357    };
358
359    let profile_output = io::BufWriter::new(profile_output);
360
361    match profile_format {
362        ProfileFormat::Html => flame::dump_html(profile_output)?,
363        ProfileFormat::Text => flame::dump_text_to_writer(profile_output)?,
364        ProfileFormat::SpeedScope => flamescope::dump(profile_output)?,
365    }
366
367    Ok(())
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use rustpython_vm::Interpreter;
374
375    fn interpreter() -> Interpreter {
376        InterpreterBuilder::new().init_stdlib().interpreter()
377    }
378
379    #[test]
380    fn test_run_script() {
381        interpreter().enter(|vm| {
382            vm.unwrap_pyresult((|| {
383                let scope = vm.new_scope_with_main()?;
384                // test file run
385                run_file(vm, scope, "extra_tests/snippets/dir_main/__main__.py")?;
386
387                #[cfg(feature = "host_env")]
388                {
389                    let scope = vm.new_scope_with_main()?;
390                    // test module run (directory with __main__.py)
391                    run_file(vm, scope, "extra_tests/snippets/dir_main")?;
392                }
393
394                Ok(())
395            })());
396        })
397    }
398}