Improve schema, support 'type', prep for 'headers' (breaking for WORKSPACE) (#23)

Improve the schema to specify a union over the binary kinds with more pedantic definitions. Add keyword args for `type` on archive binary kind and `headers` on all binary kinds when Bazel version is >= 7.1.0.

Note: this includes a breaking change for WORKSPACE users and requires loading bazel_features ahead of rules_multitool. Richer instructions for WORKSPACE usage will now be available in the release notes.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 79dbb5f..6956046 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@
         [
           ".",
           "examples/module",
-          "examples/target-determinator"
+          "examples/workspace"
         ]
       # we only support Bazel 7, and only with bzlmod enabled
       exclude: |
@@ -31,5 +31,5 @@
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
-      - working-directory: examples/target-determinator
+      - working-directory: examples/workspace
         run: ./minimal_download_test.sh
diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh
index bc2839c..133f9e5 100755
--- a/.github/workflows/release_prep.sh
+++ b/.github/workflows/release_prep.sh
@@ -18,12 +18,49 @@
 SHA=$(shasum -a 256 $ARCHIVE | awk '{print $1}')
 
 cat << EOF
-## Using Bzlmod with Bazel 6
+## Using Bzlmod (preferred)
 
-1. Enable with \`common --enable_bzlmod\` in \`.bazelrc\`.
+1. Create a multitool.lock.json ([schema](https://github.com/theoremlp/rules_multitool/blob/main/lockfile.schema.json))
 2. Add to your \`MODULE.bazel\` file:
 
 \`\`\`starlark
 bazel_dep(name = "rules_multitool", version = "${TAG:1}")
+
+multitool = use_extension("@rules_multitool//multitool:extension.bzl", "multitool")
+multitool.hub(lockfile = "//:multitool.lock.json")
+use_repo(multitool, "multitool")
+\`\`\`
+
+## Using WORKSPACE
+1. Create a multitool.lock.json ([schema](https://github.com/theoremlp/rules_multitool/blob/main/lockfile.schema.json))
+2. Add to your \`WORKSPACE\` file:
+
+\`\`\`starlark
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
+    name = "bazel_features",
+    sha256 = "06f02b97b6badb3227df2141a4b4622272cdcd2951526f40a888ab5f43897f14",
+    strip_prefix = "bazel_features-1.9.0",
+    url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.9.0/bazel_features-v1.9.0.tar.gz",
+)
+
+http_archive(
+    name = "rules_multitool",
+    sha256 = "${SHA}",
+    strip_prefix = "rules_multitool-${TAG:1}",
+    url = "https://github.com/bazel-contrib/bazel_features/releases/download/v${TAG:1}/bazel_features-${TAG:1}.tar.gz",
+)
+
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+
+bazel_features_deps()
+
+load("@rules_multitool//multitool:multitool.bzl", "multitool")
+
+multitool(
+    name = "multitool",
+    lockfile = "//:multitool.lock.json",
+)
 \`\`\`
 EOF
diff --git a/MODULE.bazel b/MODULE.bazel
index 46427b6..3fb6c5e 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -6,7 +6,8 @@
     compatibility_level = 1,
 )
 
-bazel_dep(name = "bazel_skylib", version = "1.4.1")
+bazel_dep(name = "bazel_features", version = "1.9.0")
+bazel_dep(name = "bazel_skylib", version = "1.5.0")
 bazel_dep(name = "buildifier_prebuilt", version = "6.1.2")
 bazel_dep(name = "platforms", version = "0.0.8")
 
diff --git a/examples/module/.bazelversion b/examples/module/.bazelversion
index 66ce77b..a8907c0 100644
--- a/examples/module/.bazelversion
+++ b/examples/module/.bazelversion
@@ -1 +1 @@
-7.0.0
+7.0.2
diff --git a/examples/module/MODULE.bazel b/examples/module/MODULE.bazel
index 6625e33..3b2029a 100644
--- a/examples/module/MODULE.bazel
+++ b/examples/module/MODULE.bazel
@@ -1,7 +1,7 @@
 "multitool example using target-determinator"
 
 module(
-    name = "multitool_examples__target_determinator",
+    name = "multitool_examples__module",
     version = "0.0.0",
     compatibility_level = 1,
 )
diff --git a/examples/target-determinator/.bazelrc b/examples/workspace/.bazelrc
similarity index 100%
rename from examples/target-determinator/.bazelrc
rename to examples/workspace/.bazelrc
diff --git a/examples/workspace/.bazelversion b/examples/workspace/.bazelversion
new file mode 100644
index 0000000..a8907c0
--- /dev/null
+++ b/examples/workspace/.bazelversion
@@ -0,0 +1 @@
+7.0.2
diff --git a/examples/target-determinator/BUILD.bazel b/examples/workspace/BUILD.bazel
similarity index 100%
rename from examples/target-determinator/BUILD.bazel
rename to examples/workspace/BUILD.bazel
diff --git a/examples/target-determinator/MODULE.bazel b/examples/workspace/MODULE.bazel
similarity index 71%
rename from examples/target-determinator/MODULE.bazel
rename to examples/workspace/MODULE.bazel
index 4886c26..ace0f68 100644
--- a/examples/target-determinator/MODULE.bazel
+++ b/examples/workspace/MODULE.bazel
@@ -1,11 +1,12 @@
 "multitool example using target-determinator"
 
 module(
-    name = "multitool_examples__target_determinator",
+    name = "multitool_examples__workspace",
     version = "0.0.0",
     compatibility_level = 1,
 )
 
+bazel_dep(name = "bazel_features", version = "1.9.0")
 bazel_dep(name = "rules_multitool", version = "0.0.0")
 local_path_override(
     module_name = "rules_multitool",
diff --git a/examples/target-determinator/WORKSPACE.bazel b/examples/workspace/WORKSPACE.bazel
similarity index 63%
rename from examples/target-determinator/WORKSPACE.bazel
rename to examples/workspace/WORKSPACE.bazel
index 6146eae..99dc0cb 100644
--- a/examples/target-determinator/WORKSPACE.bazel
+++ b/examples/workspace/WORKSPACE.bazel
@@ -1,3 +1,7 @@
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+
+bazel_features_deps()
+
 load("@rules_multitool//multitool:multitool.bzl", "multitool")
 
 multitool(
diff --git a/examples/target-determinator/integration_test.sh b/examples/workspace/integration_test.sh
similarity index 100%
rename from examples/target-determinator/integration_test.sh
rename to examples/workspace/integration_test.sh
diff --git a/examples/target-determinator/minimal_download_test.sh b/examples/workspace/minimal_download_test.sh
similarity index 100%
rename from examples/target-determinator/minimal_download_test.sh
rename to examples/workspace/minimal_download_test.sh
diff --git a/examples/target-determinator/multitool.lock.json b/examples/workspace/multitool.lock.json
similarity index 100%
rename from examples/target-determinator/multitool.lock.json
rename to examples/workspace/multitool.lock.json
diff --git a/lockfile.schema.json b/lockfile.schema.json
index d76d038..04956a3 100644
--- a/lockfile.schema.json
+++ b/lockfile.schema.json
@@ -1,41 +1,112 @@
 {
-    "$id": "https://github.com/theoremlp/rules_multitool/lockfile.schema.json",
-    "title": "Describes binaries and instructions for fetching them under Bazel",
+  "$id": "https://github.com/theoremlp/rules_multitool/lockfile.schema.json",
+  "title": "Describes binaries and instructions for fetching them under Bazel",
+  "type": "object",
+  "definitions": {
+    "supported_os": {
+      "enum": ["macos", "linux"]
+    },
+    "supported_cpu": {
+      "enum": ["x86_64", "arm64"]
+    },
+    "file_type": {
+      "type": "object",
+      "properties": {
+        "kind": { "enum": ["file"] },
+        "url": { "type": "string" },
+        "sha256": { "type": "string" },
+        "os": { "$ref": "#/definitions/supported_os" },
+        "cpu": { "$ref": "#/definitions/supported_cpu" },
+        "headers": {
+          "type": "object",
+          "docs": "headers to pass to the downloader (supported on Bazel >= 7.1.0)",
+          "additionalProperties": {
+            "type": "string"
+          }
+        }
+      },
+      "required": ["kind", "url", "os", "cpu"]
+    },
+    "archive_type": {
+      "type": "object",
+      "properties": {
+        "kind": { "enum": ["archive"] },
+        "url": { "type": "string" },
+        "sha256": { "type": "string" },
+        "file": {
+          "type": "string",
+          "docs": "archive root relative path to binary"
+        },
+        "os": { "$ref": "#/definitions/supported_os" },
+        "cpu": { "$ref": "#/definitions/supported_cpu" },
+        "headers": {
+          "type": "object",
+          "docs": "headers to pass to the downloader (supported on Bazel >= 7.1.0)",
+          "additionalProperties": {
+            "type": "string"
+          }
+        },
+        "type": {
+          "enum": [
+            "zip",
+            "jar",
+            "war",
+            "aar",
+            "tar",
+            "tar.gz",
+            "tgz",
+            "tar.xz",
+            "txz",
+            ".tar.zst",
+            ".tzst",
+            "tar.bz2",
+            ".tbz",
+            ".ar",
+            ".deb"
+          ]
+        }
+      },
+      "required": ["kind", "url", "os", "cpu", "file"]
+    },
+    "pkg_type": {
+      "type": "object",
+      "properties": {
+        "kind": { "enum": ["pkg"] },
+        "url": { "type": "string" },
+        "sha256": { "type": "string" },
+        "file": {
+          "type": "string",
+          "docs": "pkg archive root relative path to binary"
+        },
+        "os": { "$ref": "#/definitions/supported_os" },
+        "cpu": { "$ref": "#/definitions/supported_cpu" },
+        "headers": {
+          "type": "object",
+          "docs": "headers to pass to the downloader (supported on Bazel >= 7.1.0)",
+          "additionalProperties": {
+            "type": "string"
+          }
+        }
+      },
+      "required": ["kind", "url", "os", "cpu", "file"]
+    }
+  },
+  "properties": {
+    "$schema": { "type": "string" }
+  },
+  "additionalProperties": {
     "type": "object",
     "properties": {
-      "$schema": { "type": "string" }
-    },
-    "additionalProperties": {
-        "type": "object",
-        "properties": {
-            "binaries": {
-                "type": "array",
-                "items": {
-                    "type": "object",
-                    "properties": {
-                        "kind": {
-                            "enum": ["file", "archive", "pkg"]
-                        },
-                        "url": {
-                            "type": "string"
-                        },
-                        "file": {
-                            "type": "string",
-                            "description": "when kind is archive or pkg, relative path to the binary within it"
-                        },
-                        "sha256": {
-                            "type": "string"
-                        },
-                        "os": {
-                            "enum": ["macos", "linux"]
-                        },
-                        "cpu": {
-                            "enum": ["x86_64", "arm64"]
-                        }
-                    },
-                    "additionalProperties": false
-                }   
-            }
+      "binaries": {
+        "type": "array",
+        "items": {
+          "anyOf": [
+            { "$ref": "#/definitions/file_type" },
+            { "$ref": "#/definitions/archive_type" },
+            { "$ref": "#/definitions/pkg_type" }
+          ]
         }
+      }
     }
+  }
 }
diff --git a/multitool/extension.bzl b/multitool/extension.bzl
index 3ef40ee..3d980fd 100644
--- a/multitool/extension.bzl
+++ b/multitool/extension.bzl
@@ -1,4 +1,4 @@
-"multitool"
+"multitool module extension"
 
 load("//multitool/private:multitool.bzl", _hub = "hub")
 
diff --git a/multitool/multitool.bzl b/multitool/multitool.bzl
index 5c29db4..e4c0315 100644
--- a/multitool/multitool.bzl
+++ b/multitool/multitool.bzl
@@ -1,5 +1,21 @@
-"multitool"
+"multitool workspace macros"
 
-load("//multitool/private:multitool.bzl", _multitool = "multitool")
+load("//multitool/private:multitool.bzl", "hub")
 
-multitool = _multitool
+def multitool(name, lockfile = None, lockfiles = None):
+    """(non-bzlmod) Create a multitool hub and register its toolchains.
+
+    Args:
+        name: resulting "hub" repo name to load tools from
+        lockfile: a label for a lockfile, see /lockfile.schema.json
+        lockfiles: a list of labels of multiple lockfiles
+
+    Note: exactly one of lockfile or lockfiles may be set.
+    """
+    if (not lockfile and not lockfiles) or (lockfile and lockfiles):
+        fail("Exactly one of lockfile and lockfiles must be set")
+
+    lockfiles = lockfiles if lockfiles else [lockfile]
+
+    hub(name, lockfiles)
+    native.register_toolchains("@{name}//toolchains:all".format(name = name))
diff --git a/multitool/private/multitool.bzl b/multitool/private/multitool.bzl
index e0fe76b..0242444 100644
--- a/multitool/private/multitool.bzl
+++ b/multitool/private/multitool.bzl
@@ -39,11 +39,11 @@
 This implementation depends on rendering a number of templates, which are defined in sibling
 folders and managed by the templates starlark file.
 
-To maintain support both bzlmod and non-bzlmod setups, we provide two entrypoints to the rule:
- - (bzlmod)     hub       : invoked by the hub tag in extension.bzl
- - (non-bzlmod) multitool : invoked in WORKSPACE or related macros, and additionally registers toolchains
+Note that we intend to support both bzlmod and non-bzlmod setups, so `hub` intentionally avoids
+a register_toolchains call.
 """
 
+load("@bazel_features//:features.bzl", "bazel_features")
 load(":templates.bzl", "templates")
 
 _SUPPORTED_ENVS = [
@@ -90,6 +90,13 @@
 
     return tools
 
+def _feature_sensitive_args(binary):
+    args = {}
+    if bazel_features.external_deps.download_has_headers_param:
+        args["headers"] = binary.get("headers", {})
+
+    return args
+
 def _env_specific_tools_impl(rctx):
     tools = _load_tools(rctx)
 
@@ -110,6 +117,7 @@
                     sha256 = binary["sha256"],
                     output = target_executable,
                     executable = True,
+                    **_feature_sensitive_args(binary)
                 )
             elif binary["kind"] == "archive":
                 archive_path = "tools/{tool_name}/{os}_{cpu}_archive".format(
@@ -122,6 +130,8 @@
                     url = binary["url"],
                     sha256 = binary["sha256"],
                     output = archive_path,
+                    type = binary.get("type", ""),
+                    **_feature_sensitive_args(binary)
                 )
 
                 # link to the executable
@@ -147,6 +157,7 @@
                     url = binary["url"],
                     sha256 = binary["sha256"],
                     output = archive_path + ".pkg",
+                    **_feature_sensitive_args(binary)
                 )
 
                 rctx.execute([pkgutil_cmd, "--expand-full", archive_path + ".pkg", archive_path])
@@ -231,21 +242,3 @@
             cpu = env[1],
         )
     _multitool_hub(name = name, lockfiles = lockfiles)
-
-def multitool(name, lockfile = None, lockfiles = None):
-    """(non-bzlmod) Create a multitool hub and register its toolchains.
-
-    Args:
-        name: resulting "hub" repo name to load tools from
-        lockfile: a label for a lockfile, see /lockfile.schema.json
-        lockfiles: a list of labels of multiple lockfiles
-    """
-    if lockfile and lockfiles:
-        fail("Only one of lockfile and lockfiles may be set")
-    if not lockfile and not lockfiles:
-        fail("Exactly one of lockfile and lockfiles must be set")
-    if lockfile:
-        hub(name, [lockfile])
-    else:
-        hub(name, lockfiles)
-    native.register_toolchains("@{name}//toolchains:all".format(name = name))
diff --git a/readme.md b/readme.md
index 4700779..f8cd389 100644
--- a/readme.md
+++ b/readme.md
@@ -4,9 +4,9 @@
 
 ## Usage
 
-For a quickstart, see the [bzlmod example](examples/module/).
+For a quickstart, see the [module example](examples/module/) or [workspace example](examples/workspace/).
 
-Define a lockfile that references the tools to load:
+Define a [lockfile](lockfile.schema.json) that references the tools to load:
 
 ```json
 {
@@ -43,7 +43,9 @@
 
 Save your lockfile and ensure the file is exported using `export_files` so that it's available to Bazel.
 
-Once your lockfile is defined, load the ruleset in your **MODULE.bazel** and create a hub that refers to your lockfile:
+### Bazel Module Usage
+
+Once your lockfile is defined, load the ruleset in your MODULE.bazel and create a hub that refers to your lockfile:
 
 ```python
 bazel_dep(name = "rules_multitool", version = "0.0.0")
@@ -54,3 +56,7 @@
 ```
 
 Tools may then be accessed using `@multitool//tools/tool-name`.
+
+### Workspace Usage
+
+Instructions for using with WORKSPACE may be found in [release notes](https://github.com/theoremlp/rules_multitool/releases).