Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
dab0f7a
Added find_roslyn_tools target that searches for Visual Studio instal…
Overhatted Dec 10, 2025
c2e8367
Added possiblity of not adding /noconfig and similar arguments in C#
Overhatted Dec 10, 2025
55f33ef
Moved some .net things into dotnet_common
Overhatted Feb 25, 2026
3abbbcd
Added csharp_binary rule
Overhatted Feb 25, 2026
7754471
Refactored code so a DotNetLibraryInfo is only built for a library bu…
Overhatted Apr 10, 2026
4b237e0
Fixed buck2 run by including runtime dependencies
Overhatted Apr 10, 2026
54d4c12
Fixed dotnet_common.srcs_arg
Overhatted Apr 10, 2026
8ab8c8f
Created common _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES
Overhatted Apr 10, 2026
4060394
Small code improvement in cmd_args usage
Overhatted Apr 10, 2026
d58d501
Using or rather than ternary conditional operator
Overhatted Apr 10, 2026
69df7d5
Fixed unused variable in some cases
Overhatted Apr 10, 2026
e83525e
Removed reference to integration tests that do not exist
Overhatted Apr 10, 2026
215f649
Added typing information to vswhere.py
Overhatted Apr 10, 2026
fd7b704
Added from __future__ import annotations to improve compatibility wit…
Overhatted Apr 10, 2026
7a745b0
Fixed possible bug in code where Nones were passed to write_tool_json
Overhatted Apr 10, 2026
f5496e6
Fixed error reported by mypy.
Overhatted Apr 10, 2026
9864ad3
Fixed another mypy issue
Overhatted Apr 10, 2026
af57649
Some updates to the C# rules' docs
Overhatted Jun 7, 2026
9c8007a
Created dictionary with common MSVC toolchain attributes
Overhatted Jun 7, 2026
567466f
Removed resources attribute since it was not implemented
Overhatted Jun 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 43 additions & 23 deletions prelude/csharp/csharp.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,39 @@
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_artifact(ctx: AnalysisContext, library_or_exe_name: str, target_type: str) -> (Artifact, list[DllDepTSet]):
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_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.
# 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_artifact.as_output(),
format = "/out:{}",
)
)

# 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")
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 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")
# 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.
Expand All @@ -64,15 +61,38 @@ 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 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

library_or_exe_artifact, child_deps = _csharp_library_or_exe_artifact(ctx, dll_name, "library")

return [
DefaultInfo(default_output = library),
DefaultInfo(default_output = library_or_exe_artifact),
DotNetLibraryInfo(
name = ctx.attrs.dll_name,
object = library,
dll_deps = ctx.actions.tset(DllDepTSet, value = DllReference(reference = library), children = child_deps),
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

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()])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a hazard here that can't be directly fixed. In a very large project, you may have MANY entries here, and the size of this can blow up. You do have to enumerate these at some point and create the symlinks, though.

Another approach is to farm out the symlink creation to a script/tool/whatever that would create all the symlinks for you, and instead of calling child.traverse() here, you'd give that script the result of a "projection" on the transitive set. In your implementation, new_runtime_files will be the size of the graph represented by the TSet. In the other approach, the projection is just a refernce to the TSet root that says "run the X projection", and the TSet is only walked when the action that runs the script is run. The python_binary rules do this sort of thing to avoid building these (huge) lists in prelude/python/python.bzl, in the args_projections. These are later used to build manifest files or symlink trees (like you're doing here) with the help of external commands that take the list of arguments as inputs in some form. The python rules used .traverse() at one time, consuming single-digit gigabytes of memory for each python_binary target.

This isn't functionally wrong, but anywhere you can avoid copying the contents of a TSet is an opportunity to not waste memory. Because artifacts need to be declared, doing this work with .symlink_file has to do this, I think. I don't believe there's a way currently to say "build up a bunch of symlinks here by walking this TSet", hence the external tools that take the list of symlinks as arguments in some form and create them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured the optimal way would be to avoid traverse but I didn't see a way of doing that. I guess using a separate tool would work but it seems harder to maintain rather than using Starlark's native symlink_file.
Could we merge this like this and if people start raising issues with this taking a lot of memory we can implement this external tool improvement? With any luck by that time there will be a better way of doing this.


