Fall back to native rules if possible (#10)

Provides compatibility with Bazel versions before 7.4.0.

Also requires adding `bazel_skylib` to the dependency macro.

Fixes #9
diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 4ca63d2..d499c80 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -5,8 +5,7 @@
   platform:
     - ubuntu2004
     - macos
-    # TODO: Enable Windows once the private API it allowlists rules_shell.
-    # - windows
+    - windows
 
 tasks:
   test_module_bzlmod:
@@ -39,6 +38,15 @@
       - "--enable_workspace"
     test_targets:
       - "//..."
+  test_module_workspace_bazel6:
+    name: "Test module (Bazel 6.5.0, WORKSPACE)"
+    working_directory: "tests/bcr"
+    bazel: 6.5.0
+    platform: ${{ platform }}
+    build_targets:
+      - "//..."
+    test_targets:
+      - "//..."
   test_module_head:
     name: "Test module (Bazel@HEAD, Bzlmod)"
     working_directory: "tests/bcr"
diff --git a/shell/private/BUILD b/shell/private/BUILD
index 59632c1..d0eebed 100644
--- a/shell/private/BUILD
+++ b/shell/private/BUILD
@@ -2,6 +2,11 @@
 
 bzl_library(
     name = "private_bzl",
-    srcs = ["sh_executable.bzl"],
+    srcs = [
+        "sh_binary.bzl",
+        "sh_executable.bzl",
+        "sh_library.bzl",
+        "sh_test.bzl",
+    ],
     visibility = ["//shell:__pkg__"],
 )
diff --git a/shell/private/sh_binary.bzl b/shell/private/sh_binary.bzl
new file mode 100644
index 0000000..eec109a
--- /dev/null
+++ b/shell/private/sh_binary.bzl
@@ -0,0 +1,22 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""sh_binary rule definition."""
+
+load(":sh_executable.bzl", "make_sh_executable_rule")
+
+# For doc generation only.
+visibility("public")
+
+sh_binary = make_sh_executable_rule(executable = True)
diff --git a/shell/private/sh_library.bzl b/shell/private/sh_library.bzl
new file mode 100644
index 0000000..2f3613c
--- /dev/null
+++ b/shell/private/sh_library.bzl
@@ -0,0 +1,118 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""sh_library rule definition."""
+
+# For doc generation only.
+visibility("public")
+
+def _sh_library_impl(ctx):
+    transitive_files = []
+    for target in ctx.attr.srcs:
+        transitive_files.append(target[DefaultInfo].files)
+    for target in ctx.attr.deps:
+        transitive_files.append(target[DefaultInfo].files)
+    for target in ctx.attr.data:
+        transitive_files.append(target[DefaultInfo].files)
+    files = depset(transitive = transitive_files)
+
+    runfiles = ctx.runfiles(transitive_files = files, collect_default = True)
+
+    instrumented_files_info = coverage_common.instrumented_files_info(
+        ctx,
+        source_attributes = ["srcs"],
+        dependency_attributes = ["deps", "data"],
+    )
+
+    return [
+        DefaultInfo(
+            files = files,
+            runfiles = runfiles,
+        ),
+        instrumented_files_info,
+    ]
+
+sh_library = rule(
+    _sh_library_impl,
+    doc = """
+<p>
+  The main use for this rule is to aggregate together a logical
+  "library" consisting of related scripts&mdash;programs in an
+  interpreted language that does not require compilation or linking,
+  such as the Bourne shell&mdash;and any data those programs need at
+  run-time. Such "libraries" can then be used from
+  the <code>data</code> attribute of one or
+  more <code>sh_binary</code> rules.
+</p>
+
+<p>
+  You can use the <a href="${link filegroup}"><code>filegroup</code></a> rule to aggregate data
+  files.
+</p>
+
+<p>
+  In interpreted programming languages, there's not always a clear
+  distinction between "code" and "data": after all, the program is
+  just "data" from the interpreter's point of view. For this reason
+  this rule has three attributes which are all essentially equivalent:
+  <code>srcs</code>, <code>deps</code> and <code>data</code>.
+  The current implementation does not distinguish between the elements of these lists.
+  All three attributes accept rules, source files and generated files.
+  It is however good practice to use the attributes for their usual purpose (as with other rules).
+</p>
+
+<h4 id="sh_library_examples">Examples</h4>
+
+<pre class="code">
+sh_library(
+    name = "foo",
+    data = [
+        ":foo_service_script",  # an sh_binary with srcs
+        ":deploy_foo",  # another sh_binary with srcs
+    ],
+)
+</pre>
+""",
+    attrs = {
+        "srcs": attr.label_list(
+            allow_files = True,
+            doc = """
+The list of input files.
+<p>
+  This attribute should be used to list shell script source files that belong to
+  this library. Scripts can load other scripts using the shell's <code>source</code>
+  or <code>.</code> command.
+</p>
+""",
+        ),
+        "data": attr.label_list(
+            allow_files = True,
+            flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
+        ),
+        "deps": attr.label_list(
+            allow_rules = ["sh_library"],
+            doc = """
+The list of "library" targets to be aggregated into this target.
+See general comments about <code>deps</code>
+at <a href="${link common-definitions#typical.deps}">Typical attributes defined by
+most build rules</a>.
+<p>
+  This attribute should be used to list other <code>sh_library</code> rules that provide
+  interpreted program source code depended on by the code in <code>srcs</code>. The files
+  provided by these rules will be present among the <code>runfiles</code> of this target.
+</p>
+""",
+        ),
+    },
+)
diff --git a/shell/private/sh_test.bzl b/shell/private/sh_test.bzl
new file mode 100644
index 0000000..c12622a
--- /dev/null
+++ b/shell/private/sh_test.bzl
@@ -0,0 +1,39 @@
+# Copyright 2024 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""sh_test rule definition."""
+
+load(":sh_executable.bzl", "make_sh_executable_rule")
+
+# For doc generation only.
+visibility("public")
+
+sh_test = make_sh_executable_rule(
+    test = True,
+    fragments = ["coverage"],
+    extra_attrs = {
+        "_lcov_merger": attr.label(
+            cfg = "exec",
+            default = configuration_field(fragment = "coverage", name = "output_generator"),
+            executable = True,
+        ),
+        # Add the script as an attribute in order for sh_test to output code coverage results for
+        # code covered by CC binaries invocations.
+        "_collect_cc_coverage": attr.label(
+            cfg = "exec",
+            default = "@bazel_tools//tools/test:collect_cc_coverage",
+            executable = True,
+        ),
+    },
+)
diff --git a/shell/repositories.bzl b/shell/repositories.bzl
index 81b1cc0..62a0ff1 100644
--- a/shell/repositories.bzl
+++ b/shell/repositories.bzl
@@ -14,6 +14,8 @@
 
 """WORKSPACE dependency macros for the shell rules."""
 
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
 load("//shell/private/repositories:sh_config.bzl", "sh_config")
 
 visibility("public")
@@ -21,7 +23,15 @@
 # buildifier: disable=unnamed-macro
 def rules_shell_dependencies():
     """Instantiates repositories required by rules_shell."""
-    pass
+    maybe(
+        http_archive,
+        name = "bazel_skylib",
+        sha256 = "bc283cdfcd526a52c3201279cda4bc298652efa898b10b4db0837dc51652756f",
+        urls = [
+            "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz",
+            "https://github.com/bazelbuild/bazel-skylib/releases/download/1.7.1/bazel-skylib-1.7.1.tar.gz",
+        ],
+    )
 
 # buildifier: disable=unnamed-macro
 def rules_shell_toolchains():
diff --git a/shell/sh_binary.bzl b/shell/sh_binary.bzl
index 5e3a422..9e4a30d 100644
--- a/shell/sh_binary.bzl
+++ b/shell/sh_binary.bzl
@@ -14,8 +14,8 @@
 
 """sh_binary rule definition."""
 
-load("//shell/private:sh_executable.bzl", "make_sh_executable_rule")
+load("//shell/private:sh_binary.bzl", _sh_binary = "sh_binary")
 
 visibility("public")
 
-sh_binary = make_sh_executable_rule(executable = True)
+sh_binary = getattr(native, "sh_binary", _sh_binary)
diff --git a/shell/sh_library.bzl b/shell/sh_library.bzl
index fd6aef9..b51aeaa 100644
--- a/shell/sh_library.bzl
+++ b/shell/sh_library.bzl
@@ -14,104 +14,8 @@
 
 """sh_library rule definition."""
 
+load("//shell/private:sh_library.bzl", _sh_library = "sh_library")
+
 visibility("public")
 
-def _sh_library_impl(ctx):
-    transitive_files = []
-    for target in ctx.attr.srcs:
-        transitive_files.append(target[DefaultInfo].files)
-    for target in ctx.attr.deps:
-        transitive_files.append(target[DefaultInfo].files)
-    for target in ctx.attr.data:
-        transitive_files.append(target[DefaultInfo].files)
-    files = depset(transitive = transitive_files)
-
-    runfiles = ctx.runfiles(transitive_files = files, collect_default = True)
-
-    instrumented_files_info = coverage_common.instrumented_files_info(
-        ctx,
-        source_attributes = ["srcs"],
-        dependency_attributes = ["deps", "data"],
-    )
-
-    return [
-        DefaultInfo(
-            files = files,
-            runfiles = runfiles,
-        ),
-        instrumented_files_info,
-    ]
-
-sh_library = rule(
-    _sh_library_impl,
-    doc = """
-<p>
-  The main use for this rule is to aggregate together a logical
-  "library" consisting of related scripts&mdash;programs in an
-  interpreted language that does not require compilation or linking,
-  such as the Bourne shell&mdash;and any data those programs need at
-  run-time. Such "libraries" can then be used from
-  the <code>data</code> attribute of one or
-  more <code>sh_binary</code> rules.
-</p>
-
-<p>
-  You can use the <a href="${link filegroup}"><code>filegroup</code></a> rule to aggregate data
-  files.
-</p>
-
-<p>
-  In interpreted programming languages, there's not always a clear
-  distinction between "code" and "data": after all, the program is
-  just "data" from the interpreter's point of view. For this reason
-  this rule has three attributes which are all essentially equivalent:
-  <code>srcs</code>, <code>deps</code> and <code>data</code>.
-  The current implementation does not distinguish between the elements of these lists.
-  All three attributes accept rules, source files and generated files.
-  It is however good practice to use the attributes for their usual purpose (as with other rules).
-</p>
-
-<h4 id="sh_library_examples">Examples</h4>
-
-<pre class="code">
-sh_library(
-    name = "foo",
-    data = [
-        ":foo_service_script",  # an sh_binary with srcs
-        ":deploy_foo",  # another sh_binary with srcs
-    ],
-)
-</pre>
-""",
-    attrs = {
-        "srcs": attr.label_list(
-            allow_files = True,
-            doc = """
-The list of input files.
-<p>
-  This attribute should be used to list shell script source files that belong to
-  this library. Scripts can load other scripts using the shell's <code>source</code>
-  or <code>.</code> command.
-</p>
-""",
-        ),
-        "data": attr.label_list(
-            allow_files = True,
-            flags = ["SKIP_CONSTRAINTS_OVERRIDE"],
-        ),
-        "deps": attr.label_list(
-            allow_rules = ["sh_library"],
-            doc = """
-The list of "library" targets to be aggregated into this target.
-See general comments about <code>deps</code>
-at <a href="${link common-definitions#typical.deps}">Typical attributes defined by
-most build rules</a>.
-<p>
-  This attribute should be used to list other <code>sh_library</code> rules that provide
-  interpreted program source code depended on by the code in <code>srcs</code>. The files
-  provided by these rules will be present among the <code>runfiles</code> of this target.
-</p>
-""",
-        ),
-    },
-)
+sh_library = getattr(native, "sh_library", _sh_library)
diff --git a/shell/sh_test.bzl b/shell/sh_test.bzl
index ea5b5a9..272d193 100644
--- a/shell/sh_test.bzl
+++ b/shell/sh_test.bzl
@@ -14,25 +14,8 @@
 
 """sh_test rule definition."""
 
-load("//shell/private:sh_executable.bzl", "make_sh_executable_rule")
+load("//shell/private:sh_test.bzl", _sh_test = "sh_test")
 
 visibility("public")
 
-sh_test = make_sh_executable_rule(
-    test = True,
-    fragments = ["coverage"],
-    extra_attrs = {
-        "_lcov_merger": attr.label(
-            cfg = "exec",
-            default = configuration_field(fragment = "coverage", name = "output_generator"),
-            executable = True,
-        ),
-        # Add the script as an attribute in order for sh_test to output code coverage results for
-        # code covered by CC binaries invocations.
-        "_collect_cc_coverage": attr.label(
-            cfg = "exec",
-            default = "@bazel_tools//tools/test:collect_cc_coverage",
-            executable = True,
-        ),
-    },
-)
+sh_test = getattr(native, "sh_test", _sh_test)