diff --git a/src/finn/core/utils.py b/src/finn/core/utils.py
index e036c91348510e8708d86bc5b5070fb421cae6f9..794661e4d7b25ac54875c52a1e85ef6718a88009 100644
--- a/src/finn/core/utils.py
+++ b/src/finn/core/utils.py
@@ -1,5 +1,6 @@
 import random
 import string
+import subprocess
 
 import numpy as np
 import onnx
@@ -195,7 +196,7 @@ def gen_finn_dt_tensor(finn_dt, tensor_shape):
     return tensor_values.astype(np.float32)
 
 
-class CallCppCompiler:
+class CppBuilder:
     def __init__(self):
         self.include_paths = []
         self.cpp_files = []
@@ -207,56 +208,27 @@ class CallCppCompiler:
     def append_includes(self, library_path):
         self.include_paths.append(library_path)
 
-    def prepare_cpp_files(self, node):
-        if not self.code_gen_dir:
-            raise ValueError(
-                """There is no generated code to compile
-                    for node of op type {}""".format(
-                    node.op_type
-                )
-            )
-        else:
-            self.cpp_files.append(
-                str(self.code_gen_dir) + "/execute_" + str(node.op_type) + ".cpp"
-            )
-            for lib in self.include_paths:
-                if "cnpy" in lib:
-                    self.cpp_files.append("/workspace/cnpy/cnpy.cpp")
-                    self.append_includes("-lz")
-
-    def set_executable_path(self, node):
-        if not self.code_gen_dir:
-            raise ValueError(
-                """There is no generated code to compile
-                    for node of op type {}""".format(
-                    node.op_type
-                )
-            )
-        else:
-            self.executable_path = (
-                str(self.code_gen_dir) + "/execute_" + str(node.op_type)
-            )
+    def append_sources(self, cpp_file):
+        self.cpp_files.append(cpp_file)
 
-    def build(self, node):
+    def set_executable_path(self, path):
+        self.executable_path = path
+
+    def build(self, code_gen_dir):
         # raise error if includes are empty
-        self.code_gen_dir = (get_by_name(node.attribute, "code_gen_dir")).s.decode(
-            "UTF-8"
-        )
-        self.prepare_cpp_files(node)
-        self.set_executable_path(node)
+        self.code_gen_dir = code_gen_dir
         self.compile_components.append("g++ -o " + str(self.executable_path))
         for cpp_file in self.cpp_files:
             self.compile_components.append(cpp_file)
         for lib in self.include_paths:
             self.compile_components.append(lib)
-
         bash_compile = ""
-
         for component in self.compile_components:
             bash_compile += str(component) + " "
-
         self.compile_script = str(self.code_gen_dir) + "/compile.sh"
-        f = open(self.compile_script, "w")
-        f.write("#!/bin/sh \n")
-        f.write(bash_compile)
-        f.close()
+        with open(self.compile_script, "w") as f:
+            f.write("#!/bin/bash \n")
+            f.write(bash_compile + "\n")
+        bash_command = ["bash", self.compile_script]
+        process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE)
+        process_compile.communicate()
diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py
index ae8d8aa692d29c8514bf98a9dc5c60ced7d08486..0d9a306baa85149f2294ec8b0f6ec1ccdd603dfe 100644
--- a/src/finn/custom_op/fpgadataflow/__init__.py
+++ b/src/finn/custom_op/fpgadataflow/__init__.py
@@ -1,6 +1,7 @@
 from abc import abstractmethod
 import os
 from finn.custom_op import CustomOp
+from finn.core.utils import CppBuilder
 
 
 class HLSCustomOp(CustomOp):
@@ -62,6 +63,21 @@ class HLSCustomOp(CustomOp):
         f.write(template)
         f.close()
 
+    def compile_singlenode_code(self):
+        code_gen_dir = self.get_nodeattr("code_gen_dir")
+        builder = CppBuilder()
+        builder.append_includes("-I/workspace/finn/src/finn/data/cpp")
+        builder.append_includes("-I/workspace/cnpy/")
+        builder.append_includes("-I/workspace/finn-hlslib")
+        builder.append_includes("-I/workspace/vivado-hlslib")
+        builder.append_includes("--std=c++11")
+        builder.append_sources(code_gen_dir + "/*.cpp")
+        builder.append_sources("/workspace/cnpy/cnpy.cpp")
+        builder.append_includes("-lz")
+        builder.set_executable_path(code_gen_dir + "/node_model")
+        builder.build(code_gen_dir)
+        self.set_nodeattr("executable_path", builder.executable_path)
+
     @abstractmethod
     def generate_weights(self, context):
         pass
diff --git a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
index 97831485dafe1b66d962f44330fed37ef35d903b..fdc66ce2b1fc370e01f70b132e2c64502dc42476 100644
--- a/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingfclayer_batch.py
@@ -247,7 +247,7 @@ class StreamingFCLayer_Batch(HLSCustomOp):
             in_ind += 1
         # execute precompiled executable
         executable_path = self.get_nodeattr("executable_path")
-        # TODO sanity check executable
+        assert executable_path != ""
         process_execute = subprocess.Popen(executable_path, stdout=subprocess.PIPE)
         process_execute.communicate()
         # load output npy file
diff --git a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
index 56d43cac7e8383505c0b31ba41a28f88aee35175..8f497168ee4b9db81cc508474699aa706e84b3f1 100644
--- a/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
+++ b/src/finn/custom_op/fpgadataflow/streamingmaxpool_batch.py
@@ -38,7 +38,7 @@ class StreamingMaxPool_Batch(HLSCustomOp):
             in_ind += 1
         # execute precompiled executable
         executable_path = self.get_nodeattr("executable_path")
-        # TODO sanity check executable
+        assert executable_path != ""
         process_execute = subprocess.Popen(executable_path, stdout=subprocess.PIPE)
         process_execute.communicate()
         # load output npy file
diff --git a/src/finn/transformation/fpgadataflow/compilation_transformation.py b/src/finn/transformation/fpgadataflow/compilation_transformation.py
index d06af8682a81ed951c6f9e3554bf63dd1c3e457a..8d158c4a5561bfeec4c17b9e5fa5c3df5a7a96bd 100644
--- a/src/finn/transformation/fpgadataflow/compilation_transformation.py
+++ b/src/finn/transformation/fpgadataflow/compilation_transformation.py
@@ -1,7 +1,5 @@
-import subprocess
-
 import finn.core.utils as util
-from finn.core.utils import CallCppCompiler
+import finn.custom_op.registry as registry
 from finn.transformation import Transformation
 
 
@@ -10,40 +8,26 @@ class Compilation(Transformation):
 
     def __init__(self):
         super().__init__()
-        self.compiler_call = CallCppCompiler()
-
-    def get_includes(self):
-        # step by step addition of include paths to ensure easy extension
-        self.compiler_call.append_includes("-I/workspace/finn/src/finn/data/cpp")
-        self.compiler_call.append_includes("-I/workspace/cnpy/")
-        self.compiler_call.append_includes("-I/workspace/finn-hlslib")
-        self.compiler_call.append_includes("-I/workspace/vivado-hlslib")
-        self.compiler_call.append_includes("--std=c++11")
-
-    def prepare_bash_command(self, node):
-        self.get_includes()
-        self.compiler_call.build(node)
-        bash_command = "chmod +x " + str(self.compiler_call.compile_script)
-        process_compile = subprocess.Popen(bash_command.split(), stdout=subprocess.PIPE)
-        process_compile.communicate()
-        print(self.compiler_call.code_gen_dir)
 
     def apply(self, model):
-
         for node in model.graph.node:
+            op_type = node.op_type
             if node.domain == "finn":
                 backend_attribute = util.get_by_name(node.attribute, "backend")
                 backend_value = backend_attribute.s.decode("UTF-8")
                 if backend_value == "fpgadataflow":
-                    self.prepare_bash_command(node)
-                    bash_command = self.compiler_call.compile_script
-                    process_compile = subprocess.Popen(
-                        bash_command.split(), stdout=subprocess.PIPE
-                    )
-                    process_compile.communicate()
-
-                    model.set_attribute(
-                        node, "executable_path", self.compiler_call.executable_path
-                    )
-
+                    try:
+                        # lookup op_type in registry of CustomOps
+                        inst = registry.custom_op[op_type](node)
+                        # ensure that code is generated
+                        assert inst.get_nodeattr("code_gen_dir") != ""
+                        # call the compilation function for this node
+                        inst.compile_singlenode_code()
+                        # ensure that executable path is now set
+                        assert inst.get_nodeattr("executable_path") != ""
+                    except KeyError:
+                        # exception if op_type is not supported
+                        raise Exception(
+                            "Custom op_type %s is currently not supported." % op_type
+                        )
         return (model, False)