Add windows support (#45)

Limitations:
- requires these two flags on windows (these have no effect or are
already default on other platforms)
  - `startup --windows_enable_symlinks`
  - `common --enable_runfiles`
diff --git a/.bazelrc b/.bazelrc
index 404de54..003790d 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -4,3 +4,12 @@
 
 test --test_output=errors
 test --test_summary=terse
+
+# best practice to disable
+common --nolegacy_external_runfiles
+
+# off by default on windows, required for this module
+startup --windows_enable_symlinks
+
+# off by default on windows, required for this module
+common --enable_runfiles
diff --git a/.bazelversion b/.bazelversion
index 66ce77b..a3fcc71 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-7.0.0
+7.1.0
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0d6e430..0a5152c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,8 +25,6 @@
           {"bazelversion": "5.4.0"},
           {"bazelversion": "6.4.0"},
         ]
-      # this ruleset only supports linux and macos
-      exclude_windows: true
   integration-tests:
     runs-on: ubuntu-latest
     steps:
diff --git a/examples/module/.bazelrc b/examples/module/.bazelrc
index 694f59b..fe93ecc 100644
--- a/examples/module/.bazelrc
+++ b/examples/module/.bazelrc
@@ -1,3 +1,12 @@
 common --enable_bzlmod
 
 common --lockfile_mode=off
+
+# best practice to disable
+common --nolegacy_external_runfiles
+
+# off by default on windows, required for this module
+startup --windows_enable_symlinks
+
+# off by default on windows, required for this module
+common --enable_runfiles
diff --git a/examples/module/.bazelversion b/examples/module/.bazelversion
index a8907c0..a3fcc71 100644
--- a/examples/module/.bazelversion
+++ b/examples/module/.bazelversion
@@ -1 +1 @@
-7.0.2
+7.1.0
diff --git a/examples/module/multitool.lock.json b/examples/module/multitool.lock.json
index bee250f..b3d578b 100644
--- a/examples/module/multitool.lock.json
+++ b/examples/module/multitool.lock.json
@@ -22,6 +22,13 @@
               "sha256": "c8a09143e9fe6eccc4b27a6be92c5929e5a78034a8d0b4c43dbed4ee539ec903",
               "os": "linux",
               "cpu": "x86_64"
+          },
+          {
+            "kind": "file",
+            "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.windows.amd64.exe",
+            "sha256": "e14fd75e33d193f579505cf3e641e07025904fc027686e13e154ba8e10ac0f58",
+            "os": "windows",
+            "cpu": "x86_64"
           }
       ]
   },
@@ -47,9 +54,17 @@
             "kind": "archive",
             "url": "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_linux_amd64.tar.gz",
             "sha256": "f11eefb646768e3f53e2185f6d3b01b4cb02112c2c60e65a4b5875150287ff97",
-            "file": "gh_2.44.1_linux_amd64/bin",
+            "file": "gh_2.44.1_linux_amd64/bin/gh",
             "os": "linux",
             "cpu": "x86_64"
+        },
+        {
+            "kind": "archive",
+            "url": "https://github.com/cli/cli/releases/download/v2.44.1/gh_2.44.1_windows_amd64.zip",
+            "sha256": "5a19a334d5032cfcb01e71e418c5e355104f7afd9129daf8939b729ffa46aa08",
+            "file": "gh_2.44.1_windows_amd64/bin/gh.exe",
+            "os": "windows",
+            "cpu": "x86_64"
         }
     ]
   },
@@ -79,7 +94,6 @@
             "os": "linux",
             "cpu": "x86_64"
         }
-
     ]
   }
 }
diff --git a/examples/workspace/.bazelrc b/examples/workspace/.bazelrc
index 694f59b..fe93ecc 100644
--- a/examples/workspace/.bazelrc
+++ b/examples/workspace/.bazelrc
@@ -1,3 +1,12 @@
 common --enable_bzlmod
 
 common --lockfile_mode=off
+
+# best practice to disable
+common --nolegacy_external_runfiles
+
+# off by default on windows, required for this module
+startup --windows_enable_symlinks
+
+# off by default on windows, required for this module
+common --enable_runfiles
diff --git a/examples/workspace/.bazelversion b/examples/workspace/.bazelversion
index a8907c0..a3fcc71 100644
--- a/examples/workspace/.bazelversion
+++ b/examples/workspace/.bazelversion
@@ -1 +1 @@
-7.0.2
+7.1.0
diff --git a/examples/workspace/multitool.lock.json b/examples/workspace/multitool.lock.json
index d640c22..a7fee4f 100644
--- a/examples/workspace/multitool.lock.json
+++ b/examples/workspace/multitool.lock.json
@@ -22,6 +22,13 @@
               "sha256": "c8a09143e9fe6eccc4b27a6be92c5929e5a78034a8d0b4c43dbed4ee539ec903",
               "os": "linux",
               "cpu": "x86_64"
