bazelbuild / rules_dotnet

.NET rules for Bazel
Apache License 2.0
186 stars 81 forks source link

Support dotnet tools #337

Open Place1 opened 1 year ago

Place1 commented 1 year ago

I'd like to try and use rules_dotnet for an aspnetcore app that uses entity framework.

I can get the project to build but I'll need to be able to use the dotnet-ef cli tool for creating migrations.

Here's the command I need run; is it possible to create a bazel run target for this?

dotnet exec \
  --runtimeconfig MyApp.runtimeconfig.json \
  --depsfile MyApp.deps.json \
  --additionalprobingpath ~/.nuget/packages/ \
    bazel-myapp/external/nuget.dotnet-ef.v7.0.2/tools/net6.0/any/tools/netcoreapp2.0/any/ef.dll \
      migrations add \
        --assembly MyApp.dll \
        --startup-assembly MyApp.dll \
          my-migration-name

I was thinking to create a sh_binary target that depends on:

Currently I found that the nuget_archive rule doesn't expose any of the nuget package's tools/ folder so I can't access the ef.dll.

I also found that the :resolved_toolchain target didn't expose the dotnet runtime files which I submitted a PR for here: https://github.com/bazelbuild/rules_dotnet/pull/336 (this fixed allows me to use dotnet exec)

Does anyone know how to proceed here? I'm also wondering if anyone else uses rules_dotnet with any dotnet-tools today?

purkhusid commented 1 year ago

I've been meaning to add support for this at some point and I wanted to follow a similar approach to how rules_js does this. The docs for rules_js can be seen here: https://github.com/aspect-build/rules_js/tree/main/docs#using-binaries-published-to-npm

I haven't had the time to look at the implementation in rules_js yet so I haven't really fleshed out any details though. If you are interested in contributing this then I would recommend looking into how rules_js does this.

Place1 commented 1 year ago

Yeah that'd be great. I'm not skilled enough with bazel to figure it out unfortunately.

I think support for dotnet-tools alongside some support for generating a csproj for IDE intellisense (seperate topic) is all that's missing from rules_dotnet for my team to adopt it.

sin-ack commented 4 months ago

Hi, I would also really like to see this. The build system I'm working on at work requires running a .NET tool on the project before it can be built. Right now I'm trying to hackily implement it just to get it work for our particular case, using the package repository directly (@foo.bar.v1.2.3//:tools/net8.0/any/Foo.Bar.dll), but would love to see a proper implementation.

njlr commented 4 months ago

I will share my hacky work-around to run Fantomas (F# format checker) as a Bazel test:

in WORKSPACE:

http_archive(
  name = "fantomas",
  type = "zip",
  sha256 = "ddb7c3dd40d7b8892a2c16f0ac79a7b2bd1edd22099c356725a9cc92547ab188",
  urls = [ "https://www.nuget.org/api/v2/package/fantomas/5.0.0-beta-010" ],
  build_file = "@//:BUILD.fantomas",
)

BUILD.fantomas:

filegroup(
  name = "srcs",
  srcs = glob([ "**/*" ]),
  visibility = [ "//visibility:public" ],
)

fantomas.bzl:

def fantomas_check(name, editor_config, srcs, size = None):
  check = [
    native.package_name() + "/" + x for x in srcs
  ]

  native.sh_test(
    name = name,
    srcs = [ "@//:fantomas.sh" ],
    args = [
      "--check",
    ] + check,
    data = srcs + [
      editor_config,
      "//:fantomas",
    ],
    size = size,
  )

  return name

fantomas.sh:

#!/bin/bash

set -e

export HOME=$(mktemp -d || mktemp -d -t bazel-tmp)

trap "rm -rf $HOME" EXIT

export DOTNET_NOLOGO=1
export DOTNET_SKIP_FIRST_RUN_EXPERIENCE=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1

EXEC_ROOT=$(pwd)

if test "${BUILD_WORKING_DIRECTORY+x}"; then
  cd $BUILD_WORKING_DIRECTORY
fi

dotnet $EXEC_ROOT/external/fantomas/tools/net6.0/any/fantomas.dll ${@:1}

Usage:

load("//:fantomas.bzl", "fantomas_check")

fantomas_check(
  name = "fantomas",
  editor_config = "//:.editorconfig",
  srcs = glob([
    "*.fs",
  ]),
)

Note this uses the system dotnet so it's not truly hermetic. If anyone knows how to wire up the rules_dotnet toolchain, that would be a great improvement!

sin-ack commented 4 months ago

The way I'm currently hooking up the rules_dotnet toolchain is doing it the way rules_dotnet does it internally, i.e. creating a new rule and consuming @rules_dotnet//dotnet:toolchain_type. The toolchain will have toolchain.dotnetinfo.runtime, which is the dotnet command you can execute. Then I just feed that into a ctx.actions.run with the manual assembly path I set up. Obviously more work than your solution, but allows me to not have to put dotnet on the CI machine.

leryss commented 4 months ago

we implemented tool support by getting the dotnet binary out of the DotnetInfo provider of the toolchain and essentially doing a dotnet tool install .... we use the --tool-path argument for dotnet to create a local installation, the tool can be executed as long as there are some specific dotnet env variable set

this is a rule doing that:


def _dotnet_tool_binary_impl(ctx):
    info = ctx.attr._toolchain[DotnetInfo]
    dotnet_bin = info.runtime_path

    out_log = ctx.actions.declare_file(ctx.attr.name + ".log")
    out_store = ctx.actions.declare_directory(ctx.attr.name + "/.store")
    out_exec = ctx.actions.declare_file(ctx.attr.name + "/" + ctx.attr.tool_exec_name)
    out_run_exec = ctx.actions.declare_file(ctx.attr.name + "/run.sh")

    # See https://github.com/dotnet/sdk/issues/27761
    arch = ""
    if ctx.attr.is_darwin_arm64:
        arch = "--arch arm64 "

    install_cmd_parts = [
        "ROOTDIR=$(pwd)",
        "cd $(dirname {output})",
        "DOTNET_CLI_HOME=\"$ROOTDIR/$(dirname {dotnet_bin})\" \"$ROOTDIR/{dotnet_bin}\" tool install {package} {arch} --tool-path {tool_path} > $(basename {output})",
    ]

    install_cmd = ";".join(install_cmd_parts).format(
        dotnet_bin = dotnet_bin,
        package = ctx.attr.tool_install_name,
        arch = arch,
        tool_path = ctx.attr.name,
        output = out_log.path,
    )

    ctx.actions.run_shell(
        outputs = [out_log, out_store, out_exec],
        command = install_cmd,
        tools = info.runtime_files,
        toolchain = "@rules_dotnet//dotnet:resolved_toolchain",
        use_default_shell_env = True,
    )

    run_script = """#!/bin/bash
        DOTNETBIN=$(readlink \"{dotnet}\")
        cd $BUILD_WORKSPACE_DIRECTORY
        DOTNET_ROOT=$(dirname $DOTNETBIN) {tool_bin} "$@"
    """

    run_script = run_script.format(
        dotnet = dotnet_bin,
        tool_bin = out_exec.path,
    )

    ctx.actions.write(
        output = out_run_exec,
        content = run_script,
    )

    runfiles = ctx.runfiles(files = info.runtime_files + [out_exec, out_log, out_store])

    return [DefaultInfo(executable = out_run_exec, runfiles = runfiles, files = depset([out_exec, out_log, out_store]))]

_dotnet_tool_binary = rule(
    implementation = _dotnet_tool_binary_impl,
    executable = True,
    attrs = {
        "tool_exec_name": attr.string(),
        "tool_install_name": attr.string(),
        "is_darwin_arm64": attr.bool(mandatory = True),
        "_toolchain": attr.label(default = "@rules_dotnet//dotnet:resolved_toolchain"),
    },
)```
sin-ack commented 4 months ago

Cool solution, and very close to mine! The only difference for me was using the paket2bazel output for downloading the Nuget archive. That has its own downsides for the time being, of course.