Build and Run a C Project from Quarto Using Python
C
Quarto
Python
subprocess
Author
Galen Seilis
Published
July 30, 2024
In this post I share a way to compile and run a C file using Python, which in turn can be used to render the output of the C program in Quarto.
Python Script
The Python script I developed is similar to the one I made for Rust, except that it does two phases. First gcc -c file.c is run on each file in a target path. Then gcc -o program main.o file1.o ... is run to put together the final program with appropriate linking.
import subprocessimport osimport globimport statdef log_permissions(path): st = os.stat(path) permissions = stat.filemode(st.st_mode)print(f"Permissions for {path}: {permissions}")def compile_and_run_c(project_dir): project_dir = os.path.abspath(project_dir)# Find all .c files in the project directory c_files = glob.glob(os.path.join(project_dir, '*.c'))ifnot c_files:raiseFileNotFoundError("No C source files found in the project directory.") object_files = []# Compile each .c file into an object filefor c_file in c_files: obj_file = os.path.splitext(c_file)[0] +'.o'try: compile_process = subprocess.run( ['gcc', '-c', c_file, '-o', obj_file], cwd=project_dir, check=True, capture_output=True, text=True ) object_files.append(obj_file)except subprocess.CalledProcessError as e:print(f"Compilation Error for {c_file}: {e.stderr}")return# Determine the name of the executable (assuming the file with main is called main.c) exe_name ='program' main_file = os.path.join(project_dir, 'main.c')if os.path.exists(main_file): exe_name = os.path.splitext(os.path.basename(main_file))[0]# Link all object files into a single executabletry: link_process = subprocess.run( ['gcc', '-o', exe_name] + object_files, cwd=project_dir, check=True, capture_output=True, text=True )except subprocess.CalledProcessError as e:print(f"Linking Error: {e.stderr}")return# Find the compiled executable target_exe = os.path.join(project_dir, exe_name)ifnot os.path.exists(target_exe):raiseFileNotFoundError("Compiled executable not found.")# Set the executable permissionstry: os.chmod(target_exe, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)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_c('../posts/c-run-from-python/hello')print(output)
In the above example I am only compiling and running a single C file, but it is certainly possible to compile multiple files with a further change. For each .c file in the project path, I could run gcc -c file.c on each file. Then I could run gcc -o program main.o file1.o file2.o. This way I can link the object files together.
Linked Files Example
In this example I define a main.c source file, and a couple of helper<#>.c source files along with their header files.
Starting program...
Hello from helper1!
Hello from helper2!
Program finished.
Conclusions
As long as the linking and compiled options are kept simple, this script allows you to compile simple C langauge programs. This may be suitable for ensuring that C code examples for blogging actually work. Expanding into autotools and make files is the way to go for more complicated builds.
The same limitation as the corresponding script to run Rust code applies: if your qmd file does not change while Quarto’s setting is freeze: auto, the page will not be rerendered if the C code changes even if the Quarto document is not changed.