Skip to content

Improved C# support#1258

Open
Overhatted wants to merge 17 commits intofacebook:mainfrom
Overhatted:csharp_support
Open

Improved C# support#1258
Overhatted wants to merge 17 commits intofacebook:mainfrom
Overhatted:csharp_support

Conversation

@Overhatted
Copy link
Copy Markdown
Contributor

@Overhatted Overhatted commented Feb 25, 2026

Most importantly:

  • Added ability for the csc.exe compiler to be found automatically using vswhere (like it is done for C++ on Windows)
  • Added csharp_binary rule to compile binaries

I used the following example project to test these changes:
https://github.com/Overhatted/Buck2_CSharp_Example

There are two examples/tests there:

  • buck2 run :main is a csharp_binary with a 3rd party dependency fetched from nuget.org and the csc.exe compiler found "automatically" using vswhere. It should print '{"Name":"Apple","Expiry":"2008-12-28T00:00:00","Price":3.99,"Sizes":["Small","Medium","Large"]}'. It tests that the runtime dependencies also work because newtonsoft is in a separate DLL.
  • buck2 run :main_generator_user tests that code generation using csharp_binary also works

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 25, 2026
@meta-codesync
Copy link
Copy Markdown
Contributor

meta-codesync Bot commented Feb 25, 2026

@facebook-github-bot has imported this pull request. If you are a Meta employee, you can view this in D94396824. (Because this pull request was imported automatically, there will not be any future comments.)

@Overhatted Overhatted marked this pull request as ready for review February 25, 2026 19:48
@Overhatted
Copy link
Copy Markdown
Contributor Author

Hello @KapJI. You looked at some of my PRs in the past, can you take a look at this one?

@Overhatted
Copy link
Copy Markdown
Contributor Author

Hello @jtbraun, sorry about the random ping. Can you look at this PR?

Copy link
Copy Markdown
Contributor

@jtbraun jtbraun left a comment

Choose a reason for hiding this comment

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

I wouldn't say this review's comprehensive. My biggest concerns are the auto-finding of tools, and how it exposes you to different configs across different machines. It wasn't clear to me what providing the roslyn tools vs a csc directly on the toolchain gives you, who wins, why?

Comment thread prelude/csharp/csharp.bzl Outdated
# Run the C# compiler to produce the output artifact.
ctx.actions.run(cmd, category = "csharp_compile")

return DotNetLibraryInfo(
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.

It's not really a DotNetLibraryInfo if we're producing an "exe" anymore. Unless you can use the "exe" output ANYWHERE you could use the "library" output, this should be a DotNetExecutableInfo or something.

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.

This should be fixed now. See the comment below.

Comment thread prelude/csharp/csharp.bzl Outdated

return [
DefaultInfo(default_output = dotNetLibraryInfo.object),
RunInfo(args = cmd_args(dotNetLibraryInfo.object)),
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 suppose if you NEVER export the dotNetLibraryInfo from here, it's sortof okay (see dotNetExecutableInfo` above). Still, I think they represent two different things, and should be separate. Is there precedence elsewhere in the prelude for this sort of thing?

I'd expect if you can use exectuables where you can use libraries, but not the other way around, that executables would actually expose both providers, in addition to the DefaultInfo and RunInfo here.

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 had used DotNetLibraryInfo just to make it easier to implement but I've now changed the code so the common code only returns an Artifact and a list of DllDepTSet and the DotNetLibraryInfo is only created in the csharp_library.
I don't think you can use executables where you can use libraries.

Comment thread prelude/decls/dotnet_common.bzl Outdated
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
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.

Does this indenting pattern match other attribute values (like cxx_common) in the prelude already?

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 the indentation is different everywhere :). I've just updated the indentation to match the one in _RUST_EXECUTABLE_ATTRIBUTES. Let me know if you'd like a different one.

Comment thread prelude/decls/dotnet_common.bzl Outdated
Comment on lines +35 to +36
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 do you mean by "identified quickly"? Who is doing said identification, and what is the response? As far as the rule is concerned, there won't be duplicated keys, by definition of this being a dict.

Are these "string resources" or "non-string resources": https://learn.microsoft.com/en-us/dotnet/core/extensions/create-resource-files#resources-in-text-files

As a user, I always appreciate details about how this attribute maps to .net tool concepts.

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 see now that this was just moved from below. Fine.

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.

Yes, I don't know what resources are, it was just moved code.

Comment thread prelude/decls/dotnet_common.bzl Outdated
Comment on lines +43 to +44
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.

I'm not 100% familiar with .net. Are these canonical strings that the .Net runtime goes by, or just "common shorthand"?

AH, I see, much of this was moved...

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.

They are defined in dotnet_common so no, I don't think they are canonical strings used by the .net runtime

Comment on lines +182 to +183
if ctx.attrs.use_path_compilers:
csc_exe_script = "csc.exe"
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 sort of thing is a bad idea. You have no control now over who has what csc.exe installed, what version, etc. I wouldn't do this. Is there precedent?

Comment thread prelude/toolchains/msvc/vswhere.py Outdated


def find_with_vswhere_exe():
def run_vswhere(required_component):
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.

Typing :string on the argument. Typing everwhere really...

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 typing. I'm not sure that's a great idea because it adds minimum Python version requirements. But the script already had the type annotations list[Path] which I think require Python 3.9 and, with from __future__ import annotations hopefully it still works with Python 3.9. But I'm very far from an expert in Python type annotations.

)
sys.exit(1)

vswhere_exe = (
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.

Though this does set precedent for using the "one from path". I'd still avoid that, and require (if you can) that we still discover csc through there vswhere/whatever tools, and (optionally) pull THOSE from PATH.

Also keep in mind that this runs in the context of the daemon, NOT the client. So PATH may be from some preexisting client command in another terminal with a different starting environment. I don't believe we do a lot of sanitizing of the environment, so depending on it is risky.

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.

Ideally, since you added vswhere on the toolchain, we'd pass that to this and call the vswhere on the toolchain, not just "whatever random one we can find".

Comment thread prelude/toolchains/csharp.bzl Outdated
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
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.

csc = ctx.attrs.csc or thing.from.toolchain works 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.

Done

Comment on lines +47 to +49
"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"),
"_csharp_tools_info": attrs.exec_dep(providers = [CSharpToolchainInfo], default = "prelude//toolchains/msvc:roslyn_tools"),
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'm not sure how to interpret roslyn_tools being provided by default here AND the csc being separate. Which should be used, when, why?

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 fixed all the other issues and the only one remaining is this one. Which is the same as the comment #1258 (comment) and #1258 (comment).

The behavior should be the same as the one in C++. As for usage, in my own usage (I only use the C++ rules currently because the C# ones are not implemented yet but the behavior should be the same) I use the below toolchains:

def is_remote_enabled() -> bool:
    re_enabled = read_config("buck2_re_client", "enabled", "false")
    return re_enabled == "true"

find_msvc_tools(
    name = "msvc_tools",
    use_path_compilers = is_remote_enabled(),
    use_path_linkers = is_remote_enabled() and not host_info().os.is_windows,
    target_compatible_with = ["config//os:windows"],
    visibility = ["PUBLIC"],
)

path_clang_tools(
    name = "clang_tools",
    target_compatible_with = ["config//os:linux"],
    visibility = ["PUBLIC"],
)

cxx_tools_info_toolchain(
    cxx_tools_info = select({
        "config//os:windows": ":msvc_tools",
        "config//os:linux": ":clang_tools",
    }),
    ...

Basically the PATH tool lookup is used on RE and local Linux (where g++ and the other tools are in the path by default) and the find_msvc_tools for the local execution to work.

How would you setup the toolchains? My main goal was that buck2 build :main worked every machine, with or without RE, without the developer having to launch any special command line.

@jtbraun
Copy link
Copy Markdown
Contributor

jtbraun commented Apr 7, 2026

Hello @jtbraun, sorry about the random ping. Can you look at this PR?

I'll also note that I'm not at all familiar with our csharp support, csharp in general, or who is using it inside or outside the company. Github's diff didn't make it very clear what lines were moved vs created, so I may have commented on things that have been around for a long, long time.

I'm also a little concerned about backcompat, assuming that there are users today of these rules, we don't want to break them.

@Overhatted
Copy link
Copy Markdown
Contributor Author

Overhatted commented Apr 10, 2026

Thanks a lot for the review.

I'll also note that I'm not at all familiar with our csharp support, csharp in general, or who is using it inside or outside the company. Github's diff didn't make it very clear what lines were moved vs created, so I may have commented on things that have been around for a long, long time.

Here https://buck2.build/docs/about/language_support/ it says C# is "Unavailable". So I don't think there are many users of it.

I'm also a little concerned about backcompat, assuming that there are users today of these rules, we don't want to break them.

Either way, I think it is reasonably backwards compatible. The only change that is not is that if the user has a csc.exe in the PATH that is not the same as the one reported by vswhere.py, it will start using the vswhere.py one. But I think this change is desirable because: C# is not really working at the moment so I doubt it is used much, this makes it so it works without any special PATH configuration and bring it in line with what the C++ rules do. Either way, if the user really wants to use the csc.exe in the path they can add the "csc" = "csc.exe" attribute so it uses the csc.exe in the PATH.

I've responded to every comment (hopefully) @jtbraun. Let me know what you think and if I've missed any comment.

@Overhatted
Copy link
Copy Markdown
Contributor Author

Hello @jtbraun, can you take a look at my changes again?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants