diff --git a/BUILD.bazel b/BUILD.bazel
index c58ddfe6..f00979c8 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -4,7 +4,7 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library")
 load("@rules_python//python:packaging.bzl", "py_package", "py_wheel")
 load("@py_dev_requirements//:requirements.bzl", "requirement")
 load("//common:defs.bzl", "copy_file")
-load("//py:defs.bzl", "py_test_suite")
+load("//py:defs.bzl", "generate_devtools", "py_test_suite")
 load("//py/private:browsers.bzl", "BROWSERS")
 load("//py/private:import.bzl", "py_import")
 load("@rules_python//python:pip.bzl", "compile_pip_requirements")
@@ -273,18 +273,14 @@ py_binary(
     deps = [requirement("inflection")],
 )
 
-[genrule(
-    name = "create-cdp-srcs-" + n,
-    srcs = [
-        "//common/devtools/chromium/" + n + ":browser_protocol",
-        "//common/devtools/chromium/" + n + ":js_protocol",
-    ],
-    outs = ["selenium/webdriver/common/devtools/" + n],
-    cmd = "$(location :generate) $(location //common/devtools/chromium/" + n + ":browser_protocol) $(location //common/devtools/chromium/" + n + ":js_protocol) $@",
-    tools = [
-        ":generate",
-    ],
-) for n in BROWSER_VERSIONS]
+[generate_devtools(
+    name = "create-cdp-srcs-{}".format(devtools_version),
+    outdir = "selenium/webdriver/common/devtools/{}".format(devtools_version),
+    browser_protocol = "//common/devtools/chromium/{}:browser_protocol".format(devtools_version),
+    generator = ":generate",
+    js_protocol = "//common/devtools/chromium/{}:js_protocol".format(devtools_version),
+    protocol_version = devtools_version,
+) for devtools_version in BROWSER_VERSIONS]
 
 py_test_suite(
     name = "unit",
diff --git a/defs.bzl b/defs.bzl
index df25fa5c..8f8b4ff9 100644
--- a/defs.bzl
+++ b/defs.bzl
@@ -1,7 +1,9 @@
+load("//py/private:generate_devtools.bzl", _generate_devtools = "generate_devtools")
 load("//py/private:import.bzl", _py_import = "py_import")
 load("//py/private:pytest.bzl", _pytest_test = "pytest_test")
 load("//py/private:suite.bzl", _py_test_suite = "py_test_suite")
 
+generate_devtools = _generate_devtools
 pytest_test = _pytest_test
 py_import = _py_import
 py_test_suite = _py_test_suite
diff --git a/generate.py b/generate.py
index 4ebd69b6..5d6ea7cb 100644
--- a/generate.py
+++ b/generate.py
@@ -984,7 +984,6 @@ def main(browser_protocol_path, js_protocol_path, output_path):
         browser_protocol_path,
         js_protocol_path,
     ]
-    output_path.mkdir(parents=True)
 
     # Generate util.py
     util_path = output_path / "util.py"
diff --git a/private/generate_devtools.bzl b/private/generate_devtools.bzl
new file mode 100644
index 00000000..5fea436e
--- /dev/null
+++ b/private/generate_devtools.bzl
@@ -0,0 +1,50 @@
+def _generate_devtools_impl(ctx):
+    outdir = ctx.actions.declare_directory(ctx.attr.outdir)
+
+    args = ctx.actions.args()
+    args.add(ctx.file.browser_protocol)
+    args.add(ctx.file.js_protocol)
+    args.add(outdir.path)
+
+    ctx.actions.run(
+        executable = ctx.executable.generator,
+        progress_message = "Generating {} DevTools Protocol bindings for Python".format(ctx.attr.protocol_version),
+        arguments = [args],
+        outputs = [
+            outdir,
+        ],
+        inputs = [
+            ctx.file.browser_protocol,
+            ctx.file.js_protocol,
+        ],
+        use_default_shell_env = True,
+    )
+
+    return DefaultInfo(
+        files = depset([outdir]),
+        runfiles = ctx.runfiles(files = [outdir]),
+    )
+
+generate_devtools = rule(
+    implementation = _generate_devtools_impl,
+    attrs = {
+        "protocol_version": attr.string(
+            mandatory = True,
+            default = "",
+        ),
+        "browser_protocol": attr.label(
+            mandatory = True,
+            allow_single_file = True,
+        ),
+        "js_protocol": attr.label(
+            mandatory = True,
+            allow_single_file = True,
+        ),
+        "outdir": attr.string(),
+        "generator": attr.label(
+            executable = True,
+            cfg = "exec",
+        ),
+        "deps": attr.label_list(),
+    },
+)
diff --git a/rules_pkg_tree.patch b/rules_pkg_tree.patch
deleted file mode 100644
index 5ba6fd32..00000000
--- a/rules_pkg_tree.patch
+++ /dev/null
@@ -1,16 +0,0 @@
-diff --git a/pkg/private/tar/build_tar.py b/pkg/private/tar/build_tar.py
-index ab16610d..b6b75bd7 100644
---- a/pkg/private/tar/build_tar.py
-+++ b/pkg/private/tar/build_tar.py
-@@ -309,7 +309,10 @@ def add_manifest_entry(self, entry_list, file_attributes):
-     elif entry.entry_type == manifest.ENTRY_IS_EMPTY_FILE:
-       self.add_empty_file(entry.dest, **attrs)
-     else:
--      self.add_file(entry.src, entry.dest, **attrs)
-+      if os.path.isdir(entry.src):
-+        self.add_tree(entry.src, entry.dest, **attrs)
-+      else:
-+        self.add_file(entry.src, entry.dest, **attrs)
- 
- 
- def main():