Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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"]
125 changes: 78 additions & 47 deletions prelude/decls/dotnet_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,33 @@
# well-formatted (and then delete this TODO)

load(":common.bzl", "buck", "prelude_rule")
load(":dotnet_common.bzl", "FrameworkVersion")

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 must a string specifying a source file.

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.

"must a string specifying a source file" is wrong. You're missing "be", and it isn't a string specifying a source file in all instances...

attrs.source entries can be target names, too. Imagine you have a script that generates a source file from some typing information (like an *.idl file). This would be a reference to an output of that target. https://buck2.build/docs/api/build/attrs/#source

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.

Fixed, basically copied the sentences from that documentation link

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

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.

What does "once mapped" mean?
What does it mean for a resource to be "merged"?

"This allows non-unique keys to be identified quickly"... this is pertinent to the operation of the end binary rule, but it doesn't mean much to the user. You might instead say:

"The X rule merges the resource attributes of all of it's transitive dependencies. Duplicated key/value pairs are ignored, while duplicate keys with inequal values are errors"... or whatever the rules do so the user can reason about it.

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've removed the resources attribute since it has not been implemented yet.

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

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.

You don't want to have to keep this list up to date. Either generate the string here from FrameworkVersion, or just say "one of the values from the FrameworkVersion global". The latter is easier, the former is nicer for the user.

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've generated the list from 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
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.string(), default = [], doc = """
The set of additional compiler flags to pass to the compiler.

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.

This should be an attrs.list(attrs.arg()). I wouldn't include the "to pass to the compiler"... what else would you do with 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.

Done

"""),
"add_hermetic_arguments": attrs.bool(default = True),

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.

Needs docs. What are the "hermetic arguments", or why would I want this?

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.

And this one it looks like you added, so I do want you to write the docs for it.

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've added documentation for this argument

}

csharp_library = prelude_rule(
name = "csharp_library",
Expand All @@ -22,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',
Expand Down Expand Up @@ -57,57 +80,64 @@ 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 = """
```

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.

I think you can do "```python" here to get python syntax highlighting in the generated docs

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.

Done


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.

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.

"the dll filename will be the target's name attribute, suffixed with...X". I think X depends on whether it's an exe or dll? Or will this always be dll?

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.

Thanks for noticing, I had just copied that from csharp_library, I've fixed it now by replacing dll with executable.

"""),
} |
_CSHARP_LIBRARY_OR_EXE_ATTRIBUTES |
buck.licenses_arg() |
buck.labels_arg() |
buck.contacts_arg()
),
)

prebuilt_dotnet_library = prelude_rule(
name = "prebuilt_dotnet_library",
docs = """
Expand Down Expand Up @@ -152,5 +182,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"),
"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