+          },
+          {
+            "kind": "file",
+            "url": "https://github.com/bazel-contrib/target-determinator/releases/download/v0.25.0/target-determinator.windows.amd64.exe",
+            "sha256": "e14fd75e33d193f579505cf3e641e07025904fc027686e13e154ba8e10ac0f58",
+            "os": "windows",
+            "cpu": "x86_64"
           }
       ]
   }
diff --git a/lockfile.schema.json b/lockfile.schema.json
index 04956a3..b08f62c 100644
--- a/lockfile.schema.json
+++ b/lockfile.schema.json
@@ -4,7 +4,7 @@
   "type": "object",
   "definitions": {
     "supported_os": {
-      "enum": ["macos", "linux"]
+      "enum": ["macos", "linux", "windows"]
     },
     "supported_cpu": {
       "enum": ["x86_64", "arm64"]
diff --git a/multitool/private/BUILD.bazel b/multitool/private/BUILD.bazel
index b4859c1..0df68e1 100644
--- a/multitool/private/BUILD.bazel
+++ b/multitool/private/BUILD.bazel
@@ -1,6 +1,7 @@
 load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
 
 exports_files([
+    "cwd.template.bat",
     "cwd.template.sh",
 ])
 
diff --git a/multitool/private/cwd.bzl b/multitool/private/cwd.bzl
index 97cc681..3703836 100644
--- a/multitool/private/cwd.bzl
+++ b/multitool/private/cwd.bzl
@@ -1,12 +1,20 @@
 "cwd: a rule for executing an executable in the BUILD_WORKING_DIRECTORY"
 
 def _cwd_impl(ctx):
-    output = ctx.actions.declare_file(ctx.label.name)
+    # This algorithm requires --enable_runfiles (enabled by default on non-windows)
+    template = ctx.file._template_sh
+    wrapper_name = ctx.label.name
+    tool_short_path = ctx.file.tool.short_path
+    if ctx.file.tool.extension == "exe":
+        template = ctx.file._template_bat
+        wrapper_name = wrapper_name + ".bat"
+        tool_short_path = tool_short_path.replace("/", "\\")
+    output = ctx.actions.declare_file(wrapper_name)
     ctx.actions.expand_template(
-        template = ctx.file._template,
+        template = template,
         output = output,
         substitutions = {
-            "{{tool}}": ctx.file.tool.short_path,
+            "{{tool}}": tool_short_path,
         },
     )
     return [DefaultInfo(executable = output, runfiles = ctx.runfiles(files = [ctx.file.tool]))]
@@ -15,7 +23,8 @@
     implementation = _cwd_impl,
     attrs = {
         "tool": attr.label(mandatory = True, allow_single_file = True, executable = True, cfg = "exec"),
-        "_template": attr.label(default = "//multitool/private:cwd.template.sh", allow_single_file = True),
+        "_template_sh": attr.label(default = "//multitool/private:cwd.template.sh", allow_single_file = True),
+        "_template_bat": attr.label(default = "//multitool/private:cwd.template.bat", allow_single_file = True),
     },
     executable = True,
 )
diff --git a/multitool/private/cwd.template.bat b/multitool/private/cwd.template.bat
new file mode 100644
index 0000000..fcaf9d4
--- /dev/null
+++ b/multitool/private/cwd.template.bat
@@ -0,0 +1,3 @@
+@set tool=%cd%\{{tool}}
+@cd %BUILD_WORKING_DIRECTORY%
+@%tool% %*
diff --git a/multitool/private/env_repo_tool_template/BUILD.bazel.template b/multitool/private/env_repo_tool_template/BUILD.bazel.template
index d2f7a23..4ac2013 100644
--- a/multitool/private/env_repo_tool_template/BUILD.bazel.template
+++ b/multitool/private/env_repo_tool_template/BUILD.bazel.template
@@ -1,3 +1,3 @@
 # generated by multitool
 
-exports_files(glob(include=["*_executable"]))
+exports_files(glob(include=["*_executable", "*_executable.exe"]))
diff --git a/multitool/private/hub_repo_template/toolchain_info.bzl.template b/multitool/private/hub_repo_template/toolchain_info.bzl.template
index 836f850..90fa4b7 100644
--- a/multitool/private/hub_repo_template/toolchain_info.bzl.template
+++ b/multitool/private/hub_repo_template/toolchain_info.bzl.template
@@ -6,6 +6,7 @@
             executable = ctx.file.executable,
             cpu = ctx.attr.cpu,
             os = ctx.attr.os,
+            ext = ctx.attr.ext,
         ),
     ]
 
