Add Windows MSVC Bazel targets for native voice libraries (#43144)

## What changed

- Add explicit x64 and ARM64 targets for native builds, runtime preparation, and linking using the existing voice recipes. Each target requires native Windows execution of the matching architecture.
- Declare compiler, SDK, Python, and CMake inputs; require an explicitly supplied Cygwin/pkgconf tool tree and a fixed `SystemRoot`. Validate installed tool selections against the manifest and declared files.
- Pair Windows DLLs with SDK import libraries, preserve plugin and receipt runfiles, and omit Unix runtime-search flags. Use Python for portable payload copying.
- Correct the MSVC ARM64 tool directory casing to `HostArm64` and document provisioning and build commands.

The generic Rust-consumer aliases remain separate; native Windows Bazel execution and consumer validation are still needed to establish complete Windows voice support.

## Testing

Add eight unit tests covering tool selection, path anchoring, invalid inputs, and DLL/import-library copying, plus x64 and ARM64 link smoke targets that reference `gst_version`.

GitOrigin-RevId: fb2b4998e9ffb5b64ca830ebf4bb4633fc959eb9
This commit is contained in:
Benjamin Carlsson
2026-09-06 02:02:06 +00:00
committed by copyberry
parent 008bbd5884
commit 1c40ffe427
12 changed files with 724 additions and 18 deletions

View File

@@ -174,7 +174,7 @@ use_repo(voice_python, "python_3_12")
register_toolchains("@rules_foreign_cc//toolchains:built_make_toolchain")
foreign_cc_tools = use_extension("@rules_foreign_cc//foreign_cc:extensions.bzl", "tools")
use_repo(foreign_cc_tools, "pkgconfig_src")
use_repo(foreign_cc_tools, "cmake-3.31.8-windows-x86_64", "pkgconfig_src")
single_version_override(
module_name = "rules_rs",

4
MODULE.bazel.lock generated
View File

@@ -582,7 +582,7 @@
},
"@@windows_support+//windows:extensions.bzl%msvc_runtime": {
"general": {
"bzlTransitiveDigest": "IARx/OSheg9jZOotMp87h4ZSH1sYXy2d0otLXaCQdL0=",
"bzlTransitiveDigest": "hSBf9aCBJn12Lq4IKLqoUFuySmukaarmyjhYNGlHMX0=",
"usagesDigest": "XZ1DGFYwC06tBdgHxuZLhTfFlPFVKjmYmiBfiY2xe3c=",
"recordedInputs": [
"REPO_MAPPING:windows_support+,bazel_tools bazel_tools"
@@ -613,7 +613,7 @@
},
"@@windows_support+//windows:extensions.bzl%windows_sdk": {
"general": {
"bzlTransitiveDigest": "IARx/OSheg9jZOotMp87h4ZSH1sYXy2d0otLXaCQdL0=",
"bzlTransitiveDigest": "hSBf9aCBJn12Lq4IKLqoUFuySmukaarmyjhYNGlHMX0=",
"usagesDigest": "M5xde76Im8TfiBsVtKQIavNOkqfp1QsjwJD4fVrrtFs=",
"recordedInputs": [
"REPO_MAPPING:windows_support+,bazel_tools bazel_tools"

View File

@@ -1,7 +1,7 @@
diff --git a/windows/private/extensions/msvc_runtime.bzl b/windows/private/extensions/msvc_runtime.bzl
--- a/windows/private/extensions/msvc_runtime.bzl
+++ b/windows/private/extensions/msvc_runtime.bzl
@@ -204,13 +204,19 @@
@@ -204,13 +204,20 @@
_keep_only_children(
repository_ctx,
repository_ctx.path("{}/Contents/VC/Tools/MSVC/{}".format(sysroot_dir, msvc_version)),
@@ -16,9 +16,10 @@ diff --git a/windows/private/extensions/msvc_runtime.bzl b/windows/private/exten
+
+ # Keep native-host tools and their support files separate from runtime inputs.
+ bin_dir = "{}/Contents/VC/Tools/MSVC/{}/bin".format(sysroot_dir, msvc_version)
+ _keep_only_children(repository_ctx, repository_ctx.path(bin_dir), ["Host" + arch for arch in architectures])
+ _keep_only_children(repository_ctx, repository_ctx.path(bin_dir), ["HostArm64" if arch == "arm64" else "Host" + arch for arch in architectures])
+ for arch in architectures:
+ _keep_only_children(repository_ctx, repository_ctx.path(bin_dir + "/Host" + arch), [arch])
+ host = "HostArm64" if arch == "arm64" else "Host" + arch
+ _keep_only_children(repository_ctx, repository_ctx.path(bin_dir + "/" + host), [arch])
def _msvc_runtime_repository_impl(repository_ctx):
_check_msvc_license_requirements(repository_ctx)
@@ -69,7 +70,7 @@ diff --git a/windows/private/extensions/msvc_runtime.BUILD.bazel b/windows/priva
+directory(
+ name = "msvc_tools_arm64_files",
+ srcs = glob(
+ ["__MSVC_RUNTIME_DIR__/Contents/VC/Tools/MSVC/__MSVC_VERSION__/bin/Hostarm64/arm64/**"],
+ ["__MSVC_RUNTIME_DIR__/Contents/VC/Tools/MSVC/__MSVC_VERSION__/bin/HostArm64/arm64/**"],
+ allow_empty = True,
+ ),
+)
@@ -77,7 +78,7 @@ diff --git a/windows/private/extensions/msvc_runtime.BUILD.bazel b/windows/priva
+subdirectory(
+ name = "msvc_tools_arm64",
+ parent = ":msvc_tools_arm64_files",
+ path = "__MSVC_RUNTIME_DIR__/Contents/VC/Tools/MSVC/__MSVC_VERSION__/bin/Hostarm64/arm64",
+ path = "__MSVC_RUNTIME_DIR__/Contents/VC/Tools/MSVC/__MSVC_VERSION__/bin/HostArm64/arm64",
+)
diff --git a/windows/private/extensions/windows_sdk.bzl b/windows/private/extensions/windows_sdk.bzl
--- a/windows/private/extensions/windows_sdk.bzl

View File

@@ -1,8 +1,10 @@
load("@rules_cc//cc:cc_binary.bzl", "cc_binary")
load("@rules_foreign_cc//toolchains/native_tools:native_tools_toolchain.bzl", "native_tool_toolchain")
load(":native.bzl", "native_prefix")
load(":native_link.bzl", "native_link")
load(":pkg_config.bzl", "pkg_config")
load(":runtime.bzl", "native_runtime")
load(":windows_native.bzl", "windows_build_tools", "windows_native_prefix")
# All native build consumers must receive the same pinned source manifest.
exports_files(["opus-toolchain.cmake"])
@@ -243,3 +245,120 @@ alias(
tags = ["manual"],
visibility = ["//visibility:public"],
)
# Explicit opt-in inputs: no private downloader is loaded by the public module.
# CMake and Cygwin use x64 emulation on native ARM64; Python/MSVC/pkgconf do not.
filegroup(
name = "windows_tools_unavailable",
srcs = [],
)
label_flag(
name = "windows_installed_tools",
build_setting_default = ":windows_tools_unavailable",
visibility = ["//visibility:public"],
)
[
config_setting(
name = "windows_" + cpu + "_msvc",
constraint_values = [
"@platforms//os:windows",
"@platforms//cpu:" + cpu,
"@llvm//constraints/windows/abi:msvc",
],
)
for cpu in ("x86_64", "aarch64")
]
[
windows_build_tools(
name = "windows_tools_" + cpu,
includes = ["@msvc_runtime//:msvc_include"] + ["@windows_sdk//:winsdk_" + part + "_include" for part in ("ucrt", "shared", "um", "winrt")],
installed_tools = ":windows_installed_tools",
libraries = ["@msvc_runtime//:msvc_lib_" + arch] + ["@windows_sdk//:winsdk_" + part + "_lib_" + arch for part in ("ucrt", "um")],
msvc = "@msvc_runtime//:msvc_tools_" + arch,
sdk = "@windows_sdk//:winsdk_tools_" + arch,
tags = ["manual"],
target = cpu + "-pc-windows-msvc",
target_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:" + cpu,
],
)
for cpu, arch in (
("x86_64", "x64"),
("aarch64", "arm64"),
)
]
[
windows_native_prefix(
name = "native_prefix_windows_" + cpu,
archives = [":archives"],
build_tools = ":windows_tools_" + cpu,
exec_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:" + cpu,
],
tags = ["manual"],
target_compatible_with = select({
":windows_" + cpu + "_msvc": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
visibility = ["//visibility:public"],
)
for cpu in ("x86_64", "aarch64")
]
[
native_runtime(
name = "native_runtime_windows_" + cpu,
exec_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:" + cpu,
],
prefix = ":native_prefix_windows_" + cpu,
tags = ["manual"],
target = cpu + "-pc-windows-msvc",
target_compatible_with = select({
":windows_" + cpu + "_msvc": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
visibility = ["//visibility:public"],
windows_tools = ":windows_tools_" + cpu,
)
for cpu in ("x86_64", "aarch64")
]
[
native_link(
name = "native_link_windows_" + cpu,
runtime = ":native_runtime_windows_" + cpu,
tags = ["manual"],
target = cpu + "-pc-windows-msvc",
target_compatible_with = select({
":windows_" + cpu + "_msvc": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
visibility = ["//visibility:public"],
)
for cpu in ("x86_64", "aarch64")
]
# Link a concrete symbol so CI checks the Windows CcInfo import-library path,
# not only the native runtime and SDK artifact actions.
[
cc_binary(
name = "windows_link_smoke_" + cpu,
srcs = ["windows_link_smoke.cc"],
tags = ["manual"],
target_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:" + cpu,
"@llvm//constraints/windows/abi:msvc",
],
deps = [":native_link_windows_" + cpu],
)
for cpu in ("x86_64", "aarch64")
]

View File

@@ -240,3 +240,48 @@ Microsoft Visual C++ runtime (`VCRUNTIME140.dll`) already installed. The latter
is not a guaranteed OS component. Release redistribution/licensing, Authenticode
policy and actual helper loading remain separate requirements; this script does
not install or redistribute Microsoft runtime files or enable voice.
# Windows Bazel inputs and actions
The named `native_prefix_windows_{x86_64,aarch64}`,
`native_runtime_windows_{x86_64,aarch64}` and
`native_link_windows_{x86_64,aarch64}` targets use the existing native recipe,
runtime inspection and SDK export. These targets are MSVC-only. They require
native Windows execution of the matching architecture; they are not cross builds.
The generic Rust-consumer aliases are connected separately, after these inputs.
Provide the complete installed Cygwin/pkgconf repository explicitly. The default
`windows_installed_tools` label setting is empty and fails if a Windows action
needs it. This keeps ordinary public dependency queries independent of private
provisioning; it does not silently omit tools from a requested Windows build.
The module does not download that installed tree or accept compiler licenses.
After provisioning, a native PowerShell invocation is:
```powershell
bazel build //third_party/voice:native_link_windows_x86_64 `
--platforms=//:local_windows_msvc `
--inject_repository="voice_windows_tools=$env:VOICE_WINDOWS_BAZEL_REPOSITORY" `
--//third_party/voice:windows_installed_tools=@voice_windows_tools//:tools `
--action_env="SystemRoot=$env:SystemRoot" --host_action_env="SystemRoot=$env:SystemRoot"
```
Use `native_link_windows_aarch64` on native ARM64 Windows. Existing MSVC license
acceptance requirements still apply. `SystemRoot` must be a fixed action value,
not the inherited form `--action_env=SystemRoot`; Bazel analysis cannot inspect
that inherited value. The action validates the Windows command directory and
constructs its tool search path from declared inputs, not developer PATH entries.
Compiler, SDK, Python and CMake inputs use the existing pinned repositories.
CMake and Cygwin execute as x64 under emulation on ARM64; compiler, SDK tools,
Python and pkgconf match the native architecture. Complete support, include and
library files are declared, and installed entrypoints must match the supplied
target/architecture manifest and belong to that declared tree. This does not
turn caller-provided files into authenticated inputs: provisioning retains that
responsibility. These build tools must never enter shipped Codex packages.
Windows link inputs pair SDK import libraries with the corresponding prepared
DLLs. DLLs, plugins and the receipt remain under the normal native-link runfiles
layout; Windows receives no ELF or Mach-O runtime-search flags. This capability
still needs real native x64/ARM64 Bazel execution and consumer validation before
it establishes complete Windows voice support. The existing direct native recipe
passing on Windows does not prove this new Bazel input and execution path.

19
third_party/voice/bazel_copy.py vendored Normal file
View File

@@ -0,0 +1,19 @@
"""Copy declared library payloads while keeping a real native-search directory."""
from pathlib import Path
import shutil
import sys
def copy_payloads(locator, pairs):
if len(pairs) % 2:
raise ValueError("library copies require source/destination pairs")
Path(locator).mkdir(parents=True, exist_ok=True)
for source, destination in zip(pairs[::2], pairs[1::2], strict=True):
output = Path(destination)
output.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, output)
if __name__ == "__main__":
copy_payloads(sys.argv[1], sys.argv[2:])

171
third_party/voice/bazel_windows.py vendored Normal file
View File

@@ -0,0 +1,171 @@
"""Adapt declared Windows tool paths to the existing native build/runtime recipes."""
import hashlib
import json
import os
import re
import shutil
import sys
import tarfile
import tempfile
from pathlib import Path
from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parent))
from build_native import NativeBuild
from windows_build_inputs import build_environment
def selected_inputs(config, root):
inputs = dict(config["inputs"])
tools = {name: (root / path).absolute() for name, path in inputs["tools"].items()}
manifest = (root / config["manifest"]).absolute()
declared = {(root / path).absolute() for path in config["installed_files"]}
if manifest.stat().st_size > 65536:
raise ValueError("installed Windows tool metadata exceeds limits")
installed = json.loads(manifest.read_text(encoding="utf-8"))
expected = {"shell", "make", "cygpath", "automake", "pkg_config"}
if (
installed.get("schemaVersion") != 1
or installed.get("target") != inputs["target"]
or installed.get("cygwinArchitecture") != "x86_64"
or set(installed.get("tools", {})) != expected
):
raise ValueError("installed Windows tools do not match the selected target")
for name in expected:
relative = Path(installed["tools"][name])
source = (manifest.parent / relative).absolute()
if (
relative.is_absolute()
or ".." in relative.parts
or source not in declared
or (name == "pkg_config" and tools.get(name) != source)
or not source.resolve(strict=True).is_relative_to(
manifest.parent.resolve(strict=True)
)
):
raise ValueError(f"installed Windows tool selection differs: {name}")
tools[name] = source
inputs["tools"] = {name: str(path) for name, path in tools.items()}
for name in ("INCLUDE", "LIB"):
inputs[name] = [str((root / path).absolute()) for path in inputs[name]]
return inputs
def main():
operation, config_path = sys.argv[1:]
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
root = Path.cwd()
with tempfile.TemporaryDirectory(prefix="voice-windows-") as temporary:
temporary = Path(temporary)
document = temporary / "windows-inputs.json"
inputs = selected_inputs(config, root)
document.write_text(json.dumps(inputs), encoding="utf-8")
environment, _ = build_environment(
document, inputs["target"], {"python": Path(sys.executable)}
)
# Keep executor scratch directories, never its developer tool search path.
environment.update(
{name: os.environ[name] for name in ("TMP", "TEMP") if name in os.environ}
)
home = temporary / "home"
home.mkdir()
environment.update({"HOME": str(home), "USERPROFILE": str(home)})
os.environ.clear()
os.environ.update(environment)
if operation == "prepare":
from prepare_built_runtime import prepare_archive
output = root / config["output"]
sdk = root / config["sdk"]
for path in (output, sdk):
if path.exists():
path.rmdir()
try:
prepare_archive(
root / config["prefix"],
root / config["receipt"],
root / config["status"],
inputs["target"],
output,
sdk_output=sdk,
)
except ValueError as exc:
if str(exc) != "native build receipt is incomplete or mismatched":
raise
receipt = json.loads((root / config["receipt"]).read_text())
commits = [
line
for line in (root / config["status"]).read_text().splitlines()
if line.startswith("STABLE_GIT_COMMIT ")
]
steps = receipt.get("steps", [])
checks = {
"commit_count": len(commits) == 1,
"commit_format": len(commits) == 1
and bool(
re.fullmatch(r"STABLE_GIT_COMMIT [0-9a-f]{40}", commits[0])
),
"target": receipt.get("target") == inputs["target"],
"manifest": receipt.get("manifest_sha256")
== hashlib.sha256(
Path(__file__).with_name("sources.json").read_bytes()
).hexdigest(),
"step_count": 1 <= len(steps) <= 128,
"step_exit": all(step.get("exit_code") == 0 for step in steps),
"last_step": bool(steps)
and steps[-1].get("name") == "gst-plugins-good-install",
}
print(
"Receipt checks failed: "
+ ", ".join(name for name, valid in checks.items() if not valid),
file=sys.stderr,
)
raise
return
if operation != "build":
raise ValueError("unknown Windows native action")
archives = temporary / "archives"
archives.mkdir()
for path in config["archives"]:
source = root / path
shutil.copyfile(source, archives / source.name)
tools = inputs["tools"]
builder = NativeBuild(
SimpleNamespace(
target=inputs["target"],
deployment_target=None,
jobs=8,
output=temporary / "build",
archives=archives,
windows_build_inputs=document,
**{
name: Path(tools[name])
for name in (
"cc",
"cxx",
"cmake",
"make",
"pkg_config",
"shell",
"bootstrap_make",
)
},
),
environment,
)
try:
builder.build()
except Exception:
if builder.record["steps"]:
log = builder.output / (builder.record["steps"][-1]["name"] + ".log")
if log.is_file():
print(log.read_text(errors="replace"), file=sys.stderr)
raise
with tarfile.open(root / config["prefix"], "w") as archive:
archive.add(builder.prefix, arcname=".")
shutil.copyfile(builder.output / "built.json", root / config["receipt"])
if __name__ == "__main__":
main()

View File

@@ -3,6 +3,7 @@
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
load("@rules_python//python:py_runtime_info.bzl", "PyRuntimeInfo")
# ABI filenames from the pinned native sources. Missing outputs fail the build.
_ABI_VERSIONS = {
@@ -30,8 +31,10 @@ def _native_link_impl(ctx):
runtime = ctx.file.runtime
versions = dict(_ABI_VERSIONS)
macos = ctx.attr.target.endswith("apple-darwin")
if not macos:
windows = ctx.attr.target.endswith("windows-msvc")
if ctx.attr.target.endswith("unknown-linux-gnu"):
versions["gstallocators-1.0"] = "0"
sdk = ctx.attr.runtime[OutputGroupInfo].sdk.to_list()[0] if windows else None
locator = ctx.actions.declare_directory(ctx.label.name + "/lib/search-path")
originals, aliases, libraries = [], [], []
arguments = [locator.path]
@@ -45,39 +48,47 @@ def _native_link_impl(ctx):
for name, version in versions.items():
filename = "lib" + name + ("." + version + ".dylib" if macos else ".so." + version)
alias = "lib" + name + (".dylib" if macos else ".so")
library = ctx.actions.declare_file(ctx.label.name + "/lib/" + filename)
directory = "lib"
if windows:
# Windows names come from the same pinned recipe's SDK and runtime.
filename = {"ffi": "libffi-8.dll", "intl": "intl-8.dll", "opus": "opus.dll", "pcre2-8": "pcre2-8.dll", "z": "z.dll"}.get(name, name + "-0.dll")
alias = ("libffi" if name == "ffi" else name) + ".lib"
directory = "bin"
library = ctx.actions.declare_file(ctx.label.name + "/" + directory + "/" + filename)
development = ctx.actions.declare_file(ctx.label.name + "/lib/" + alias)
originals.append(library)
aliases.append(development)
arguments.extend([runtime.path + "/lib/" + filename, library.path])
arguments.extend([runtime.path + "/lib/" + filename, development.path])
arguments.extend([runtime.path + "/" + directory + "/" + filename, library.path])
arguments.extend([sdk.path + "/lib/" + alias if windows else runtime.path + "/lib/" + filename, development.path])
libraries.append(cc_common.create_library_to_link(
actions = ctx.actions,
feature_configuration = features,
cc_toolchain = cc,
dynamic_library = library,
interface_library = development if windows else None,
# @loader_path/$ORIGIN dependencies must stay beside one another.
dynamic_library_symlink_path = "voice/" + ctx.label.name + "/" + filename,
))
payloads = []
paths = ["runtime.json"] + [
("plugins/libgst" + plugin + ".dylib" if macos else "lib/gstreamer-1.0/libgst" + plugin + ".so")
("bin/gst" + plugin + ".dll" if windows else "plugins/libgst" + plugin + ".dylib" if macos else "lib/gstreamer-1.0/libgst" + plugin + ".so")
for plugin in "app audioconvert audioresample coreelements opus rtp rtpmanager".split(" ")
]
for path in paths:
payload = ctx.actions.declare_file(ctx.label.name + "/" + path)
payloads.append(payload)
arguments.extend([runtime.path + "/" + path, payload.path])
ctx.actions.run_shell(
inputs = [runtime],
python = ctx.attr._python[PyRuntimeInfo]
ctx.actions.run(
executable = python.interpreter,
inputs = depset([runtime, ctx.file._copy, python.interpreter] + ([sdk] if windows else []), transitive = [python.files]),
outputs = [locator] + originals + aliases + payloads,
arguments = arguments,
command = 'set -eu; /bin/mkdir -p "$1"; shift; while [ "$#" -gt 0 ]; do /bin/mkdir -p "${2%/*}"; /bin/cp "$1" "$2"; shift 2; done',
arguments = [ctx.file._copy.path] + arguments,
mnemonic = "VoiceNativeLinkInputs",
)
linker = cc_common.create_linker_input(
owner = ctx.label,
user_link_flags = depset([
user_link_flags = depset([] if windows else [
"-Wl,-rpath," + ("@loader_path/../lib" if macos else "$ORIGIN/../lib"),
]),
libraries = depset(libraries),
@@ -98,6 +109,8 @@ native_link = rule(
attrs = {
"runtime": attr.label(mandatory = True, allow_single_file = True),
"target": attr.string(mandatory = True),
"_copy": attr.label(default = "//third_party/voice:bazel_copy.py", allow_single_file = True),
"_python": attr.label(default = "@python_3_12//:py3_runtime", cfg = "exec"),
},
fragments = ["cpp"],
toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],

View File

@@ -1,6 +1,7 @@
"""Prepare build-produced native libraries with the existing platform policy."""
load("@rules_python//python:py_runtime_info.bzl", "PyRuntimeInfo")
load(":windows_native.bzl", "WindowsBuildToolsInfo")
def _native_runtime_impl(ctx):
python = ctx.attr._python[PyRuntimeInfo]
@@ -8,6 +9,31 @@ def _native_runtime_impl(ctx):
receipt = ctx.attr.prefix[OutputGroupInfo].receipt.to_list()[0]
output = ctx.actions.declare_directory(ctx.label.name)
sdk = ctx.actions.declare_directory(ctx.label.name + "_sdk")
if ctx.attr.windows_tools:
tools = ctx.attr.windows_tools[WindowsBuildToolsInfo]
if tools.inputs["target"] != ctx.attr.target:
fail("Windows runtime target must match its declared native tools")
config = ctx.actions.declare_file(ctx.label.name + ".json")
ctx.actions.write(config, json.encode({
"inputs": tools.inputs,
"manifest": tools.manifest.path,
"installed_files": [file.path for file in tools.installed_files],
"prefix": prefix.path,
"receipt": receipt.path,
"status": ctx.info_file.path,
"output": output.path,
"sdk": sdk.path,
}))
ctx.actions.run(
executable = tools.python.interpreter,
arguments = [ctx.file._windows_driver.path, "prepare", config.path],
inputs = depset([config, prefix, receipt, ctx.info_file, ctx.file._driver, ctx.file._windows_driver] + ctx.files._preparers, transitive = [tools.files]),
outputs = [output, sdk],
env = tools.environment,
execution_requirements = {"no-remote-exec": "1"},
mnemonic = "VoiceWindowsRuntime",
)
return [DefaultInfo(files = depset([output])), OutputGroupInfo(sdk = depset([sdk]))]
ctx.actions.run(
executable = python.interpreter,
arguments = [
@@ -45,6 +71,8 @@ native_runtime = rule(
attrs = {
"prefix": attr.label(mandatory = True, allow_single_file = True),
"target": attr.string(mandatory = True),
"windows_tools": attr.label(cfg = "exec", providers = [WindowsBuildToolsInfo]),
"_windows_driver": attr.label(default = "//third_party/voice:bazel_windows.py", allow_single_file = True),
"_driver": attr.label(default = "//third_party/voice:prepare_built_runtime.py", allow_single_file = True),
"_preparers": attr.label(default = "//third_party/voice:build_inputs"),
"_python": attr.label(default = "@python_3_12//:py3_runtime", cfg = "exec"),

170
third_party/voice/test_bazel_windows.py vendored Normal file
View File

@@ -0,0 +1,170 @@
"""Exercise declared tool selection and the platform-independent payload copy action."""
import copy
import errno
import json
from pathlib import Path
import tempfile
import unittest
from bazel_copy import copy_payloads
from bazel_windows import selected_inputs
class WindowsInputsTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.repository = self.root / "external/installed tools"
self.names = {
"shell": "cygwin/bin/bash.exe",
"make": "cygwin/bin/make.exe",
"cygpath": "cygwin/bin/cygpath.exe",
"automake": "cygwin/bin/automake-1.18",
"pkg_config": "pkgconf-image/PFiles64/pkgconf 3.0.6/pkgconf.exe",
}
for name in self.names.values():
path = self.repository / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"synthetic declared executable")
self.manifest = self.repository / "voice-tools.json"
self.metadata = {
"schemaVersion": 1,
"target": "aarch64-pc-windows-msvc",
"cygwinArchitecture": "x86_64",
"tools": self.names,
}
self.manifest.write_text(json.dumps(self.metadata))
self.config = {
"inputs": {
"schemaVersion": 1,
"target": "aarch64-pc-windows-msvc",
"systemRoot": "C:/Windows",
"tools": {
"cc": "external/msvc/cl.exe",
"pkg_config": str(
(self.repository / self.names["pkg_config"]).relative_to(
self.root
)
),
},
"INCLUDE": ["external/sdk/include"],
"LIB": ["external/sdk/lib"],
},
"manifest": str(self.manifest.relative_to(self.root)),
"installed_files": [
str((self.repository / name).relative_to(self.root))
for name in self.names.values()
],
}
def test_paths_are_anchored_before_recipe_changes_directory(self):
before = copy.deepcopy(self.config)
expected = {
**self.config["inputs"],
"tools": {
"cc": str(self.root / "external/msvc/cl.exe"),
**{
name: str(self.repository / path)
for name, path in self.names.items()
},
},
"INCLUDE": [str(self.root / "external/sdk/include")],
"LIB": [str(self.root / "external/sdk/lib")],
}
self.assertEqual(selected_inputs(self.config, self.root), expected)
self.assertEqual(self.config, before)
def test_manifest_cannot_select_a_file_omitted_from_action_inputs(self):
self.config["installed_files"].pop()
with self.assertRaisesRegex(ValueError, "selection differs: pkg_config"):
selected_inputs(self.config, self.root)
def test_manifest_cannot_escape_installed_tree(self):
outside = self.repository.parent / "outside.exe"
outside.write_bytes(b"not part of the installed support tree")
inside = self.repository / "cygwin/bin/other-bash.exe"
try:
inside.symlink_to(outside)
except OSError as error:
if error.errno not in (errno.EPERM, errno.EACCES):
raise
self.skipTest("creating symlinks requires OS permission")
self.config["installed_files"].append(str(inside.relative_to(self.root)))
self.metadata["tools"]["shell"] = str(inside.relative_to(self.repository))
self.manifest.write_text(json.dumps(self.metadata))
with self.assertRaisesRegex(ValueError, "selection differs: shell"):
selected_inputs(self.config, self.root)
def test_manifest_cannot_replace_the_build_script_executable(self):
alternative = self.repository / "pkgconf-image/another/pkgconf.exe"
alternative.parent.mkdir(parents=True)
alternative.write_bytes(b"different declared executable")
self.config["installed_files"].append(str(alternative.relative_to(self.root)))
self.metadata["tools"]["pkg_config"] = str(
alternative.relative_to(self.repository)
)
self.manifest.write_text(json.dumps(self.metadata))
with self.assertRaisesRegex(ValueError, "selection differs: pkg_config"):
selected_inputs(self.config, self.root)
def test_wrong_target_or_emulation_contract_is_rejected(self):
for key, value in (
("target", "x86_64-pc-windows-msvc"),
("cygwinArchitecture", "aarch64"),
):
with self.subTest(key=key):
self.manifest.write_text(json.dumps({**self.metadata, key: value}))
with self.assertRaisesRegex(ValueError, "selected target"):
selected_inputs(self.config, self.root)
def test_missing_declared_executable_does_not_fall_back_to_path(self):
(self.repository / self.names["shell"]).unlink()
with self.assertRaises(FileNotFoundError):
selected_inputs(self.config, self.root)
class LibraryCopiesTests(unittest.TestCase):
def test_import_library_and_dll_bytes_remain_distinct(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
dll = root / "source.dll"
library = root / "source.lib"
dll.write_bytes(b"DLL payload")
library.write_bytes(b"import library payload")
locator = root / "package/lib/search-path"
copy_payloads(
locator,
[
dll,
root / "package/bin/audio.dll",
library,
root / "package/lib/audio.lib",
],
)
self.assertTrue(locator.is_dir())
self.assertEqual(
{
path.relative_to(root / "package").as_posix(): path.read_bytes()
for path in (root / "package").rglob("*")
if path.is_file()
},
{
"bin/audio.dll": b"DLL payload",
"lib/audio.lib": b"import library payload",
},
)
def test_missing_payload_fails_the_action(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
with self.assertRaises(FileNotFoundError):
copy_payloads(
root / "lib/search-path",
[root / "missing.dll", root / "bin/audio.dll"],
)
if __name__ == "__main__":
unittest.main()

11
third_party/voice/windows_link_smoke.cc vendored Normal file
View File

@@ -0,0 +1,11 @@
// Force a real final link against the prepared GStreamer import library.
extern "C" void gst_version(unsigned int*, unsigned int*, unsigned int*, unsigned int*);
int main() {
unsigned int major = 0;
unsigned int minor = 0;
unsigned int micro = 0;
unsigned int nano = 0;
gst_version(&major, &minor, &micro, &nano);
return major == 1 ? 0 : 1;
}

129
third_party/voice/windows_native.bzl vendored Normal file
View File

@@ -0,0 +1,129 @@
"""Native Windows audio actions with complete, explicitly provisioned tool inputs."""
load("@bazel_skylib//rules/directory:providers.bzl", "DirectoryInfo")
load("@rules_python//python:py_runtime_info.bzl", "PyRuntimeInfo")
WindowsBuildToolsInfo = provider(
doc = "Selected native Windows tools, their support closure and recipe input document.",
fields = {
"files": "Complete declared executable, support, header and library files.",
"inputs": "Existing windows_build_inputs schema, with execroot-relative tool paths.",
"manifest": "Imported installed-tree manifest File.",
"installed_files": "Files supplied by the explicitly selected installed tool repository.",
"python": "Declared PyRuntimeInfo used by the actions.",
"environment": "Explicit Windows OS environment; never a developer PATH.",
},
)
def _windows_tools_impl(ctx):
architectures = {"x86_64-pc-windows-msvc": "ml64.exe", "aarch64-pc-windows-msvc": "armasm64.exe"}
if ctx.attr.target not in architectures:
fail("Windows native tools require a supported MSVC target")
python = ctx.attr._python[PyRuntimeInfo]
if not python.interpreter:
fail("Windows native tools require a declared native Python interpreter")
system_root = ctx.configuration.default_shell_env.get("SystemRoot")
if not system_root:
fail("Pass --action_env=SystemRoot=<actual Windows directory> as a fixed value")
msvc = ctx.attr.msvc[DirectoryInfo]
sdk = ctx.attr.sdk[DirectoryInfo]
tools = {
name: msvc.get_file(filename)
for name, filename in {
"cc": "cl.exe",
"cxx": "cl.exe",
"link": "link.exe",
"lib": "lib.exe",
"dumpbin": "dumpbin.exe",
"bootstrap_make": "nmake.exe",
"assembler": architectures[ctx.attr.target],
}.items()
}
tools.update({"rc": sdk.get_file("rc.exe"), "mt": sdk.get_file("mt.exe")})
tools.update({"cmake": ctx.file.cmake, "python": python.interpreter})
installed = ctx.files.installed_tools
manifests = [file for file in installed if file.basename == "voice-tools.json"]
if len(manifests) != 1:
fail("Select a complete installed tool repository with --//third_party/voice:windows_installed_tools=<tools label>")
pkgconf_root = manifests[0].dirname + "/pkgconf-image/"
pkgconf_files = [file for file in installed if file.path.startswith(pkgconf_root)]
pkgconf = [file for file in pkgconf_files if file.basename == "pkgconf.exe"]
if len(pkgconf) != 1:
fail("Installed tool repository must declare exactly one pkgconf-image pkgconf.exe")
tools["pkg_config"] = pkgconf[0]
if ctx.file.cmake not in ctx.files.cmake_data:
fail("CMake executable must belong to its declared support tree")
includes = [target[DirectoryInfo] for target in ctx.attr.includes]
libraries = [target[DirectoryInfo] for target in ctx.attr.libraries]
files = depset(
tools.values() + installed,
transitive = [msvc.transitive_files, sdk.transitive_files, python.files, ctx.attr.cmake_data.files] +
[directory.transitive_files for directory in includes + libraries],
)
return [DefaultInfo(
files = depset(pkgconf),
runfiles = ctx.runfiles(files = pkgconf_files),
), WindowsBuildToolsInfo(
files = files,
python = python,
manifest = manifests[0],
installed_files = installed,
environment = {"SystemRoot": system_root, "SYSTEMROOT": system_root},
inputs = {
"schemaVersion": 1,
"target": ctx.attr.target,
"systemRoot": system_root,
"tools": {name: file.path for name, file in tools.items()},
"INCLUDE": [directory.path for directory in includes],
"LIB": [directory.path for directory in libraries],
},
)]
windows_build_tools = rule(
implementation = _windows_tools_impl,
attrs = {
"target": attr.string(mandatory = True),
"msvc": attr.label(mandatory = True, providers = [DirectoryInfo]),
"sdk": attr.label(mandatory = True, providers = [DirectoryInfo]),
"includes": attr.label_list(mandatory = True, providers = [DirectoryInfo]),
"libraries": attr.label_list(mandatory = True, providers = [DirectoryInfo]),
"installed_tools": attr.label(mandatory = True),
"cmake": attr.label(default = "@cmake-3.31.8-windows-x86_64//:cmake_bin", allow_single_file = True),
"cmake_data": attr.label(default = "@cmake-3.31.8-windows-x86_64//:cmake_data"),
"_python": attr.label(default = "@python_3_12//:py3_runtime", cfg = "exec"),
},
)
def _windows_prefix_impl(ctx):
tools = ctx.attr.build_tools[WindowsBuildToolsInfo]
prefix = ctx.actions.declare_file(ctx.label.name + "/prefix.tar")
receipt = ctx.actions.declare_file(ctx.label.name + "/built.json")
config = ctx.actions.declare_file(ctx.label.name + ".json")
ctx.actions.write(config, json.encode({
"inputs": tools.inputs,
"manifest": tools.manifest.path,
"installed_files": [file.path for file in tools.installed_files],
"archives": [file.path for file in ctx.files.archives],
"prefix": prefix.path,
"receipt": receipt.path,
}))
ctx.actions.run(
executable = tools.python.interpreter,
arguments = [ctx.file._driver.path, "build", config.path],
inputs = depset([config, ctx.file._driver] + ctx.files.archives + ctx.files._recipe, transitive = [tools.files]),
outputs = [prefix, receipt],
env = tools.environment,
execution_requirements = {"no-remote-exec": "1"},
mnemonic = "VoiceWindowsPrefix",
)
return [DefaultInfo(files = depset([prefix])), OutputGroupInfo(receipt = depset([receipt]))]
windows_native_prefix = rule(
implementation = _windows_prefix_impl,
attrs = {
"build_tools": attr.label(mandatory = True, cfg = "exec", providers = [WindowsBuildToolsInfo]),
"archives": attr.label_list(mandatory = True, allow_files = True),
"_driver": attr.label(default = "//third_party/voice:bazel_windows.py", allow_single_file = True),
"_recipe": attr.label(default = "//third_party/voice:native_recipe"),
},
)