return [
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]:
# Prebuilt libraries are just passed through since they are already built.
return [
Expand Down
14 changes: 14 additions & 0 deletions prelude/decls/dotnet_common.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 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"]
126 changes: 74 additions & 52 deletions prelude/decls/dotnet_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,32 @@
# well-formatted (and then delete this TODO)

load(":common.bzl", "buck", "prelude_rule")
load(":dotnet_common.bzl", "FrameworkVersion")
load("@prelude//csharp:csharp_providers.bzl", "DotNetLibraryInfo")

FrameworkVersion = ["net35", "net40", "net45", "net46"]
_CSHARP_LIBRARY_OR_EXE_ATTRIBUTES = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the comments below are almost all just improvements to what was already pre-existing. While we can fix it, we should. If you're not certain about this, when we go to merge it I can make edits, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've fixed all the issues except for the traverse. I've also left the resources implementation for a possible future PR.

"srcs": attrs.list(attrs.source(), default = [], doc = """
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.
"""),
"framework_ver": attrs.enum(FrameworkVersion, doc = """
The version of the .Net framework that this library targets. This is
one of """ + ", ".join(FrameworkVersion) + """.
"""),
"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.
"""),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"to rely on" => "this target depends on."

What does it mean for this to be an attrs.string()? That seems wrong.

If you depend on a particular provider being in here, your attrs.dep() should have the providers=[...] listed, presumably DotNetLibraryInfo?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attrs.string is for "system-provided assemblies" like System.IO.dll

"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.
"""),
}

csharp_library = prelude_rule(
name = "csharp_library",
Expand All @@ -22,19 +46,14 @@ 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/).

```
```python
csharp_library(
name = 'simple',
dll_name = 'Cake.dll',
framework_ver = 'net46',
srcs = [
'Hello.cs',
],
resources = {
'greeting.txt': '//some:target',
},
deps=[
':other',
'System.dll',
Expand All @@ -57,65 +76,67 @@ 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.
""",
),
"""),
}
| _CSHARP_LIBRARY_OR_EXE_ATTRIBUTES
| buck.licenses_arg()
| buck.labels_arg()
| buck.contacts_arg()
),
)

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 = """
```python
csharp_binary(
name = 'simple',
exe_name = 'Cake.exe',
framework_ver = 'net46',
srcs = [
'Hello.cs',
],
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 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.
"""),
} |
_CSHARP_LIBRARY_OR_EXE_ATTRIBUTES |
buck.licenses_arg() |
buck.labels_arg() |
buck.contacts_arg()
),
)

prebuilt_dotnet_library = prelude_rule(
name = "prebuilt_dotnet_library",
docs = """
A `prebuilt_dotnet_library()` rule is used to include
prebuilt .Net assembles into your .Net code.
""",
examples = """
```
```python
prebuilt_dotnet_library(
name = 'log4net',
assembly = 'log4net.dll',
Expand Down Expand Up @@ -152,5 +173,6 @@ prebuilt_dotnet_library = prelude_rule(

dotnet_rules = struct(
csharp_library = csharp_library,
csharp_binary = csharp_binary,
prebuilt_dotnet_library = prebuilt_dotnet_library,
)
6 changes: 5 additions & 1 deletion prelude/rules_impl.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = (
Expand Down
9 changes: 6 additions & 3 deletions prelude/toolchains/csharp.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.csc or ctx.attrs._csharp_tools_info[CSharpToolchainInfo].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",
Expand All @@ -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. 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()),
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,
)
8 changes: 7 additions & 1 deletion prelude/toolchains/msvc/BUCK
Original file line number Diff line number Diff line change
@@ -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")

Expand All @@ -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"],
)
Loading
Loading