@@ -13,17 +14,25 @@
     attrs = dict(
         executable = attr.label(allow_single_file = True),
         cpu = attr.string(mandatory = True, values = ["arm64", "x86_64"]),
-        os = attr.string(mandatory = True, values = ["linux", "macos"]),
+        os = attr.string(mandatory = True, values = ["linux", "macos", "windows"]),
+        ext = attr.string(mandatory = True, values = ["", ".exe"]),
     ),
     implementation = _toolchain_info_impl,
 )
 
+def _extension(os):
+    if os == "windows":
+        return ".exe"
+    return ""
+
 def declare_toolchain(name, os, cpu, toolchain_type):
+    ext = _extension(os)
     toolchain_info(
         name = "{name}_{os}_{cpu}_toolchain_info".format(name=name, os=os, cpu=cpu),
-        executable = "@@{hub_name}.{os}_{cpu}//tools/{name}:{os}_{cpu}_executable".format(name=name, os=os, cpu=cpu),
+        executable = "@@{hub_name}.{os}_{cpu}//tools/{name}:{os}_{cpu}_executable{ext}".format(name=name, os=os, cpu=cpu, ext=ext),
         os = os,
         cpu = cpu,
+        ext = ext,
     )
 
     native.toolchain(
diff --git a/multitool/private/hub_repo_tool_template/tool.bzl.template b/multitool/private/hub_repo_tool_template/tool.bzl.template
index 7d9af1a..c55b71a 100644
--- a/multitool/private/hub_repo_tool_template/tool.bzl.template
+++ b/multitool/private/hub_repo_tool_template/tool.bzl.template
@@ -6,7 +6,7 @@
 
 def _tool_impl(ctx):
     toolchain = ctx.toolchains[_TOOLCHAIN_TYPE]
-    output = ctx.actions.declare_file(ctx.label.name)
+    output = ctx.actions.declare_file(ctx.label.name + toolchain.ext)
     ctx.actions.symlink(output = output, target_file = toolchain.executable)
     return [DefaultInfo(executable = output)]
 
diff --git a/multitool/private/multitool.bzl b/multitool/private/multitool.bzl
index 0242444..02648d3 100644
--- a/multitool/private/multitool.bzl
+++ b/multitool/private/multitool.bzl
@@ -51,6 +51,8 @@
     ("linux", "x86_64"),
     ("macos", "arm64"),
     ("macos", "x86_64"),
+    ("windows", "arm64"),
+    ("windows", "x86_64"),
 ]
 
 def _check(condition, message):
@@ -58,6 +60,15 @@
     if not condition:
         fail(message)
 
+def _check_version(os):
+    # skip version check on windows if we don't have a release version. We can't tell from a hash what features we have.
+    if os == "windows" and native.bazel_version:
+        version = native.bazel_version.split(".")
+        if int(version[0]) > 7 or (int(version[0]) == 7 and int(version[1]) >= 1):
+            pass
+        else:
+            fail("rules_multitool: windows artifacts require bazel 7.1+; current bazel is " + native.bazel_version)
+
 def _load_tools(rctx):
     tools = {}
     for lockfile in rctx.attr.lockfiles:
@@ -74,7 +85,7 @@
     for tool_name, tool in tools.items():
         for binary in tool["binaries"]:
             _check(
-                binary["os"] in ["linux", "macos"],
+                binary["os"] in ["linux", "macos", "windows"],
                 "{tool_name}: Unknown os '{os}'".format(
                     tool_name = tool_name,
                     os = binary["os"],
@@ -87,6 +98,7 @@
                     cpu = binary["cpu"],
                 ),
             )
+            _check_version(binary["os"])
 
     return tools
 
@@ -97,6 +109,11 @@
 
     return args
 
+def _extension(os):
+    if os == "windows":
+        return ".exe"
+    return ""
+
 def _env_specific_tools_impl(rctx):
     tools = _load_tools(rctx)
 
@@ -105,10 +122,11 @@
             if binary["os"] != rctx.attr.os or binary["cpu"] != rctx.attr.cpu:
                 continue
 
-            target_executable = "tools/{tool_name}/{os}_{cpu}_executable".format(
+            target_executable = "tools/{tool_name}/{os}_{cpu}_executable{ext}".format(
                 tool_name = tool_name,
                 cpu = binary["cpu"],
                 os = binary["os"],
+                ext = _extension(binary["os"]),
             )
 
             if binary["kind"] == "file":
diff --git a/readme.md b/readme.md
index 64a7f67..d13c8f8 100644
--- a/readme.md
+++ b/readme.md
@@ -17,7 +17,7 @@
         "kind": "file",
         "url": "https://...",
         "sha256": "sha256 of the file",
-        "os": "linux|macos",
+        "os": "linux|macos|windows",
         "cpu": "x86_64|arm64"
       }
     ]