Build and Run a Rust Project from Quarto Using Python
Rust
Quarto
Python
Cargo
rustc
Author
Galen Seilis
Published
July 29, 2024
In a previous post I used a Lua extension to compile a Rust file using the rustc compiler. I ran into multiple problems.
Using rustc rather than Cargo means that I miss out on a lot of the build tools, and it is also less conventional for Rust projects.
There were also issues with my plugin. It did not correctly turn off echo either locally to a code block, or to the global setting in the preamble of the Quarto file. It also did not put code on a new line. Further, I got feedback that this might not be supported. I also learned from a discussion answer on the Quarto Github discussion board that there are tools that might be better.
While other tools like evcxr look appealing, I have not looked into how to exactly integrate it with Quarto yet.
But there is a low-hanging fruit we can take advantage of here. We can certainly use Python subprocess library to indirectly orchestrate building and running a rust project. It also allows us to capture the output as text and return that into a Jupyter notebook. So that’s exaxtly what I made:
import subprocessimport osimport statdef log_permissions(path): st = os.stat(path) permissions = stat.filemode(st.st_mode)print(f"Permissions for {path}: {permissions}")def compile_and_run_rust(target_file):# Get the directory and the file name target_dir = os.path.dirname(target_file) target_name = os.path.basename(target_dir) # Adjusted to get the correct target name# Ensure Cargo.toml exists in the target directory cargo_toml_path = os.path.join(target_dir, 'Cargo.toml')ifnot os.path.exists(cargo_toml_path):raiseFileNotFoundError("Cargo.toml not found in the target directory.")# Compile the Rust projecttry: build_process = subprocess.run( ['cargo', 'build', '--release'], cwd=target_dir, check=True, capture_output=True, text=True )except subprocess.CalledProcessError as e:print(f"Compilation Error: {e.stderr}")return# Find the compiled executable target_exe = os.path.join(target_dir, 'target', 'release', target_name)if os.name =='nt': target_exe +='.exe'ifnot os.path.exists(target_exe):raiseFileNotFoundError("Compiled executable not found.")if os.name !='nt':try: os.chmod(target_exe, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) os.chmod(os.path.dirname(target_exe), stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)exceptPermissionErroras e:print(f"Error setting permissions: {e}")return# Run the compiled executable and capture its outputtry: run_process = subprocess.run( [target_exe], check=True, capture_output=True, text=True ) output = run_process.stdoutreturn outputexcept subprocess.CalledProcessError as e:print(f"Execution Error: {e.stderr}")return# Example usageif__name__=="__main__": output = compile_and_run_rust('../posts/rust-run-from-python/hello/main.rs')print(output)
Python Jupyter notebooks run Python in interactive mode, so it is slightly less convenient for importing Python files. Nonetheless this can be done by inserting our script into the path using the sys library. Once we have imported the run_rust file, we can call the compile_and_run_rust pointing to a Rust project path that is locally stored
Let us start a Rust project called “hello”.
cargo init hello
I also added a loop with a println macro just so we can see how this approach handles keeping newline characters. Here is the Rust code in hello/src/main.rs.
In conclusion, this approach using Python itself and Python Jupyter notebooks to compile, run, and display the printed output from a Rust program. It succeeds in preserving newline characters, and echo works locally. It also works globally for the whole file. The only remaining thing to watch out for in particular if Quarto’s automatic freezing of posts will not detect if you have changed the Rust code; you may need to change your qmd file in some way.