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.
"""Compile and run a C project, returning the program's standard output.
Every ``.c`` file in the project directory is compiled to an object file with
``gcc -c``, the objects are linked into a single executable, and the
executable is run so its output can be captured.
This is the standalone version of the code that used to live in the
"Build and Run a C Project from Quarto Using Python" post.
"""
import subprocess
import os
import glob
import stat
def 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"))
if not c_files:
raise FileNotFoundError("No C source files found in the project directory.")
object_files = []
# Compile each .c file into an object file
for 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 executable
try:
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)
if not os.path.exists(target_exe):
raise FileNotFoundError("Compiled executable not found.")
# Set the executable permissions
try:
os.chmod(
target_exe,
stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH,
)
except PermissionError as e:
print(f"Error setting permissions: {e}")
return
# Run the compiled executable and capture its output
try:
run_process = subprocess.run(
[target_exe], check=True, capture_output=True, text=True
)
output = run_process.stdout
return output
except subprocess.CalledProcessError as e:
print(f"Execution Error: {e.stderr}")
return
# Example usage
if __name__ == "__main__":
here = os.path.dirname(os.path.abspath(__file__))
output = compile_and_run_c(os.path.join(here, "..", "hello"))
print(output)Let’s see it in action.
Single File Example
"""Compile and run the single-file C example in ``hello/`` and print the output."""
import os
import run_c
here = os.path.dirname(os.path.abspath(__file__))
print(run_c.compile_and_run_c(os.path.join(here, "..", "hello")))1 Hello, world!
2 Hello, world!
3 Hello, world!
4 Hello, world!
5 Hello, world!
6 Hello, world!
7 Hello, world!
8 Hello, world!
9 Hello, world!
10 Hello, world!
11 Hello, world!
12 Hello, world!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.
// main.c
#include <stdio.h>
#include "helper1.h"
#include "helper2.h"
int main() {
printf("Starting program...\n");
helper1();
helper2();
printf("Program finished.\n");
return 0;
}// helper1.c
#include <stdio.h>
#include "helper1.h"
void helper1() {
printf("Hello from helper1!\n");
}// helper1.h
#ifndef HELPER1_H
#define HELPER1_H
void helper1();
#endif// helper2.c
#include <stdio.h>
#include "helper2.h"
void helper2() {
printf("Hello from helper2!\n");
}// helper2.h
#ifndef HELPER2_H
#define HELPER2_H
void helper2();
#endifNow we can similarly point run_c.compile_and_rune_c at the path where these files exist.
"""Compile and run the linked-files C example in ``hello2/`` and print the output."""
import os
import run_c
here = os.path.dirname(os.path.abspath(__file__))
print(run_c.compile_and_run_c(os.path.join(here, "..", "hello2")))Starting program...
Hello from helper1!
Hello from helper2!
Program finished.Conclusions
As long as the linking and compilation options are kept simple, this script allows you to compile simple C language 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.