diff --git a/src/libcamera/shaders/meson.build b/src/libcamera/shaders/meson.build
index adac77327a3b..4f4e8da607c7 100644
--- a/src/libcamera/shaders/meson.build
+++ b/src/libcamera/shaders/meson.build
@@ -14,7 +14,7 @@ libcamera_shader_headers = custom_target(
     'gen-shader-headers',
     input : [shader_files],
     output : 'glsl_shaders.h',
-    command : [gen_shader_headers, meson.project_source_root(), '@OUTPUT@', '@INPUT@'],
+    command : [gen_shader_headers, '-o', '@OUTPUT@', '@INPUT@'],
 )
 
 libcamera_internal_headers += libcamera_shader_headers
diff --git a/utils/gen-shader-header.py b/utils/gen-shader-header.py
deleted file mode 100755
index 745852b4c03f..000000000000
--- a/utils/gen-shader-header.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: GPL-2.0-or-later
-# Copyright (C) 2025, Bryan O'Donoghue.
-#
-# Author: Bryan O'Donoghue <bryan.odonoghue@linaro.org>
-#
-# A Python script which takes a list of shader files and converts each of them
-# into a C header.
-#
-import sys
-
-try:
-    with open(sys.argv[2], "rb") as file:
-        data = file.read()
-        data_len = len(data)
-
-        name = sys.argv[1].replace(".", "_")
-        name_len = name + "_len"
-
-        j = 0
-        print("unsigned char const", name, "[] = {")
-        for ch in data:
-            print(f"0x{ch:02x}, ", end="")
-            j = (j + 1) % 16
-            if j == 0:
-                print()
-        if j != 0:
-            print()
-        print("};")
-
-        print()
-        print(f"const unsigned int {name_len}={data_len};")
-
-except FileNotFoundError:
-    print(f"File {sys.argv[2]} not found", file=sys.stderr)
-except IOError:
-    print(f"Unable to read {sys.argv[2]}", file=sys.stderr)
diff --git a/utils/gen-shader-headers.py b/utils/gen-shader-headers.py
new file mode 100755
index 000000000000..fe4f06065f0c
--- /dev/null
+++ b/utils/gen-shader-headers.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2025, Bryan O'Donoghue
+# Copyright (C) 2026, Ideas on Board Oy
+#
+# Generate a C header file containing hex-encoded shader sources
+
+import argparse
+import binascii
+import math
+import pathlib
+import sys
+
+
+def process_file(path, out):
+    data = open(path, 'rb').read()
+
+    hex_data = [f'0x{c:02x}' for c in data]
+    var_name = path.name.replace('.', '_')
+
+    out.write(f'unsigned char const {var_name}[] = {{\n')
+
+    for i in range(math.ceil(len(data) / 16)):
+        out.write('\t')
+        out.write(', '.join(hex_data[16 * i:16 * (i + 1)]))
+        out.write(',\n')
+
+    out.write('};\n\n')
+    out.write(f'const unsigned int {var_name}_len = {len(data)};\n')
+
+
+def main(argv):
+    parser = argparse.ArgumentParser(
+        description='Generate a C header file containing hex-encoded shader sources')
+    parser.add_argument('--output', '-o', metavar='file', default=sys.stdout,
+                        type=argparse.FileType('w', encoding='utf-8'),
+                        help='Output file name. Defaults to standard output if not specified.')
+    parser.add_argument('inputs', nargs='+', type=pathlib.Path,
+                        help='Input file names.')
+    args = parser.parse_args(argv[1:])
+
+    args.output.write('''\
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/* This file is auto-generated, do not edit! */
+
+#pragma once
+
+/*
+ * List the names of the shaders at the top of header for readability's sake.
+ *
+''')
+
+    for path in args.inputs:
+        name = path.name.replace('.', '_')
+        args.output.write(f' * unsigned char {name};\n')
+
+    args.output.write('''\
+ */
+
+/* Hex encoded shader data */
+''')
+
+    for path in args.inputs:
+        process_file(path, args.output)
+        args.output.write('\n')
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main(sys.argv))
+
diff --git a/utils/gen-shader-headers.sh b/utils/gen-shader-headers.sh
deleted file mode 100755
index 0d6649521a7a..000000000000
--- a/utils/gen-shader-headers.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/sh
-# SPDX-License-Identifier: GPL-2.0-or-later
-
-set -e
-
-usage() {
-	echo "Usage: $0 <src_dir> <output_header> <shader_file1> [shader_file2 ...]"
-	echo
-	echo "Generates a C header file containing hex-encoded shader data."
-	echo
-	echo "Arguments:"
-	echo "  src_dir             Path to the base of the source directory"
-	echo "  output_header       Path to the generated header file"
-	echo "  shader_file(s)      One or more shader files to embed in the header"
-	exit 1
-}
-
-if [ $# -lt 4 ]; then
-	echo "Error: Invalid argument count."
-	usage
-fi
-
-src_dir="$1"; shift
-build_path="$1"; shift
-
-cat <<EOF > "$build_path"
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/* This file is auto-generated, do not edit! */
-
-#pragma once
-
-EOF
-
-cat <<EOF >> "$build_path"
-/*
- * List the names of the shaders at the top of
- * header for readability's sake
- *
-EOF
-
-for file in "$@"; do
-	name=$(basename "$file" | tr '.' '_')
-	echo "[SHADER-GEN] $name"
-	echo " * unsigned char $name;" >> "$build_path"
-done
-
-echo "*/" >> "$build_path"
-
-echo "/* Hex encoded shader data */" >> "$build_path"
-for file in "$@"; do
-	name=$(basename "$file")
-	"$src_dir/utils/gen-shader-header.py" "$name" "$file" >> "$build_path"
-	echo >> "$build_path"
-done
diff --git a/utils/meson.build b/utils/meson.build
index 9c598793035c..17a7aa7c3f5e 100644
--- a/utils/meson.build
+++ b/utils/meson.build
@@ -3,7 +3,7 @@
 subdir('codegen')
 subdir('ipu3')
 
-gen_shader_headers = files('gen-shader-headers.sh')
+gen_shader_headers = files('gen-shader-headers.py')
 
 ## Module signing
 gen_ipa_priv_key = files('gen-ipa-priv-key.sh')
