From dab0f7abe5221b26393e4a2e876f4cfbd4d596d4 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Wed, 10 Dec 2025 13:31:26 +0000 Subject: [PATCH 01/20] Added find_roslyn_tools target that searches for Visual Studio installations. This works on Windows and is the same behavior that the C++ rules already have --- prelude/toolchains/csharp.bzl | 9 ++- prelude/toolchains/msvc/BUCK | 8 ++- prelude/toolchains/msvc/tools.bzl | 49 +++++++++++++++ prelude/toolchains/msvc/vswhere.py | 95 +++++++++++++++++++++--------- 4 files changed, 129 insertions(+), 32 deletions(-) diff --git a/prelude/toolchains/csharp.bzl b/prelude/toolchains/csharp.bzl index b48d7b65c3948..594bae0f79ab0 100644 --- a/prelude/toolchains/csharp.bzl +++ b/prelude/toolchains/csharp.bzl @@ -12,10 +12,12 @@ def _system_csharp_toolchain_impl(ctx): if not host_info().os.is_windows: fail("csharp toolchain only supported on windows for now") + csc = ctx.attrs._csharp_tools_info[CSharpToolchainInfo].csc if ctx.attrs.csc == None else ctx.attrs.csc + return [ DefaultInfo(), CSharpToolchainInfo( - csc = RunInfo(args = ctx.attrs.csc), + csc = RunInfo(args = csc), framework_dirs = { "net35": "C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v3.5\\Profile\\Client", "net40": "C:\\Program Files (x86)\\Reference Assemblies\\Microsoft\\Framework\\.NETFramework\\v4.0", @@ -42,12 +44,13 @@ system_csharp_toolchain = rule( visibility = ["PUBLIC"], )""", attrs = { - "csc": attrs.string(default = "csc.exe", doc = "Executable name or a path to the C# compiler frequently referred to as csc.exe"), + "csc": attrs.option(attrs.string(), default = None, doc = "Executable name or a path to the C# compiler frequently referred to as csc.exe"), "framework_dirs": attrs.dict( key = attrs.string(), value = attrs.one_of(attrs.source(), attrs.string()), - doc = "Dictionary of .NET framework assembly directories, where each key is a supported value in `framework_ver` and the value is a path to a directory containing .net assemblies such as System.dll matching the given framework version", + doc = "Dictionary of .NET framework assembly directories, where each key is a supported value in `framework_ver` and the value is a path to a directory containing .net assemblies such as System.dll matching the given framework version" ), + "_csharp_tools_info": attrs.exec_dep(providers = [CSharpToolchainInfo], default = "prelude//toolchains/msvc:roslyn_tools"), }, is_toolchain_rule = True, ) diff --git a/prelude/toolchains/msvc/BUCK b/prelude/toolchains/msvc/BUCK index 42d4db5370d7a..789f1237b6244 100644 --- a/prelude/toolchains/msvc/BUCK +++ b/prelude/toolchains/msvc/BUCK @@ -1,5 +1,5 @@ load("@prelude//utils:source_listing.bzl", "source_listing") -load(":tools.bzl", "find_msvc_tools") +load(":tools.bzl", "find_msvc_tools", "find_roslyn_tools") oncall("build_infra") @@ -22,3 +22,9 @@ find_msvc_tools( target_compatible_with = ["config//os:windows"], visibility = ["PUBLIC"], ) + +find_roslyn_tools( + name = "roslyn_tools", + target_compatible_with = ["config//os:windows"], + visibility = ["PUBLIC"], +) diff --git a/prelude/toolchains/msvc/tools.bzl b/prelude/toolchains/msvc/tools.bzl index e7903f2ffdece..a587942174f11 100644 --- a/prelude/toolchains/msvc/tools.bzl +++ b/prelude/toolchains/msvc/tools.bzl @@ -6,6 +6,7 @@ # of this source tree. You may select, at your option, one of the # above-listed licenses. +load("@prelude//csharp:toolchain.bzl", "CSharpToolchainInfo") load("@prelude//cxx:cxx_toolchain_types.bzl", "LinkerType") load("@prelude//os_lookup:defs.bzl", "ScriptLanguage") load("@prelude//toolchains:cxx.bzl", "CxxToolsInfo") @@ -177,6 +178,45 @@ def _windows_linker_wrapper(ctx: AnalysisContext, linker: [cmd_args, str]) -> cm language = ScriptLanguage("bat"), ) +def _find_roslyn_tools_impl(ctx: AnalysisContext) -> list[Provider]: + csc_exe_json = ctx.actions.declare_output("csc.exe.json") + + cmd = [ + ctx.attrs.vswhere[RunInfo], + cmd_args("--csc=", csc_exe_json.as_output(), delimiter = ""), + ] + + ctx.actions.run( + cmd, + category = "vswhere", + local_only = True, + ) + + run_msvc_tool = ctx.attrs.run_msvc_tool[RunInfo] + if ctx.attrs.use_path_compilers: + csc_exe_script = "csc.exe" + else: + csc_exe_script = cmd_script( + actions = ctx.actions, + name = "csc", + cmd = cmd_args(run_msvc_tool, csc_exe_json), + language = ScriptLanguage("bat"), + ) + + return [ + DefaultInfo(sub_targets = { + "csc.exe": [ + RunInfo(args = [csc_exe_script]), + DefaultInfo(sub_targets = { + "json": [DefaultInfo(default_output = csc_exe_json)], + }), + ], + }), + CSharpToolchainInfo( + csc = csc_exe_script + ), + ] + find_msvc_tools = rule( impl = _find_msvc_tools_impl, attrs = { @@ -187,3 +227,12 @@ find_msvc_tools = rule( "vswhere": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:vswhere")), }, ) + +find_roslyn_tools = rule( + impl = _find_roslyn_tools_impl, + attrs = { + "run_msvc_tool": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:run_msvc_tool")), + "use_path_compilers": attrs.bool(default = False), + "vswhere": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:vswhere")), + }, +) diff --git a/prelude/toolchains/msvc/vswhere.py b/prelude/toolchains/msvc/vswhere.py index e5e5530551cba..6347ee23fc3b6 100644 --- a/prelude/toolchains/msvc/vswhere.py +++ b/prelude/toolchains/msvc/vswhere.py @@ -33,6 +33,7 @@ class OutputJsonFiles(NamedTuple): ml64: IO[str] link: IO[str] rc: IO[str] + csc: IO[str] class Tool(NamedTuple): @@ -53,7 +54,7 @@ def find_in_path(executable, is_optional=False): return Tool(which) -def find_with_vswhere_exe(): +def run_vswhere(required_component): program_files = os.environ.get("ProgramFiles(x86)") if program_files is None: program_files = os.environ.get("ProgramFiles") @@ -73,7 +74,7 @@ def find_with_vswhere_exe(): "-products", "*", "-requires", - "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + required_component, "-format", "json", "-utf8", @@ -92,6 +93,12 @@ def find_with_vswhere_exe(): reverse=True, ) + return vswhere_json + + +def find_msvc_with_vswhere_exe(): + vswhere_json = run_vswhere("Microsoft.VisualStudio.Component.VC.Tools.x86.x64") + for vs_instance in list(vswhere_json): installation_path = Path(vs_instance["installationPath"]) @@ -153,6 +160,32 @@ def find_with_vswhere_exe(): sys.exit(1) +def find_roslyn_with_vswhere_exe(): + vswhere_json = run_vswhere("Microsoft.VisualStudio.Component.Roslyn.Compiler") + + for vs_instance in list(vswhere_json): + installation_path = Path(vs_instance["installationPath"]) + + roslyn_path = ( + installation_path + / "MSBuild" + / "Current" + / "Bin" + / "Roslyn" + / "csc.exe" + ) + + if not roslyn_path.exists(): + continue + + return Tool(exe=roslyn_path, LIB=[], PATH=[], INCLUDE=[]) + + print( + "vswhere.exe did not find a suitable Roslyn toolchain containing csc.exe", + file=sys.stderr, + ) + sys.exit(1) + # To find the Universal CRT we look in a specific registry key for where all the # Universal CRTs are located and then sort them asciibetically to find the # newest version. While this sort of sorting isn't ideal, it is what vcvars does @@ -298,36 +331,42 @@ def find_with_ewdk(ewdkdir: Path): def main(): parser = argparse.ArgumentParser() - parser.add_argument("--cl", type=argparse.FileType("w"), required=True) - parser.add_argument("--cvtres", type=argparse.FileType("w"), required=True) - parser.add_argument("--lib", type=argparse.FileType("w"), required=True) - parser.add_argument("--ml64", type=argparse.FileType("w"), required=True) - parser.add_argument("--link", type=argparse.FileType("w"), required=True) - parser.add_argument("--rc", type=argparse.FileType("w"), required=True) + parser.add_argument("--cl", type=argparse.FileType("w")) + parser.add_argument("--cvtres", type=argparse.FileType("w")) + parser.add_argument("--lib", type=argparse.FileType("w")) + parser.add_argument("--ml64", type=argparse.FileType("w")) + parser.add_argument("--link", type=argparse.FileType("w")) + parser.add_argument("--rc", type=argparse.FileType("w")) + parser.add_argument("--csc", type=argparse.FileType("w")) output = OutputJsonFiles(**vars(parser.parse_args())) - # If vcvars has been run, it puts these tools onto $PATH. - if "VCINSTALLDIR" in os.environ: - cl_exe, cvtres_exe, lib_exe, ml64_exe, link_exe = ( - find_in_path(exe) for exe in VC_EXE_NAMES - ) - rc_exe = find_in_path("rc.exe", is_optional=True) - elif "EWDKDIR" in os.environ: - cl_exe, cvtres_exe, lib_exe, ml64_exe, link_exe, rc_exe = find_with_ewdk( - Path(os.environ["EWDKDIR"]) - ) - else: - cl_exe, cvtres_exe, lib_exe, ml64_exe, link_exe, rc_exe = ( - find_with_vswhere_exe() - ) + if output.cl or output.cvtres or output.lib or output.ml64 or output.link or output.rc: + # If vcvars has been run, it puts these tools onto $PATH. + if "VCINSTALLDIR" in os.environ: + cl_exe, cvtres_exe, lib_exe, ml64_exe, link_exe = ( + find_in_path(exe) for exe in VC_EXE_NAMES + ) + rc_exe = find_in_path("rc.exe", is_optional=True) + elif "EWDKDIR" in os.environ: + cl_exe, cvtres_exe, lib_exe, ml64_exe, link_exe, rc_exe = find_with_ewdk( + Path(os.environ["EWDKDIR"]) + ) + else: + cl_exe, cvtres_exe, lib_exe, ml64_exe, link_exe, rc_exe = ( + find_msvc_with_vswhere_exe() + ) + + write_tool_json(output.cl, cl_exe) + write_tool_json(output.cvtres, cvtres_exe) + write_tool_json(output.lib, lib_exe) + write_tool_json(output.ml64, ml64_exe) + write_tool_json(output.link, link_exe) + write_tool_json(output.rc, rc_exe) - write_tool_json(output.cl, cl_exe) - write_tool_json(output.cvtres, cvtres_exe) - write_tool_json(output.lib, lib_exe) - write_tool_json(output.ml64, ml64_exe) - write_tool_json(output.link, link_exe) - write_tool_json(output.rc, rc_exe) + if output.csc: + csc_exe = find_roslyn_with_vswhere_exe() + write_tool_json(output.csc, csc_exe) if __name__ == "__main__": main() From c2e8367d7cc4b8abce7aeb9e98609e31d3299757 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:50:05 +0000 Subject: [PATCH 02/20] Added possiblity of not adding /noconfig and similar arguments in C# --- prelude/csharp/csharp.bzl | 23 ++++++++++++----------- prelude/decls/dotnet_rules.bzl | 1 + 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/prelude/csharp/csharp.bzl b/prelude/csharp/csharp.bzl index 8ca3b6e1efff6..0da89859fe2ef 100644 --- a/prelude/csharp/csharp.bzl +++ b/prelude/csharp/csharp.bzl @@ -34,17 +34,18 @@ def csharp_library_impl(ctx: AnalysisContext) -> list[Provider]: ) ) - # Don't include any default .NET framework assemblies like "mscorlib" or "System" unless - # explicitly requested with `/reference:{}`. This flag also stops injection of other - # default compiler flags. - cmd.append("/noconfig") - - # Don't reference mscorlib.dll unless asked for. This is required for targets that target - # embedded platforms such as Silverlight or WASM. (Originally for Buck1 compatibility.) - cmd.append("/nostdlib") - - # Don't search any paths for .NET libraries unless explicitly referenced with `/lib:{}`. - cmd.append("/nosdkpath") + if ctx.attrs.add_hermetic_arguments: + # Don't include any default .NET framework assemblies like "mscorlib" or "System" unless + # explicitly requested with `/reference:{}`. This flag also stops injection of other + # default compiler flags. + cmd.append("/noconfig") + + # Don't reference mscorlib.dll unless asked for. This is required for targets that target + # embedded platforms such as Silverlight or WASM. (Originally for Buck1 compatibility.) + cmd.append("/nostdlib") + + # Don't search any paths for .NET libraries unless explicitly referenced with `/lib:{}`. + cmd.append("/nosdkpath") # Let csc know the directory path where it can find system assemblies. This is the path # that is searched by `/reference:{libname}` if `libname` is just a DLL name. diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index 074a1fa26cb4c..66bc48fbfbfee 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -101,6 +101,7 @@ csharp_library = prelude_rule( The set of additional compiler flags to pass to the compiler. """, ), + "add_hermetic_arguments": attrs.bool(default = True), } | buck.licenses_arg() | buck.labels_arg() From 55f33effc2396cd4fee99257efb83192d3485145 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:47:06 +0000 Subject: [PATCH 03/20] Moved some .net things into dotnet_common --- prelude/decls/dotnet_common.bzl | 74 +++++++++++++++++++++++++++++++++ prelude/decls/dotnet_rules.bzl | 55 ++++-------------------- 2 files changed, 82 insertions(+), 47 deletions(-) create mode 100644 prelude/decls/dotnet_common.bzl diff --git a/prelude/decls/dotnet_common.bzl b/prelude/decls/dotnet_common.bzl new file mode 100644 index 0000000000000..5e186065079f9 --- /dev/null +++ b/prelude/decls/dotnet_common.bzl @@ -0,0 +1,74 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +# TODO(cjhopman): This was generated by scripts/hacks/rules_shim_with_docs.py, +# but should be manually edited going forward. There may be some errors in +# the generated docs, and so those should be verified to be accurate and +# well-formatted (and then delete this TODO) + +FrameworkVersion = ["net35", "net40", "net45", "net46"] + +def _srcs_arg(): + return { + "srcs": attrs.list(attrs.one_of(attrs.source(), attrs.tuple(attrs.source(), attrs.list(attrs.arg()))), default = [], doc = """ + The set of C, C++, Objective-C, Objective-C++, or assembly source files + to be preprocessed, compiled, and assembled by this + rule. We determine which stages to run on each input source based on its file extension. See the + [GCC documentation](https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html) for more detail on how file extensions are interpreted. Each element can be either a string + specifying a source file (e.g. `''`) or a tuple of + a string specifying a source file and a list of compilation flags + (e.g. `('', ['-Wall', '-Werror'])` ). In the latter case the specified flags will be used in addition to the rule's other + flags when preprocessing and compiling that file (if applicable). +"""), + } + +def _resources_arg(): + return { + "resources": attrs.dict(key = attrs.string(), value = attrs.source(), sorted = False, default = {}, doc = """ + Resources that should be embedded within the built DLL. The format + is the name of the resource once mapped into the DLL as the key, and + the value being the resource that should be merged. This allows + non-unique keys to be identified quickly. +"""), + } + +def _framework_ver_arg(): + return { + "framework_ver": attrs.enum(FrameworkVersion, doc = """ + The version of the .Net framework that this library targets. This is + one of 'net35', 'net40', 'net45' and 'net46'. +"""), + } + +def _deps_arg(): + return { + "deps": attrs.list(attrs.one_of(attrs.dep(), attrs.string()), default = [], doc = """ + The set of targets or system-provided assemblies to rely on. Any + values that are targets must be either csharp\\_library or `prebuilt_dotnet_library` + instances. +"""), + } + +def _compiler_flags_arg(): + return { + "compiler_flags": attrs.list(attrs.string(), default = [], doc = """ + The set of additional compiler flags to pass to the compiler. +"""), + } + +def _add_hermetic_arguments_arg(): + return {"add_hermetic_arguments": attrs.bool(default = True)} + +dotnet_common = struct( + srcs_arg = _srcs_arg, + resources_arg = _resources_arg, + framework_ver_arg = _framework_ver_arg, + deps_arg = _deps_arg, + compiler_flags_arg = _compiler_flags_arg, + add_hermetic_arguments_arg = _add_hermetic_arguments_arg, +) diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index 66bc48fbfbfee..372a115222329 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -12,8 +12,7 @@ # well-formatted (and then delete this TODO) load(":common.bzl", "buck", "prelude_rule") - -FrameworkVersion = ["net35", "net40", "net45", "net46"] +load(":dotnet_common.bzl", "dotnet_common") csharp_library = prelude_rule( name = "csharp_library", @@ -57,52 +56,14 @@ csharp_library = prelude_rule( The output name of the dll. This allows you to specify the name of the dll exactly. When this is not set, the dll will be named after the short name of the target. - """, - ), - "srcs": attrs.list( - attrs.source(), - default = [], - doc = """ - The collection of source files to compile. - """, - ), - "resources": attrs.dict( - key = attrs.string(), - value = attrs.source(), - sorted = False, - default = {}, - doc = """ - Resources that should be embedded within the built DLL. The format - is the name of the resource once mapped into the DLL as the key, and - the value being the resource that should be merged. This allows - non-unique keys to be identified quickly. - """, - ), - "framework_ver": attrs.enum( - FrameworkVersion, - doc = """ - The version of the .Net framework that this library targets. This is - one of 'net35', 'net40', 'net45' and 'net46'. - """, - ), - "deps": attrs.list( - attrs.one_of(attrs.dep(), attrs.string()), - default = [], - doc = """ - The set of targets or system-provided assemblies to rely on. Any - values that are targets must be either csharp\\_library or `prebuilt_dotnet_library` - instances. - """, - ), - "compiler_flags": attrs.list( - attrs.string(), - default = [], - doc = """ - The set of additional compiler flags to pass to the compiler. - """, - ), - "add_hermetic_arguments": attrs.bool(default = True), + """), } + | dotnet_common.srcs_arg() + | dotnet_common.resources_arg() + | dotnet_common.framework_ver_arg() + | dotnet_common.deps_arg() + | dotnet_common.compiler_flags_arg() + | dotnet_common.add_hermetic_arguments_arg() | buck.licenses_arg() | buck.labels_arg() | buck.contacts_arg() From 3abbbcd03f0b332e821364489823a0506c6d741f Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:47:31 +0000 Subject: [PATCH 04/20] Added csharp_binary rule --- prelude/csharp/csharp.bzl | 47 +++++++++++++++++---------- prelude/decls/dotnet_rules.bzl | 58 ++++++++++++++++++++++++++++++++++ prelude/rules_impl.bzl | 6 +++- 3 files changed, 94 insertions(+), 17 deletions(-) diff --git a/prelude/csharp/csharp.bzl b/prelude/csharp/csharp.bzl index 0da89859fe2ef..81007ae7e6a5d 100644 --- a/prelude/csharp/csharp.bzl +++ b/prelude/csharp/csharp.bzl @@ -9,27 +9,23 @@ load(":csharp_providers.bzl", "DllDepTSet", "DllReference", "DotNetLibraryInfo", "generate_target_tset_children") load(":toolchain.bzl", "CSharpToolchainInfo") -def csharp_library_impl(ctx: AnalysisContext) -> list[Provider]: +def _csharp_library_or_exe(ctx: AnalysisContext, library_or_exe_name: str, target_type: str) -> DotNetLibraryInfo: toolchain = ctx.attrs._csharp_toolchain[CSharpToolchainInfo] - # Automatically set the output dll_name to this target's name if the caller did not specify a - # custom name. - dll_name = "{}.dll".format(ctx.attrs.name) if not ctx.attrs.dll_name else ctx.attrs.dll_name - - # Declare that this rule will produce a dll. - library = ctx.actions.declare_output(dll_name, has_content_based_path = False) + # Declare that this rule will produce a dll or exe. + library_or_exe = ctx.actions.declare_output(library_or_exe_name, has_content_based_path = False) - # Create a command invoking a wrapper script that calls csc.exe to compile the .dll. + # Create a command invoking a wrapper script that calls csc.exe to compile the .dll or the .exe. cmd = [toolchain.csc] # Add caller specified compiler flags. cmd.append(ctx.attrs.compiler_flags) # Set the output target as a .NET library. - cmd.append("/target:library") + cmd.append("/target:" + target_type) cmd.append( cmd_args( - library.as_output(), + library_or_exe.as_output(), format = "/out:{}", ) ) @@ -65,13 +61,32 @@ def csharp_library_impl(ctx: AnalysisContext) -> list[Provider]: # Run the C# compiler to produce the output artifact. ctx.actions.run(cmd, category = "csharp_compile") + return DotNetLibraryInfo( + name = library_or_exe_name, + object = library_or_exe, + dll_deps = ctx.actions.tset(DllDepTSet, value = DllReference(reference = library_or_exe), children = child_deps), + ) + +def csharp_library_impl(ctx: AnalysisContext) -> list[Provider]: + # Automatically set the output dll_name to this target's name if the caller did not specify a + # custom name. + dll_name = "{}.dll".format(ctx.attrs.name) if not ctx.attrs.dll_name else ctx.attrs.dll_name + + dotNetLibraryInfo = _csharp_library_or_exe(ctx, dll_name, "library") + return [ - DefaultInfo(default_output = library), - DotNetLibraryInfo( - name = ctx.attrs.dll_name, - object = library, - dll_deps = ctx.actions.tset(DllDepTSet, value = DllReference(reference = library), children = child_deps), - ), + DefaultInfo(default_output = dotNetLibraryInfo.object), + dotNetLibraryInfo, + ] + +def csharp_binary_impl(ctx: AnalysisContext) -> list[Provider]: + exe_name = "{}.exe".format(ctx.attrs.name) if not ctx.attrs.exe_name else ctx.attrs.exe_name + + dotNetLibraryInfo = _csharp_library_or_exe(ctx, exe_name, "exe") + + return [ + DefaultInfo(default_output = dotNetLibraryInfo.object), + RunInfo(args = cmd_args(dotNetLibraryInfo.object)), ] def prebuilt_dotnet_library_impl(ctx: AnalysisContext) -> list[Provider]: diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index 372a115222329..2baaf070066d4 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -70,6 +70,63 @@ csharp_library = prelude_rule( ), ) +csharp_binary = prelude_rule( + name = "csharp_binary", + docs = """ + A csharp\\_binary() rule builds a .Net library from the supplied set of C# source files + and dependencies by invoking csc. + """, + examples = """ + For more examples, check out our [integration tests](https://github.com/facebook/buck/tree/dev/test/com/facebook/buck/rust/testdata/). + + + ``` + + csharp_binary( + name = 'simple', + exe_name = 'Cake.exe', + framework_ver = 'net46', + srcs = [ + 'Hello.cs', + ], + resources = { + 'greeting.txt': '//some:target', + }, + deps=[ + ':other', + 'System.dll', + ], + ) + + prebuilt_dotnet_library( + name = 'other', + assembly = 'other-1.0.dll', + ) + + ``` + """, + further = None, + attrs = ( + # @unsorted-dict-items + { + "exe_name": attrs.string(default = "", doc = """ + The output name of the dll. This allows you to specify the name of + the dll exactly. When this is not set, the dll will be named after + the short name of the target. + """), + } | + dotnet_common.srcs_arg() | + dotnet_common.resources_arg() | + dotnet_common.framework_ver_arg() | + dotnet_common.deps_arg() | + dotnet_common.compiler_flags_arg() | + dotnet_common.add_hermetic_arguments_arg() | + buck.licenses_arg() | + buck.labels_arg() | + buck.contacts_arg() + ), +) + prebuilt_dotnet_library = prelude_rule( name = "prebuilt_dotnet_library", docs = """ @@ -114,5 +171,6 @@ prebuilt_dotnet_library = prelude_rule( dotnet_rules = struct( csharp_library = csharp_library, + csharp_binary = csharp_binary, prebuilt_dotnet_library = prebuilt_dotnet_library, ) diff --git a/prelude/rules_impl.bzl b/prelude/rules_impl.bzl index 7c890e96891ae..809c69d3bac03 100644 --- a/prelude/rules_impl.bzl +++ b/prelude/rules_impl.bzl @@ -22,7 +22,7 @@ load("@prelude//apple:apple_common.bzl", "apple_common") load("@prelude//apple:apple_rules_decls.bzl", "apple_rules") load("@prelude//apple:apple_rules_impl.bzl", _apple_extra_attributes = "extra_attributes", _apple_implemented_rules = "implemented_rules") load("@prelude//configurations:rules.bzl", _config_extra_attributes = "extra_attributes", _config_implemented_rules = "implemented_rules") -load("@prelude//csharp:csharp.bzl", "csharp_library_impl", "prebuilt_dotnet_library_impl") +load("@prelude//csharp:csharp.bzl", "csharp_library_impl", "csharp_binary_impl", "prebuilt_dotnet_library_impl") load("@prelude//cxx:bitcode.bzl", "llvm_link_bitcode_impl") load("@prelude//cxx:cuda.bzl", "CudaCompileStyle") load("@prelude//cxx:cxx.bzl", "cxx_binary_impl", "cxx_library_impl", "cxx_precompiled_header_impl", "cxx_test_impl", "prebuilt_cxx_library_impl") @@ -174,6 +174,7 @@ extra_implemented_rules = struct( worker_tool = worker_tool, # c# csharp_library = csharp_library_impl, + csharp_binary = csharp_binary_impl, prebuilt_dotnet_library = prebuilt_dotnet_library_impl, # c++ cxx_binary = cxx_binary_impl, @@ -266,6 +267,9 @@ _dotnet_extra_attributes = { "csharp_library": { "_csharp_toolchain": toolchains_common.csharp(), }, + "csharp_binary": { + "_csharp_toolchain": toolchains_common.csharp(), + }, } _cxx_extra_library_attrs = ( From 7754471e52f77f7755ee2c5fac27a78455304602 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:02 +0100 Subject: [PATCH 05/20] Refactored code so a DotNetLibraryInfo is only built for a library but not for an exe --- prelude/csharp/csharp.bzl | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/prelude/csharp/csharp.bzl b/prelude/csharp/csharp.bzl index 81007ae7e6a5d..b70b1bbed9740 100644 --- a/prelude/csharp/csharp.bzl +++ b/prelude/csharp/csharp.bzl @@ -9,11 +9,11 @@ load(":csharp_providers.bzl", "DllDepTSet", "DllReference", "DotNetLibraryInfo", "generate_target_tset_children") load(":toolchain.bzl", "CSharpToolchainInfo") -def _csharp_library_or_exe(ctx: AnalysisContext, library_or_exe_name: str, target_type: str) -> DotNetLibraryInfo: +def _csharp_library_or_exe_artifact(ctx: AnalysisContext, library_or_exe_name: str, target_type: str) -> (Artifact, list[DllDepTSet]): toolchain = ctx.attrs._csharp_toolchain[CSharpToolchainInfo] # Declare that this rule will produce a dll or exe. - library_or_exe = ctx.actions.declare_output(library_or_exe_name, has_content_based_path = False) + library_or_exe_artifact = ctx.actions.declare_output(library_or_exe_name, has_content_based_path = False) # Create a command invoking a wrapper script that calls csc.exe to compile the .dll or the .exe. cmd = [toolchain.csc] @@ -25,7 +25,7 @@ def _csharp_library_or_exe(ctx: AnalysisContext, library_or_exe_name: str, targe cmd.append("/target:" + target_type) cmd.append( cmd_args( - library_or_exe.as_output(), + library_or_exe_artifact.as_output(), format = "/out:{}", ) ) @@ -61,32 +61,32 @@ def _csharp_library_or_exe(ctx: AnalysisContext, library_or_exe_name: str, targe # Run the C# compiler to produce the output artifact. ctx.actions.run(cmd, category = "csharp_compile") - return DotNetLibraryInfo( - name = library_or_exe_name, - object = library_or_exe, - dll_deps = ctx.actions.tset(DllDepTSet, value = DllReference(reference = library_or_exe), children = child_deps), - ) + return library_or_exe_artifact, child_deps def csharp_library_impl(ctx: AnalysisContext) -> list[Provider]: # Automatically set the output dll_name to this target's name if the caller did not specify a # custom name. dll_name = "{}.dll".format(ctx.attrs.name) if not ctx.attrs.dll_name else ctx.attrs.dll_name - dotNetLibraryInfo = _csharp_library_or_exe(ctx, dll_name, "library") + library_or_exe_artifact, child_deps = _csharp_library_or_exe_artifact(ctx, dll_name, "library") return [ - DefaultInfo(default_output = dotNetLibraryInfo.object), - dotNetLibraryInfo, + DefaultInfo(default_output = library_or_exe_artifact), + DotNetLibraryInfo( + name = library_or_exe_artifact.basename, + object = library_or_exe_artifact, + dll_deps = ctx.actions.tset(DllDepTSet, value = DllReference(reference = library_or_exe_artifact), children = child_deps), + ), ] def csharp_binary_impl(ctx: AnalysisContext) -> list[Provider]: exe_name = "{}.exe".format(ctx.attrs.name) if not ctx.attrs.exe_name else ctx.attrs.exe_name - dotNetLibraryInfo = _csharp_library_or_exe(ctx, exe_name, "exe") + library_or_exe_artifact, _ = _csharp_library_or_exe_artifact(ctx, exe_name, "exe") return [ - DefaultInfo(default_output = dotNetLibraryInfo.object), - RunInfo(args = cmd_args(dotNetLibraryInfo.object)), + DefaultInfo(default_output = library_or_exe_artifact), + RunInfo(args = cmd_args(library_or_exe_artifact)), ] def prebuilt_dotnet_library_impl(ctx: AnalysisContext) -> list[Provider]: From 4b237e00f63d5bd3017077fbf71e59881d1e9004 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:02 +0100 Subject: [PATCH 06/20] Fixed buck2 run by including runtime dependencies --- prelude/csharp/csharp.bzl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/prelude/csharp/csharp.bzl b/prelude/csharp/csharp.bzl index b70b1bbed9740..fef897d804d22 100644 --- a/prelude/csharp/csharp.bzl +++ b/prelude/csharp/csharp.bzl @@ -82,11 +82,15 @@ def csharp_library_impl(ctx: AnalysisContext) -> list[Provider]: def csharp_binary_impl(ctx: AnalysisContext) -> list[Provider]: exe_name = "{}.exe".format(ctx.attrs.name) if not ctx.attrs.exe_name else ctx.attrs.exe_name - library_or_exe_artifact, _ = _csharp_library_or_exe_artifact(ctx, exe_name, "exe") + library_or_exe_artifact, child_deps = _csharp_library_or_exe_artifact(ctx, exe_name, "exe") + + new_runtime_files = [] + for child in child_deps: + new_runtime_files.extend([ctx.actions.symlink_file(dll_dep.reference.basename, dll_dep.reference) for dll_dep in child.traverse()]) return [ - DefaultInfo(default_output = library_or_exe_artifact), - RunInfo(args = cmd_args(library_or_exe_artifact)), + DefaultInfo(default_output = library_or_exe_artifact, other_outputs = new_runtime_files), + RunInfo(args = cmd_args(library_or_exe_artifact, hidden = new_runtime_files)), ] def prebuilt_dotnet_library_impl(ctx: AnalysisContext) -> list[Provider]: From 54d4c1217577786b8b40c12dd8f8cfc7c5f234e4 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:03 +0100 Subject: [PATCH 07/20] Fixed dotnet_common.srcs_arg --- prelude/decls/dotnet_common.bzl | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/prelude/decls/dotnet_common.bzl b/prelude/decls/dotnet_common.bzl index 5e186065079f9..ac4cbde7b7116 100644 --- a/prelude/decls/dotnet_common.bzl +++ b/prelude/decls/dotnet_common.bzl @@ -15,15 +15,9 @@ FrameworkVersion = ["net35", "net40", "net45", "net46"] def _srcs_arg(): return { - "srcs": attrs.list(attrs.one_of(attrs.source(), attrs.tuple(attrs.source(), attrs.list(attrs.arg()))), default = [], doc = """ - The set of C, C++, Objective-C, Objective-C++, or assembly source files - to be preprocessed, compiled, and assembled by this - rule. We determine which stages to run on each input source based on its file extension. See the - [GCC documentation](https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html) for more detail on how file extensions are interpreted. Each element can be either a string - specifying a source file (e.g. `''`) or a tuple of - a string specifying a source file and a list of compilation flags - (e.g. `('', ['-Wall', '-Werror'])` ). In the latter case the specified flags will be used in addition to the rule's other - flags when preprocessing and compiling that file (if applicable). + "srcs": attrs.list(attrs.source(), default = [], doc = """ + The set of C# source files to be compiled, and assembled by this rule. + Each element must a string specifying a source file. """), } From 8ab8c8f1d1d3d5e55ef0893e356ba5517d3598e2 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:03 +0100 Subject: [PATCH 08/20] Created common _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES --- prelude/decls/dotnet_common.bzl | 54 --------------------------------- prelude/decls/dotnet_rules.bzl | 42 +++++++++++++++++-------- 2 files changed, 29 insertions(+), 67 deletions(-) diff --git a/prelude/decls/dotnet_common.bzl b/prelude/decls/dotnet_common.bzl index ac4cbde7b7116..55a1f4d6786e8 100644 --- a/prelude/decls/dotnet_common.bzl +++ b/prelude/decls/dotnet_common.bzl @@ -12,57 +12,3 @@ # well-formatted (and then delete this TODO) FrameworkVersion = ["net35", "net40", "net45", "net46"] - -def _srcs_arg(): - return { - "srcs": attrs.list(attrs.source(), default = [], doc = """ - The set of C# source files to be compiled, and assembled by this rule. - Each element must a string specifying a source file. -"""), - } - -def _resources_arg(): - return { - "resources": attrs.dict(key = attrs.string(), value = attrs.source(), sorted = False, default = {}, doc = """ - Resources that should be embedded within the built DLL. The format - is the name of the resource once mapped into the DLL as the key, and - the value being the resource that should be merged. This allows - non-unique keys to be identified quickly. -"""), - } - -def _framework_ver_arg(): - return { - "framework_ver": attrs.enum(FrameworkVersion, doc = """ - The version of the .Net framework that this library targets. This is - one of 'net35', 'net40', 'net45' and 'net46'. -"""), - } - -def _deps_arg(): - return { - "deps": attrs.list(attrs.one_of(attrs.dep(), attrs.string()), default = [], doc = """ - The set of targets or system-provided assemblies to rely on. Any - values that are targets must be either csharp\\_library or `prebuilt_dotnet_library` - instances. -"""), - } - -def _compiler_flags_arg(): - return { - "compiler_flags": attrs.list(attrs.string(), default = [], doc = """ - The set of additional compiler flags to pass to the compiler. -"""), - } - -def _add_hermetic_arguments_arg(): - return {"add_hermetic_arguments": attrs.bool(default = True)} - -dotnet_common = struct( - srcs_arg = _srcs_arg, - resources_arg = _resources_arg, - framework_ver_arg = _framework_ver_arg, - deps_arg = _deps_arg, - compiler_flags_arg = _compiler_flags_arg, - add_hermetic_arguments_arg = _add_hermetic_arguments_arg, -) diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index 2baaf070066d4..c497be71e9b02 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -12,7 +12,33 @@ # well-formatted (and then delete this TODO) load(":common.bzl", "buck", "prelude_rule") -load(":dotnet_common.bzl", "dotnet_common") +load(":dotnet_common.bzl", "FrameworkVersion") + +_CSHARP_LIBRARY_OR_EXE_ATTRIBUTES = { + "srcs": attrs.list(attrs.source(), default = [], doc = """ + The set of C# source files to be compiled, and assembled by this rule. + Each element must a string specifying a source file. + """), + "resources": attrs.dict(key = attrs.string(), value = attrs.source(), sorted = False, default = {}, doc = """ + Resources that should be embedded within the built DLL. The format + is the name of the resource once mapped into the DLL as the key, and + the value being the resource that should be merged. This allows + non-unique keys to be identified quickly. + """), + "framework_ver": attrs.enum(FrameworkVersion, doc = """ + The version of the .Net framework that this library targets. This is + one of 'net35', 'net40', 'net45' and 'net46'. + """), + "deps": attrs.list(attrs.one_of(attrs.dep(), attrs.string()), default = [], doc = """ + The set of targets or system-provided assemblies to rely on. Any + values that are targets must be either csharp\\_library or `prebuilt_dotnet_library` + instances. + """), + "compiler_flags": attrs.list(attrs.string(), default = [], doc = """ + The set of additional compiler flags to pass to the compiler. + """), + "add_hermetic_arguments": attrs.bool(default = True), +} csharp_library = prelude_rule( name = "csharp_library", @@ -58,12 +84,7 @@ csharp_library = prelude_rule( the short name of the target. """), } - | dotnet_common.srcs_arg() - | dotnet_common.resources_arg() - | dotnet_common.framework_ver_arg() - | dotnet_common.deps_arg() - | dotnet_common.compiler_flags_arg() - | dotnet_common.add_hermetic_arguments_arg() + | _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES | buck.licenses_arg() | buck.labels_arg() | buck.contacts_arg() @@ -115,12 +136,7 @@ csharp_binary = prelude_rule( the short name of the target. """), } | - dotnet_common.srcs_arg() | - dotnet_common.resources_arg() | - dotnet_common.framework_ver_arg() | - dotnet_common.deps_arg() | - dotnet_common.compiler_flags_arg() | - dotnet_common.add_hermetic_arguments_arg() | + _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES | buck.licenses_arg() | buck.labels_arg() | buck.contacts_arg() From 40603948039838f935bcf3c8aa99c541b16b160b Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:03 +0100 Subject: [PATCH 09/20] Small code improvement in cmd_args usage --- prelude/toolchains/msvc/tools.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prelude/toolchains/msvc/tools.bzl b/prelude/toolchains/msvc/tools.bzl index a587942174f11..a17cfe5690d2e 100644 --- a/prelude/toolchains/msvc/tools.bzl +++ b/prelude/toolchains/msvc/tools.bzl @@ -183,7 +183,7 @@ def _find_roslyn_tools_impl(ctx: AnalysisContext) -> list[Provider]: cmd = [ ctx.attrs.vswhere[RunInfo], - cmd_args("--csc=", csc_exe_json.as_output(), delimiter = ""), + cmd_args(csc_exe_json.as_output(), format="--csc={}"), ] ctx.actions.run( From d58d5014e745ddfd6fb5e8eac14d4b7cc94b5b2e Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:03 +0100 Subject: [PATCH 10/20] Using or rather than ternary conditional operator --- prelude/toolchains/csharp.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prelude/toolchains/csharp.bzl b/prelude/toolchains/csharp.bzl index 594bae0f79ab0..a0923ac415469 100644 --- a/prelude/toolchains/csharp.bzl +++ b/prelude/toolchains/csharp.bzl @@ -12,7 +12,7 @@ def _system_csharp_toolchain_impl(ctx): if not host_info().os.is_windows: fail("csharp toolchain only supported on windows for now") - csc = ctx.attrs._csharp_tools_info[CSharpToolchainInfo].csc if ctx.attrs.csc == None else ctx.attrs.csc + csc = ctx.attrs.csc or ctx.attrs._csharp_tools_info[CSharpToolchainInfo].csc return [ DefaultInfo(), From 69df7d59bba1bf2c4087fcc2f41a1ebfcba8bb05 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:04 +0100 Subject: [PATCH 11/20] Fixed unused variable in some cases --- prelude/toolchains/msvc/tools.bzl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prelude/toolchains/msvc/tools.bzl b/prelude/toolchains/msvc/tools.bzl index a17cfe5690d2e..23139fdef1fa4 100644 --- a/prelude/toolchains/msvc/tools.bzl +++ b/prelude/toolchains/msvc/tools.bzl @@ -192,10 +192,10 @@ def _find_roslyn_tools_impl(ctx: AnalysisContext) -> list[Provider]: local_only = True, ) - run_msvc_tool = ctx.attrs.run_msvc_tool[RunInfo] if ctx.attrs.use_path_compilers: csc_exe_script = "csc.exe" else: + run_msvc_tool = ctx.attrs.run_msvc_tool[RunInfo] csc_exe_script = cmd_script( actions = ctx.actions, name = "csc", From e83525e49b99bb213cd4da89815b83a23e5f4559 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:21:04 +0100 Subject: [PATCH 12/20] Removed reference to integration tests that do not exist --- prelude/decls/dotnet_rules.bzl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index c497be71e9b02..48898e34f97d3 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -47,8 +47,6 @@ csharp_library = prelude_rule( and dependencies by invoking csc. """, examples = """ - For more examples, check out our [integration tests](https://github.com/facebook/buck/tree/dev/test/com/facebook/buck/rust/testdata/). - ``` csharp_library( name = 'simple', @@ -98,9 +96,6 @@ csharp_binary = prelude_rule( and dependencies by invoking csc. """, examples = """ - For more examples, check out our [integration tests](https://github.com/facebook/buck/tree/dev/test/com/facebook/buck/rust/testdata/). - - ``` csharp_binary( From 215f6499d2653920c3621dac5a9aa7fc79f973eb Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:51:25 +0100 Subject: [PATCH 13/20] Added typing information to vswhere.py --- prelude/toolchains/msvc/vswhere.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/prelude/toolchains/msvc/vswhere.py b/prelude/toolchains/msvc/vswhere.py index 6347ee23fc3b6..3e57ab497dcd3 100644 --- a/prelude/toolchains/msvc/vswhere.py +++ b/prelude/toolchains/msvc/vswhere.py @@ -19,7 +19,7 @@ import tempfile import winreg from pathlib import Path -from typing import IO, NamedTuple +from typing import IO, Any, NamedTuple VC_EXE_NAMES = ["cl.exe", "cvtres.exe", "lib.exe", "ml64.exe", "link.exe"] UCRT_EXE_NAMES = ["rc.exe"] @@ -43,7 +43,7 @@ class Tool(NamedTuple): INCLUDE: list[Path] = [] -def find_in_path(executable, is_optional=False): +def find_in_path(executable: str, is_optional: bool = False) -> Tool | None: which = shutil.which(executable) if which is None: if is_optional: @@ -54,7 +54,7 @@ def find_in_path(executable, is_optional=False): return Tool(which) -def run_vswhere(required_component): +def run_vswhere(required_component: str) -> list[dict[str, Any]]: program_files = os.environ.get("ProgramFiles(x86)") if program_files is None: program_files = os.environ.get("ProgramFiles") @@ -96,7 +96,7 @@ def run_vswhere(required_component): return vswhere_json -def find_msvc_with_vswhere_exe(): +def find_msvc_with_vswhere_exe() -> list[Tool | None]: vswhere_json = run_vswhere("Microsoft.VisualStudio.Component.VC.Tools.x86.x64") for vs_instance in list(vswhere_json): @@ -160,7 +160,7 @@ def find_msvc_with_vswhere_exe(): sys.exit(1) -def find_roslyn_with_vswhere_exe(): +def find_roslyn_with_vswhere_exe() -> Tool: vswhere_json = run_vswhere("Microsoft.VisualStudio.Component.Roslyn.Compiler") for vs_instance in list(vswhere_json): @@ -192,7 +192,7 @@ def find_roslyn_with_vswhere_exe(): # so that's good enough for us. # # Returns a pair of (root, version) for the ucrt dir if found. -def get_ucrt_dir(): +def get_ucrt_dir() -> tuple[Path, str | None]: registry = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) key_name = "SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots" registry_key = winreg.OpenKey(registry, key_name) @@ -220,7 +220,7 @@ def get_ucrt_dir(): # sdk version if one is already configured. # # Returns a pair of (root, version). -def get_sdk10_dir(): +def get_sdk10_dir() -> tuple[Path, str | None]: windows_sdk_dir = os.environ.get("WindowsSdkDir") windows_sdk_version = os.environ.get("WindowsSDKVersion") if windows_sdk_dir is not None and windows_sdk_version is not None: @@ -245,7 +245,7 @@ def get_sdk10_dir(): return installation_folder, max_version -def write_tool_json(out, tool): +def write_tool_json(out: IO[str], tool: Tool) -> None: j = json.dumps( tool._asdict(), indent=4, @@ -255,7 +255,7 @@ def write_tool_json(out, tool): # for use with the ewdk to grab the environment strings -def get_ewdk_env(ewdkdir: Path): +def get_ewdk_env(ewdkdir: Path) -> dict[str, str]: """ Inspiration taken from the following: http://pythonwise.blogspot.fr/2010/04/sourcing-shell-script.html (Miki Tebeka) @@ -283,7 +283,7 @@ def get_ewdk_env(ewdkdir: Path): return env -def find_with_ewdk(ewdkdir: Path): +def find_with_ewdk(ewdkdir: Path) -> list[Tool | None]: env = get_ewdk_env(ewdkdir) installation_path = Path(env["VSINSTALLDIR"]) @@ -329,7 +329,7 @@ def find_with_ewdk(ewdkdir: Path): ] -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--cl", type=argparse.FileType("w")) parser.add_argument("--cvtres", type=argparse.FileType("w")) From fd7b7043e089f03fae7c89872de83b2f7651c08a Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:51:46 +0100 Subject: [PATCH 14/20] Added from __future__ import annotations to improve compatibility with older Python versions --- prelude/toolchains/msvc/vswhere.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prelude/toolchains/msvc/vswhere.py b/prelude/toolchains/msvc/vswhere.py index 3e57ab497dcd3..e8baf30f76a89 100644 --- a/prelude/toolchains/msvc/vswhere.py +++ b/prelude/toolchains/msvc/vswhere.py @@ -10,6 +10,8 @@ # Translated from the Rust `cc` crate's windows_registry.rs. # https://github.com/rust-lang/cc-rs/blob/1.0.79/src/windows_registry.rs +from __future__ import annotations + import argparse import json import os From 7a745b05e26c3fa1483d75c3c5f45925209621b2 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:52:09 +0100 Subject: [PATCH 15/20] Fixed possible bug in code where Nones were passed to write_tool_json --- prelude/toolchains/msvc/vswhere.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/prelude/toolchains/msvc/vswhere.py b/prelude/toolchains/msvc/vswhere.py index e8baf30f76a89..2cc0458d9b138 100644 --- a/prelude/toolchains/msvc/vswhere.py +++ b/prelude/toolchains/msvc/vswhere.py @@ -135,8 +135,8 @@ def find_msvc_with_vswhere_exe() -> list[Tool | None]: LIB.append(ucrt / "lib" / ucrt_version / "ucrt" / "x64") INCLUDE.append(ucrt / "include" / ucrt_version / "ucrt") - ucrt_exe_paths = [ucrt_bin_path / exe for exe in UCRT_EXE_NAMES] - ucrt_exe_paths = [exe if exe.exists() else None for exe in ucrt_exe_paths] + ucrt_exe_paths_not_checked = [ucrt_bin_path / exe for exe in UCRT_EXE_NAMES] + ucrt_exe_paths = [exe if exe.exists() else None for exe in ucrt_exe_paths_not_checked] else: ucrt_exe_paths = [None for exe in UCRT_EXE_NAMES] @@ -150,7 +150,7 @@ def find_msvc_with_vswhere_exe() -> list[Tool | None]: INCLUDE.append(sdk / "include" / sdk_version / "shared") return [ - Tool(exe=exe, LIB=LIB, PATH=PATH, INCLUDE=INCLUDE) + None if exe == None else Tool(exe=exe, LIB=LIB, PATH=PATH, INCLUDE=INCLUDE) for exe in vc_exe_paths + ucrt_exe_paths ] @@ -310,8 +310,8 @@ def find_with_ewdk(ewdkdir: Path) -> list[Tool | None]: LIB.append(ucrt / "lib" / ucrt_version / "ucrt" / "x64") INCLUDE.append(ucrt / "include" / ucrt_version / "ucrt") - ucrt_exe_paths = [ucrt_bin_path / exe for exe in UCRT_EXE_NAMES] - ucrt_exe_paths = [exe if exe.exists() else None for exe in ucrt_exe_paths] + ucrt_exe_paths_not_checked = [ucrt_bin_path / exe for exe in UCRT_EXE_NAMES] + ucrt_exe_paths = [exe if exe.exists() else None for exe in ucrt_exe_paths_not_checked] else: ucrt_exe_paths = [None for exe in UCRT_EXE_NAMES] @@ -326,7 +326,7 @@ def find_with_ewdk(ewdkdir: Path) -> list[Tool | None]: INCLUDE.append(sdk / "include" / sdk_version / "shared") return [ - Tool(exe=bin_path / exe, LIB=LIB, PATH=PATH, INCLUDE=INCLUDE) + None if exe == None else Tool(exe=bin_path / exe, LIB=LIB, PATH=PATH, INCLUDE=INCLUDE) for exe in vc_exe_paths + ucrt_exe_paths ] @@ -358,12 +358,18 @@ def main() -> None: find_msvc_with_vswhere_exe() ) - write_tool_json(output.cl, cl_exe) - write_tool_json(output.cvtres, cvtres_exe) - write_tool_json(output.lib, lib_exe) - write_tool_json(output.ml64, ml64_exe) - write_tool_json(output.link, link_exe) - write_tool_json(output.rc, rc_exe) + if output.cl and cl_exe: + write_tool_json(output.cl, cl_exe) + if output.cvtres and cvtres_exe: + write_tool_json(output.cvtres, cvtres_exe) + if output.lib and lib_exe: + write_tool_json(output.lib, lib_exe) + if output.ml64 and ml64_exe: + write_tool_json(output.ml64, ml64_exe) + if output.link and link_exe: + write_tool_json(output.link, link_exe) + if output.rc and rc_exe: + write_tool_json(output.rc, rc_exe) if output.csc: csc_exe = find_roslyn_with_vswhere_exe() From f5496e621856445e04bd4915c32e0547726e8d0d Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:57:32 +0100 Subject: [PATCH 16/20] Fixed error reported by mypy. prelude\toolchains\msvc\vswhere.py:56: error: Argument 1 to "Tool" has incompatible type "str"; expected "Path" [arg-type] --- prelude/toolchains/msvc/vswhere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prelude/toolchains/msvc/vswhere.py b/prelude/toolchains/msvc/vswhere.py index 2cc0458d9b138..88a330f46ba63 100644 --- a/prelude/toolchains/msvc/vswhere.py +++ b/prelude/toolchains/msvc/vswhere.py @@ -53,7 +53,7 @@ def find_in_path(executable: str, is_optional: bool = False) -> Tool | None: else: print(f"{executable} not found in $PATH", file=sys.stderr) sys.exit(1) - return Tool(which) + return Tool(Path(which)) def run_vswhere(required_component: str) -> list[dict[str, Any]]: From 9864ad330e55f3c67e2691f8e758e1ce1701908c Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:10:11 +0100 Subject: [PATCH 17/20] Fixed another mypy issue prelude\toolchains\msvc\vswhere.py:277: error: Argument 1 to "check_output" has incompatible type "list[object]"; expected "str | bytes | PathLike[str] | PathLike[bytes] | Sequence[str | bytes | PathLike[str] | PathLike[bytes]]" [arg-type] --- prelude/toolchains/msvc/vswhere.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prelude/toolchains/msvc/vswhere.py b/prelude/toolchains/msvc/vswhere.py index 88a330f46ba63..95f9ba1f00573 100644 --- a/prelude/toolchains/msvc/vswhere.py +++ b/prelude/toolchains/msvc/vswhere.py @@ -273,7 +273,7 @@ def get_ewdk_env(ewdkdir: Path) -> dict[str, str]: print("set", file=tmp) env_script = ewdkdir / "BuildEnv" / "SetupBuildEnv.cmd" - cmd = [tmp.name, env_script, "amd64"] + cmd = [tmp.name, str(env_script), "amd64"] output = subprocess.check_output(cmd).decode("utf-8") env = {} From af5764991df8f0788b37ae94df940bef4e2b197f Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:23:01 +0100 Subject: [PATCH 18/20] Some updates to the C# rules' docs --- prelude/decls/dotnet_rules.bzl | 31 +++++++++++++++++-------------- prelude/toolchains/csharp.bzl | 2 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index 48898e34f97d3..e180a3f4fc794 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -13,11 +13,12 @@ load(":common.bzl", "buck", "prelude_rule") load(":dotnet_common.bzl", "FrameworkVersion") +load("@prelude//csharp:csharp_providers.bzl", "DotNetLibraryInfo") _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES = { "srcs": attrs.list(attrs.source(), default = [], doc = """ The set of C# source files to be compiled, and assembled by this rule. - Each element must a string specifying a source file. + Each element may be either a literal string (representing the path within this package), or a target. """), "resources": attrs.dict(key = attrs.string(), value = attrs.source(), sorted = False, default = {}, doc = """ Resources that should be embedded within the built DLL. The format @@ -27,17 +28,21 @@ _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES = { """), "framework_ver": attrs.enum(FrameworkVersion, doc = """ The version of the .Net framework that this library targets. This is - one of 'net35', 'net40', 'net45' and 'net46'. + one of """ + ", ".join(FrameworkVersion) + """. """), - "deps": attrs.list(attrs.one_of(attrs.dep(), attrs.string()), default = [], doc = """ - The set of targets or system-provided assemblies to rely on. Any + "deps": attrs.list(attrs.one_of(attrs.dep(providers = [DotNetLibraryInfo]), attrs.string()), default = [], doc = """ + The set of targets or system-provided assemblies this target depends on. Any values that are targets must be either csharp\\_library or `prebuilt_dotnet_library` instances. """), - "compiler_flags": attrs.list(attrs.string(), default = [], doc = """ - The set of additional compiler flags to pass to the compiler. + "compiler_flags": attrs.list(attrs.arg(), default = [], doc = """ + The set of additional compiler flags. + """), + "add_hermetic_arguments": attrs.bool(default = True, doc = """ + If true, the following arguments are passed to the compiler: "/noconfig", "/nostdlib" and "/nosdkpath". + These attributes prevent loading of default assemblies by the compiler so that they can be explicitly + controlled in Buck2. """), - "add_hermetic_arguments": attrs.bool(default = True), } csharp_library = prelude_rule( @@ -47,7 +52,7 @@ csharp_library = prelude_rule( and dependencies by invoking csc. """, examples = """ - ``` + ```python csharp_library( name = 'simple', dll_name = 'Cake.dll', @@ -96,8 +101,7 @@ csharp_binary = prelude_rule( and dependencies by invoking csc. """, examples = """ - ``` - + ```python csharp_binary( name = 'simple', exe_name = 'Cake.exe', @@ -118,7 +122,6 @@ csharp_binary = prelude_rule( name = 'other', assembly = 'other-1.0.dll', ) - ``` """, further = None, @@ -126,8 +129,8 @@ csharp_binary = prelude_rule( # @unsorted-dict-items { "exe_name": attrs.string(default = "", doc = """ - The output name of the dll. This allows you to specify the name of - the dll exactly. When this is not set, the dll will be named after + The output name of the executable. This allows you to specify the name of + the executable exactly. When this is not set, the executable will be named after the short name of the target. """), } | @@ -145,7 +148,7 @@ prebuilt_dotnet_library = prelude_rule( prebuilt .Net assembles into your .Net code. """, examples = """ - ``` + ```python prebuilt_dotnet_library( name = 'log4net', assembly = 'log4net.dll', diff --git a/prelude/toolchains/csharp.bzl b/prelude/toolchains/csharp.bzl index a0923ac415469..52d5a508b49ae 100644 --- a/prelude/toolchains/csharp.bzl +++ b/prelude/toolchains/csharp.bzl @@ -44,7 +44,7 @@ system_csharp_toolchain = rule( visibility = ["PUBLIC"], )""", attrs = { - "csc": attrs.option(attrs.string(), default = None, doc = "Executable name or a path to the C# compiler frequently referred to as csc.exe"), + "csc": attrs.option(attrs.string(), default = None, doc = "Executable name or a path to the C# compiler frequently referred to as csc.exe. If not provided, the csc compiler is inferred from the CSharpToolchainInfo provided in the _csharp_tools_info attribute."), "framework_dirs": attrs.dict( key = attrs.string(), value = attrs.one_of(attrs.source(), attrs.string()), From 9c8007a82318278504e3269e46f0b1f6d9d0c49a Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Sun, 7 Jun 2026 14:54:46 +0100 Subject: [PATCH 19/20] Created dictionary with common MSVC toolchain attributes --- prelude/toolchains/msvc/tools.bzl | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/prelude/toolchains/msvc/tools.bzl b/prelude/toolchains/msvc/tools.bzl index 23139fdef1fa4..1d8aa167cf366 100644 --- a/prelude/toolchains/msvc/tools.bzl +++ b/prelude/toolchains/msvc/tools.bzl @@ -217,22 +217,25 @@ def _find_roslyn_tools_impl(ctx: AnalysisContext) -> list[Provider]: ), ] +_MSVC_TOOL_ATTRIBUTES = { + "run_msvc_tool": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:run_msvc_tool")), + "vswhere": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:vswhere")), +} + find_msvc_tools = rule( impl = _find_msvc_tools_impl, attrs = { "linker_wrapper": attrs.default_only(attrs.exec_dep(providers = [RunInfo], default = "prelude//cxx/tools:linker_wrapper")), - "run_msvc_tool": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:run_msvc_tool")), "use_path_compilers": attrs.bool(default = False), "use_path_linkers": attrs.bool(default = False), - "vswhere": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:vswhere")), - }, + } + | _MSVC_TOOL_ATTRIBUTES, ) find_roslyn_tools = rule( impl = _find_roslyn_tools_impl, attrs = { - "run_msvc_tool": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:run_msvc_tool")), "use_path_compilers": attrs.bool(default = False), - "vswhere": attrs.default_only(attrs.dep(providers = [RunInfo], default = "prelude//toolchains/msvc:vswhere")), - }, + } + | _MSVC_TOOL_ATTRIBUTES, ) From 567466fbeb1a93f4437278654c61d4fe0ab01509 Mon Sep 17 00:00:00 2001 From: Overhatted <15021741+Overhatted@users.noreply.github.com> Date: Sun, 7 Jun 2026 16:10:02 +0100 Subject: [PATCH 20/20] Removed resources attribute since it was not implemented --- prelude/decls/dotnet_rules.bzl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/prelude/decls/dotnet_rules.bzl b/prelude/decls/dotnet_rules.bzl index e180a3f4fc794..327b5317c6779 100644 --- a/prelude/decls/dotnet_rules.bzl +++ b/prelude/decls/dotnet_rules.bzl @@ -20,12 +20,6 @@ _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES = { The set of C# source files to be compiled, and assembled by this rule. Each element may be either a literal string (representing the path within this package), or a target. """), - "resources": attrs.dict(key = attrs.string(), value = attrs.source(), sorted = False, default = {}, doc = """ - Resources that should be embedded within the built DLL. The format - is the name of the resource once mapped into the DLL as the key, and - the value being the resource that should be merged. This allows - non-unique keys to be identified quickly. - """), "framework_ver": attrs.enum(FrameworkVersion, doc = """ The version of the .Net framework that this library targets. This is one of """ + ", ".join(FrameworkVersion) + """. @@ -60,9 +54,6 @@ csharp_library = prelude_rule( srcs = [ 'Hello.cs', ], - resources = { - 'greeting.txt': '//some:target', - }, deps=[ ':other', 'System.dll', @@ -109,9 +100,6 @@ csharp_binary = prelude_rule( srcs = [ 'Hello.cs', ], - resources = { - 'greeting.txt': '//some:target', - }, deps=[ ':other', 'System.dll',