Last active
June 2, 2026 20:36
-
-
Save rrbutani/7ff6a5dce491351bd6e685ddbcd060b1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| '''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 | |
Author
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
x-ref: