Skip to content

Instantly share code, notes, and snippets.

@rrbutani
Last active June 2, 2026 20:36
Show Gist options
  • Select an option

  • Save rrbutani/7ff6a5dce491351bd6e685ddbcd060b1 to your computer and use it in GitHub Desktop.

Select an option

Save rrbutani/7ff6a5dce491351bd6e685ddbcd060b1 to your computer and use it in GitHub Desktop.
'''Module extension machinery that fetches NuGet packages, given a
`packages.lock.json` file (NuGet lock file).
Intended for use with `nuget_repo` in `rules_dotnet`.
The machinery here makes use of the NuGet v3 API to fill in information not
present in the lock file (i.e. dependency edges, actual nupkg hashes,
information about tools) and makes use of the module extension facts API for
speed (i.e. so that subsequent module extension evaluations for the same
dependencies do not need to hit the NuGet APIs again).
See:
- https://devblogs.microsoft.com/dotnet/enable-repeatable-package-restores-using-a-lock-file/
- https://registry.bazel.build/docs/rules_dotnet/0.21.5#function-nuget_repo
- https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/dotnet/private/rules/nuget/nuget_repo.bzl
- https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/tools/paket2bazel/Models.fs
- https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/examples/paket/deps/paket.main.bzl
'''
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@xml.bzl", "xml")
#-------------------------------------------------------------------------------
_RequestedDepKind = struct(
direct = "direct",
transitive = "transitive",
)
_RequestedDepInfo = provider(fields = dict(
name = "string",
version = "string",
kind = "_RequestedDepKind",
))
# NOTE: previously (?) `packages.lock.json` appeared to contain dependency edge
# information but no longer? not sure; see, for example:
# - https://github.com/DamirsCorner/20220708-dotnet-nuget-lock/blob/9d708fdcfce84bfb5877d12b204a7e80319572a5/GenericHostConsoleApp/packages.lock.json
#
# for now, going to always just fetch this info from the NuGet API
def _dep_list_from_lock_file(mctx, packages_lock_json):
# type: (module_ctx, path | Label) -> list[_RequestedDepInfo]
lock_file = json.decode(mctx.read(packages_lock_json))
if not lock_file["version"] == 1:
fail("unsupported packages.lock.json version:", lock_file["version"])
# NOTE: not keeping TFM (target framework moniker) information around for
# now
dep_map = dict() # (name, version) -> kind
for _tfm, deps in lock_file["dependencies"].items():
for name, info in deps.items():
raw_ty = info["type"]
if raw_ty == "Project":
continue # skip project references; these are not external deps
ty = {
"Direct": _RequestedDepKind.direct,
"Transitive": _RequestedDepKind.transitive,
}.get(raw_ty, None)
if not ty:
fail("unsupported dep type for dep", name + ":", info)
ver = info["resolved"]
key = (name, ver)
if key in dep_map:
if dep_map[key] != ty: fail(
"inconsistent dep kinds for dep", name + ":", dep_map[key],
"and", ty,
)
else:
dep_map[key] = ty
return [
_RequestedDepInfo(name = name, version = ver, kind = ty)
for (name, ver), ty in dep_map.items()
]
def _dep_list_from_pairs(pairs):
# type: (list[tuple[string, string]]) -> list[_RequestedDepInfo]
return [
_RequestedDepInfo(
name = name, version = ver, kind = _RequestedDepKind.direct
)
for name, ver in pairs
]
def _dep_list_merge(*dep_lists, strict = True):
# type: (list[_RequestedDepInfo], ...) -> list[_RequestedDepInfo]
dep_map = dict() # (name, version) -> kind
for dep_list in dep_lists:
for dep in dep_list:
key = (dep.name, dep.version)
if key in dep_map:
if dep_map[key] != dep.kind and strict: fail(
"inconsistent dep kinds for dep", dep.name + ":",
dep_map[key], "and", dep.kind,
)
else:
dep_map[key] = dep.kind
return [
_RequestedDepInfo(name = name, version = ver, kind = ty)
for (name, ver), ty in dep_map.items()
]
def _dep_list_serialize(dep_list):
# type: (list[_RequestedDepInfo]) -> dict[_RequestedDepKind, list[tuple[string, ...]]]
# useful for storing in `Facts`
#
# NOTE: serialize/deserialize do not preserve ordering; you are encouraged
# to do a serialize/deserialize round trip in your non-cached codepath for
# consistency
res = dict()
for dep in dep_list:
res.setdefault(dep.kind, []).append((dep.name, dep.version))
return res
def _dep_list_deserialize(serialized_dep_list):
# type: (dict[_RequestedDepKind, list[tuple[string, ...]]]) -> list[_RequestedDepInfo]
return [
_RequestedDepInfo(name = name, version = ver, kind = kind)
for kind, pairs in serialized_dep_list.items()
for name, ver in pairs
]
dep_list = struct(
from_lock_file = _dep_list_from_lock_file,
from_pairs = _dep_list_from_pairs,
merge = _dep_list_merge,
serialize = _dep_list_serialize,
deserialize = _dep_list_deserialize,
)
#-------------------------------------------------------------------------------
# see:
# - https://github.com/bazel-contrib/rules_dotnet/blob/16ac6a16e1f199f64d3131f792f8900f44e47bed/tools/paket2bazel/Paket.fs#L15-L57
# - https://learn.microsoft.com/en-us/nuget/reference/target-frameworks
_known_tfms = dict(
# divided into groups; map from our names to aliases that nuget uses
framework = {
"net11": [".NETFramework1.1"],
"net20": [".NETFramework2.0"],
"net30": [".NETFramework3.0"],
"net35": [".NETFramework3.5"],
"net40": [".NETFramework4.0"],
"net403": [".NETFramework4.0.3"],
"net45": [".NETFramework4.5"],
"net451": [".NETFramework4.5.1"],
"net452": [".NETFramework4.5.2"],
"net46": [".NETFramework4.6"],
"net461": [".NETFramework4.6.1"],
"net462": [".NETFramework4.6.2"],
"net47": [".NETFramework4.7"],
"net471": [".NETFramework4.7.1"],
"net472": [".NETFramework4.7.2"],
"net48": [".NETFramework4.8"],
},
standard = {
"netstandard": [],
"netstandard1.0": [".NETStandard1.0"],
"netstandard1.1": [".NETStandard1.1"],
"netstandard1.2": [".NETStandard1.2"],
"netstandard1.3": [".NETStandard1.3"],
"netstandard1.4": [".NETStandard1.4"],
"netstandard1.5": [".NETStandard1.5"],
"netstandard1.6": [".NETStandard1.6"],
"netstandard2.0": [".NETStandard2.0"],
"netstandard2.1": [".NETStandard2.1"],
},
core = {
"netcoreapp1.0": [".NETCoreApp1.0"],
"netcoreapp1.1": [".NETCoreApp1.1"],
"netcoreapp2.0": [".NETCoreApp2.0"],
"netcoreapp2.1": [".NETCoreApp2.1"],
"netcoreapp2.2": [".NETCoreApp2.2"],
"netcoreapp3.0": [".NETCoreApp3.0"],
"netcoreapp3.1": [".NETCoreApp3.1"],
"net5.0": [".NETCore5.0"],
"net6.0": [],
"net7.0": [],
"net8.0": [],
"net9.0": [],
"net10.0": [],
},
)
_known_tfms_set = set([
tfm for group in _known_tfms.values() for tfm in group.keys()
])
_alias_to_tfm_map = {
alias: tfm
for group in _known_tfms.values()
for tfm, aliases in group.items()
for alias in [tfm] + aliases
}
_tfm_ignore_prefixes = [
# prefixes of TFMs/aliases that we know to ignore
"MonoAndroid",
"MonoTouch",
".NETPortable", # deprecated in favor of `.NETStandard`
"Xamarin.iOS",
"Xamarin.Mac",
"Xamarin.TVOS",
"Xamarin.WatchOS",
"WindowsPhone",
"Windows8.0",
".NETFramework4.6.3", # ???
"UAP10.0",
"UAP10.1",
".NETCoreApp0.0",
]
# designed to map somewhat closely to the `rules_dotnet` data model:
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/tools/paket2bazel/Models.fs
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/examples/paket/deps/paket.main.bzl
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/dotnet/private/rules/nuget/nuget_repo.bzl
_DepInfo = provider(fields = dict(
name = "string",
version = "string",
sha512 = "string, base64",
# note: unlike in `paket2bazel`, this map is sparse; to keep `Facts` small,
# we do not fill in deps for all supported TFMs
#
# instead we interpolate when requested (see `deps.for_nuget_repo`)
dependencies_per_tfm = "dict[tfm string, set[package name string]]",
tools = "dict[tfm str, list[tuple[name str, entrypoint (dll) str, runner str]]]",
# NOTE: not supporting (for now):
# group (mostly just a `paket` concept?)
# sources (hardcoded to `"https://www.nuget.org/api/v2"`)
# overrides (`targeting_pack_overrides`)
# frameworkList (`framework_list`)
))
def _process_pkg_dependency_group(group):
if group["@type"] != "PackageDependencyGroup": fail(group)
deps = group.get("dependencies", [])
for dep in deps:
if dep["@type"] != "PackageDependency": fail(group, dep)
return list(set([ dep["id"] for dep in deps ]))
def _resolve_deps(mctx, dep_list, *, facts = {}):
# type: (module_ctx, list[_RequestedDepInfo], Facts | dict[string, ...]) -> tuple[dict[_RequestedDepInfo, _DepInfo], dict[string, ...]]
new_facts = dict()
results = dict()
progress = lambda dep, msg: mctx.report_progress(
"{} for {} {}".format(msg, dep.name, dep.version)
)
def _dep_info_serialize_to_fact(dep_info_dict):
# type: (dict[string, ...]) -> dict[string, ...]
return {
k: v
for k, v in dep_info_dict.items()
# drop `name` and `version` (contained in the fact key)
if k not in ["name", "version"]
# drop `tools` if empty
if not (k == "tools" and v == {})
# drop `dependencies_per_tfm` if empty
if not (k == "dependencies_per_tfm" and v == {})
}
def _dep_info_deserialize_from_fact(fact, dep):
# type: (dict[string, ...], _RequestedDepInfo) -> _DepInfo
extras = dict(name = dep.name, version = dep.version)
if "tools" not in fact:
extras["tools"] = {}
if "dependencies_per_tfm" not in fact:
extras["dependencies_per_tfm"] = {}
return _DepInfo(**(fact | extras))
def _mk_fact_key(dep):
# type: (string, string) -> string
return "_nuget_dep_resolve_v1--{}--{}".format(dep.name, dep.version)
def set_result(dep_info_dict, dep, fact_key):
# type: (dict[string, ...], _RequestedDepInfo, string) -> None
# debug assert: `_mk_fact_key(dep) == fact_key`
new_facts[fact_key] = _dep_info_serialize_to_fact(dep_info_dict)
results[dep] = _DepInfo(**dep_info_dict)
# four passes over the deps in `dep_list`:
# 1. check `fact`s for result (`_DepInfo`), otherwise download reg info
# 2. get catalog URL for dep, kick off download
# 3. process downloaded catalog + produce/store `_DepInfo`
# + if contains tools, queue download of `.nupkg`
# 4. search downloaded `.nupkg` for tool info, produce/store `_DepInfo`
# 1: check `facts`; otherwise download registration info
reg_info_downloads = dict()
for dep in set(dep_list): # NOTE: `set` to dedupe
fact_key = _mk_fact_key(dep)
if fact_key in facts:
new_facts[fact_key] = facts[fact_key]
results[dep] = _dep_info_deserialize_from_fact(facts[fact_key], dep)
else:
url = "https://api.nuget.org/v3/registration5-gz-semver2/{}/{}.json".format(
dep.name.lower(), dep.version.lower()
)
out_path = fact_key + ".registration.json"
reg_info_downloads[dep] = (
fact_key, out_path,
# NOTE: `Accept-Encoding` is hardcoded to `gzip`:
# - https://github.com/bazelbuild/bazel/blob/2b8885ed954e58d09d47290d47882a7298c88334/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java#L55-L57
# - https://github.com/bazelbuild/bazel/blob/2b8885ed954e58d09d47290d47882a7298c88334/src/main/java/com/google/devtools/build/lib/bazel/repository/downloader/HttpConnectorMultiplexer.java#L109-L112
mctx.download(output = out_path, url = url, block = False),
)
# 2: download catalogs
pkg_catalog_downloads = dict()
for dep, (fact_key, reg_info_file, t) in reg_info_downloads.items():
progress(dep, "downloading NuGet registration info")
t.wait()
reg_info = json.decode(mctx.read(reg_info_file))
if not "catalogEntry" in reg_info:
fail("invalid registration response for dep", dep, "—", reg_info)
catalog_url = reg_info["catalogEntry"]
out_path = fact_key + ".catalog.json"
pkg_catalog_downloads[dep] = (
fact_key, reg_info, out_path,
mctx.download(output = out_path, url = catalog_url, block = False),
)
# 3: process catalogs; download `.nupkg`s` for deps w/tools
tool_pkg_downloads = dict()
for dep, (fact_key, reg_info, cat_file, t) in pkg_catalog_downloads.items():
progress(dep, "downloading NuGet catalog entry")
t.wait()
progress(dep, "processing dependency information")
cat = json.decode(mctx.read(cat_file))
# extract/check hash:
if cat["packageHashAlgorithm"] != "SHA512":
fail("unsupported package hash algorithm for dep", dep, "—", cat)
sha512_b64_hash = cat["packageHash"]
# extract dependencies:
dep_groups = cat.get("dependencyGroups", [])
if any([ "targetFramework" not in g for g in dep_groups ]):
# we support dependencyGroups w/no `targetFramework` and interpret
# this as meaning the deps listed apply to all TFMs
#
# if such a dependency group exists it must be the only group:
if len(dep_groups) != 1: fail(
"error in dependency groups for dep", dep,
"— a dependency group with no `targetFramework` is present but",
"there are multiple dependency groups present:",
json.encode_indent(dep_groups, prefix="\t\t"),
)
deps = _process_pkg_dependency_group(dep_groups[0])
# record as if the deps listed were specified for the lowest TFM
# in each group; this way `_as_packages_for_nuget_repo` will specify
# these deps for all TFMs
dependencies_per_tfm = {
# NOTE: relying on dict iteration order being insertion order
tfm_group.keys()[0]: deps
for tfm_group in _known_tfms_set.values()
}
else:
dependencies_per_tfm = {}
for group in dep_groups:
tfm = _alias_to_tfm_map.get(group["targetFramework"], None)
if not tfm:
tf = group["targetFramework"]
if any([tf.startswith(p) for p in _tfm_ignore_prefixes]):
continue
print(
"warning: unrecognized `targetFramework` in dependency",
"group for dep", dep, "—", tf,
"\nskipping this dependency group:",
json.encode_indent(group, prefix="\t\t"),
)
continue
if tfm in dependencies_per_tfm: fail(
"error processing dependencies for", dep,
"— `targetFramework`s for multiple dependency groups",
"resolve to the same TFM (`{}`)".format(tfm),
"\n\n2nd group is:",
json.encode_indent(group, prefix="\t\t"),
)
dependencies_per_tfm[tfm] = _process_pkg_dependency_group(group)
dep_info = dict(
name = dep.name,
version = dep.version,
sha512 = sha512_b64_hash,
dependencies_per_tfm = dependencies_per_tfm,
tools = {}, # see below
)
# check if `dep` has tools:
if any([
ty["name"] == "DotnetTool" for ty in cat.get("packageTypes", [])
]):
# we're dealing with a tool; need to download the nupkg and look for
# `DotnetToolSettings.xml` files
nupkg_url = reg_info["packageContent"]
out_path = fact_key + ".nupkg"
tool_pkg_downloads[dep] = (
fact_key, cat, dep_info, out_path,
mctx.download(
output = out_path, url = nupkg_url, block = False,
integrity = "sha512-" + sha512_b64_hash,
),
)
else:
# otherwise, we're done! we have all the information we need for
# this package
set_result(dep_info, dep, fact_key)
# 4: process downloaded `.nupkg`s for tool info
for dep, (fact_key, cat, dep_info, nupkg, t) in tool_pkg_downloads.items():
progress(dep, "downloading NuGet package")
t.wait()
progress(dep, "extracting NuGet package")
out_path = fact_key + ".nupkg_extracted"
mctx.extract(nupkg, output=out_path, type="nupkg")
progress(dep, "scanning NuGet package for tools")
# look in `cat["packageEntries"]` for `name` == `DotnetToolSettings.xml`
#
# see:
# - https://github.com/bazelbuild/rules_dotnet/blob/2c171dd622650f85a66c32e1afddf07329494d53/tools/paket2bazel/Paket.fs#L152-L214
# - https://github.com/dotnet/sdk/blob/3fa6eb6eb5d14adb090cbcb00a998046cd2f4ff4/documentation/general/tool-nuget-package-format.md
tool_setting_files = [
entry["fullName"] for entry in cat["packageEntries"]
if entry["name"] == "DotnetToolSettings.xml"
if paths.starts_with(entry["fullName"], "tools")
]
if len(tool_setting_files) != 1: fail(
"error in dependency with tools", dep,
"— expected exactly one `DotnetToolSettings.xml` file but found: ",
tool_setting_files,
)
# must have path of the form: `tools/{tfm}/{rid}/DotnetToolSettings.xml`
tool_setting_file = tool_setting_files[0]
path_parts = paths.normalize(tool_setting_file).split("/")
if len(path_parts) != 4: fail(
"error in dependency with tools", dep,
"— expected `DotnetToolSettings.xml` file to have path of the form",
"`tools/{tfm}/{rid}/DotnetToolSettings.xml` but got:",
tool_setting_file,
)
_tools, tfm_, rid, _name = path_parts
if (_tools, _name) != ("tools", "DotnetToolSettings.xml"):
fail(path_parts)
if rid != "any": fail(
"error in dependency with tools", dep,
"expected `rid` to be `any` but got:", rid,
)
tfm = _alias_to_tfm_map.get(tfm_, None)
if not tfm: fail(
"error in dependency with tools", dep,
"— unrecognized TFM in path of `DotnetToolSettings.xml` file:",
tfm_,
)
# read `DotnetToolSettings.xml` file to get `Command`s:
tool_settings_path = mctx.path(out_path).get_child(tool_setting_file)
tool_settings_str = mctx.read(tool_settings_path)
tool_settings = xml.parse(tool_settings_str)
root = xml.get_document_element(tool_settings)
if xml.get_tag_name(root) != "DotNetCliTool": fail(
"format error in", tool_settings_str
)
if xml.get_attribute(root, "Version") != "1": fail(
"expected `Version` == '1'", tool_settings_str
)
tools = []
for child in xml.get_child_elements(root):
if xml.get_tag_name(child) != "Commands": continue
for child in xml.get_child_elements(child):
if xml.get_tag_name(child) != "Command": continue
tools.append((
xml.get_attribute(child, "Name"),
xml.get_attribute(child, "EntryPoint"),
xml.get_attribute(child, "Runner"),
))
# and, we're done:
dep_info["tools"][tfm] = tools
set_result(dep_info, dep, fact_key)
return results, new_facts
# produces a dict describing the given dep, suitable for use as part of the
# `packages` argument to `nuget_repo`:
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/examples/paket/deps/paket.main.bzl
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/dotnet/private/rules/nuget/nuget_repo.bzl
#
# if `limit_deps` is specified, `dep`'s dependencies are filtered to only
# include packages in `limit_deps`
def _dep_to_pkg(dep, *, limit_deps = None, warn = True, warning_cache = set()):
# type: (_DepInfo, ..., set[string]) -> dict[string, string]
if limit_deps != None:
limit_deps = set([ d.lower() for d in limit_deps ])
# NOTE: `rules_dotnet`'s `nuget_repo` machinery wants the list of
# dependencies for each TFM whereas `_DepInfo` (like NuGet) only lists
# dependencies for some TFMs
#
# `paket2bazel` "fills in" the dependencies for TFMs not explicitly listed
# in the NuGet metadata by using the dependency list for the nearest lower
# TFM, making use of `FrameworkReducer` in NuGet to make this determination:
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/tools/paket2bazel/Paket.fs#L15-L57
# - https://github.com/NuGet/NuGet.Client/blob/0781854111129d4c502fa44849fac974b46cc1f9/src/NuGet.Core/NuGet.Frameworks/FrameworkReducer.cs#L45-L192
#
# here we do... an incomplete, likely bug-ridden approximation of that:
dependencies = { tfm: set() for tfm in _known_tfms_set }
# for each group of TFMs, walk from lowest to highest, looking for a
# matching dependency list in `dep`; failing that, uses the last found list
# for the group
for tfms_in_group in _known_tfms.values():
last_match = set()
for tfm in tfms_in_group:
if tfm in dep.dependencies_per_tfm:
last_match = dep.dependencies_per_tfm[tfm]
dependencies[tfm] = last_match
if limit_deps != None:
prev = dependencies[tfm]
dependencies[tfm] = set([
d for d in dependencies[tfm]
if d.lower() in limit_deps
])
if warn:
for dropped in set(prev) - dependencies[tfm]:
if dropped in warning_cache:
continue
warning_cache.add(dropped)
print(
"warning: dropping missing dependency for dep",
"{} {}:".format(dep.name, dep.version), dropped,
)
return dict(
name = dep.name,
id = dep.name,
version = dep.version,
sha512 = "sha512-" + dep.sha512,
sources = ["https://www.nuget.org/api/v2"], # NOTE: hardcoded
dependencies = dependencies,
targeting_pack_overrides = [], # NOTE: not implemented
framework_list = [], # NOTE: not implemented
tools = {
tfm: [
dict(name = name, entrypoint = entrypoint, runner = runner)
for name, entrypoint, runner in tool_infos
]
for tfm, tool_infos in dep.tools.items()
},
)
# produces a list of dicts describing the given list of deps, suitable for use
# as the `packages` argument to `nuget_repo`:
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/examples/paket/deps/paket.main.bzl
# - https://github.com/bazel-contrib/rules_dotnet/blob/v0.21.5/dotnet/private/rules/nuget/nuget_repo.bzl
def _as_packages_for_nuget_repo(dep_info_list, *, warn = False):
# type: (list[_DepInfo]) -> list[dict[string, string]]
limit = set([ dep.name for dep in dep_info_list ])
cache = set()
return [
_dep_to_pkg(info, limit_deps=limit, warn=warn, warning_cache = cache)
for info in dep_info_list
]
deps = struct(
resolve = _resolve_deps,
as_packages_for_nuget_repo = _as_packages_for_nuget_repo,
)
#-------------------------------------------------------------------------------
# NOTE: ideally we could download + deserialize JSON without having to spill
# to disk but alas
# TODO: in package.lock.json parse, preserve direct deps and framework version
# - and then create a repo with a `select` that you can `load` and use as your
# library/binary's deps attribute?
# - actually nah, not going to bother with this; you should specify this
# manually because:
# + we can't distinguish between dev deps, tool deps, and regular deps
# + we don't have information about what project a dep was used for so we
# would be giving you the union of all the direct deps across all the
# projects; this is over-declared (can hurt build parallelism)
# * EDIT: this is not quite true; we likely do have enough information
# + the repetition (needing to duplicate the direct deps in `.csproj` and
# `BUILD.bazel` files) grates a little bit but for our use case it's a
# non-issue at this point
# TODO(testing): test against nuget + paket2bazel
@rrbutani

rrbutani commented Jun 2, 2026

Copy link
Copy Markdown
Author

@rrbutani

rrbutani commented Jun 2, 2026

Copy link
Copy Markdown
Author

example usage:

def _example_impl(mctx):
    deps1 = dep_list.from_lock_file(mctx, Label("@//:packages.lock.json"))
    deps2 = dep_list.from_pairs(mctx, [("dotnet-t4", "3.0.0")]) # to add extras
    requested_deps = dep_list.merge(deps1, deps2)

    dep_map, new_facts = deps.resolve(mctx, requested_deps, facts = mctx.facts)
    packages = deps.as_packages_for_nuget_repo(dep_map.values())

    nuget_repo(name = "nuget_deps", packages = packages)
    return mctx.extension_metadata(
        # NOTE: in reality, get name from tag; consider `dev_dependency`, etc
        root_module_direct_deps = ["nuget_deps"],
        reproducible = True,
        facts = new_facts,
    )

example = module_extension(implementation = _example_impl)

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