diff --git a/core/MODULE.bazel b/core/MODULE.bazel index ce11d85de..320da3b2d 100644 --- a/core/MODULE.bazel +++ b/core/MODULE.bazel @@ -5,3 +5,12 @@ module( bazel_dep(name = "platforms", version = "0.0.4") bazel_dep(name = "bazel_skylib", version = "1.0.3") + +nix_repo = use_extension("//extensions:repository.bzl", "nix_repo") +use_repo(nix_repo, "nixpkgs_repositories") +nix_repo.override(name = "nixpkgs") +nix_repo.github( + name = "nixpkgs", + tag = "22.11", + sha256 = "ddc3428d9e1a381b7476750ac4dbea7a42885cbbe6e1af44b21d6447c9609a6f", +) diff --git a/core/extensions/BUILD.bazel b/core/extensions/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/core/extensions/package.bzl b/core/extensions/package.bzl new file mode 100644 index 000000000..32f8bc421 --- /dev/null +++ b/core/extensions/package.bzl @@ -0,0 +1,223 @@ +"""Defines the nix_pkg module extension. +""" + +load("//:nixpkgs.bzl", "nixpkgs_package") +load("//:util.bzl", "fail_on_err") +load("//private:module_registry.bzl", "registry") +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@nixpkgs_repositories//:defs.bzl", "nix_repo") + +_ACCESSOR = '''\ +def nix_pkg(module_name, name, label): + """Access a Nix package imported with `nix_pkg`. + + Args: + module_name: `String`; Name of the calling Bazel module. + This is needed until Bazel offers unique module identifiers, + see [#17652][bazel-17652]. + name: `String`; Name of the package. + label: `String`; Target within the package. + A string representation of a label under the package's external + workspace. + + Returns: + `Label`; The resolved label to the target within the package's external workspace. + + [bazel-17652]: https://github.com/bazelbuild/bazel/issues/17652 + """ + resolved = _fail_on_err( + _get_repository(module_name, name), + prefix = "Invalid Nix repository, you must use the nix_repo extension and request a global repository or register a local repository: ", + ) + return resolved.relative(label) +''' + +def _name_from_attr(attr): + """Generate a global workspace name from an attribute path. + """ + return attr + +def _attr_pkg(attr): + return partial.make( + nixpkgs_package, + attribute_path = attr.attr, + repository = nix_repo("rules_nixpkgs_core", "nixpkgs"), + ) + +def _local_attr_pkg(key, local_attr): + kwargs = {} + + if bool(local_attr.attr): + kwargs["attribute_path"] = local_attr.attr + else: + kwargs["attribute_path"] = local_attr.name + + repo_set = bool(local_attr.repo) + repos_set = bool(local_attr.repos) + + if repo_set and repos_set: + fail("Duplicate Nix repositories. Specify at most one of `repo` and `repos`.") + elif repo_set: + kwargs["repository"] = nix_repo(key, local_attr.repo) + elif repos_set: + kwargs["repositories"] = { + name: nix_repo(key, repo) + for name, repo in local_attr.repos.items() + } + else: + kwargs["repository"] = nix_repo(key, "nixpkgs") + + build_file_set = bool(local_attr.build_file) + build_file_content_set = bool(local_attr.build_file_content) + + if build_file_set and build_file_content_set: + fail("Duplicate BUILD file. Specify at most one of `build_file` and `build_file_contents`.") + elif build_file_set: + kwargs["build_file"] = local_attr.build_file + elif build_file_content_set: + kwargs["build_file"] = local_attr.build_file_content + + return partial.make( + nixpkgs_package, + **kwargs + ) + +def _nix_pkg_impl(module_ctx): + r = registry.make() + + for mod in module_ctx.modules: + key = fail_on_err(registry.add_module(r, name = mod.name, version = mod.version)) + + for attr in mod.tags.attr: + name = _name_from_attr(attr.attr) + fail_on_err( + registry.use_global_repo(r, key = key, name = name), + prefix = "Cannot use unified Nix package: ", + ) + if not registry.has_global_repo(r, name = name): + fail_on_err( + registry.add_global_repo( + r, + name = name, + repo = _attr_pkg(attr), + ), + prefix = "Cannot define unified Nix package: ", + ) + + for local_attr in mod.tags.local_attr: + fail_on_err( + registry.add_local_repo( + r, + key = key, + name = local_attr.name, + repo = _local_attr_pkg(key, local_attr), + ), + prefix = "Cannot use Nix package: ", + ) + + for repo_name, repo in registry.get_all_repositories(r).items(): + partial.call(repo, name = repo_name) + + fail_on_err( + registry.hub_repo(r, name = "nixpkgs_packages", accessor = _ACCESSOR), + prefix = "Failed to generate `nixpkgs_packages`: ", + ) + +_ATTR_ATTRS = { + "attr": attr.string( + doc = "The attribute path of the package to import.", + mandatory = True, + ), +} + +_COMMON_ATTRS = { + "name": attr.string( + doc = "A unique name for this package. The name must be unique within the requesting module.", + mandatory = True, + ), + "attr": attr.string( + doc = "The attribute path of the package to import. Defaults to `name`.", + mandatory = False, + ), +} + +_REPO_ATTRS = { + "repo": attr.string( + doc = """\ +The Nix repository to use. +Equivalent to `repos = {"nixpkgs": repo}`. +Specify at most one of `repo` or `repos`. +""", + mandatory = False, + ), + "repos": attr.string_dict( + doc = """\ +The Nix repositories to use. The dictionary keys represent the names of the +`NIX_PATH` entries. For example, `repositories = { "myrepo" : "somerepo" }` +would replace all instances of `` in the Nix code by the path to the +Nix repository `somerepo`. See the [relevant section in the nix +manual](https://nixos.org/nix/manual/#env-NIX_PATH) for more information. +Specify at most one of `repo` or `repos`. +""", + mandatory = False, + ), +} + +_BUILD_ATTRS = { + "build_file": attr.label( + doc = """\ +The file to use as the `BUILD` file for the external workspace generated for this package. + +Its contents are copied into the file `BUILD` in root of the nix output folder. The Label does not need to be named `BUILD`, but can be. + +For common use cases we provide filegroups that expose certain files as targets: + +
+
:bin
+
Everything in the bin/ directory.
+
:lib
+
All .so, .dylib and .a files that can be found in subdirectories of lib/.
+
:include
+
All .h, .hh, .hpp and .hxx files that can be found in subdirectories of include/.
+
+ +If you need different files from the nix package, you can reference them like this: +``` +package(default_visibility = [ "//visibility:public" ]) +filegroup( + name = "our-docs", + srcs = glob(["share/doc/ourpackage/**/*"]), +) +``` +See the bazel documentation of [`filegroup`](https://docs.bazel.build/versions/master/be/general.html#filegroup) and [`glob`](https://docs.bazel.build/versions/master/be/functions.html#glob). +Specify at most one of `build_file` or `build_file_content`. +""", + mandatory = False, + ), + "build_file_content": attr.string( + doc = """\ +Like `build_file`, but a string of the contents instead of a file name. +Specify at most one of `build_file` or `build_file_content`. +""", + mandatory = False, + ), +} + +_attr_tag = tag_class( + attrs = _ATTR_ATTRS, + doc = "Import a globally unified Nix package. If multiple Bazel modules import the same nixpkgs attribute, then they will all use the same external Bazel repository that imports the Nix package.", +) + +_local_attr_tag = tag_class( + attrs = dicts.add(_COMMON_ATTRS, _REPO_ATTRS, _BUILD_ATTRS), + doc = "Import a Nix package by attribute path.", +) + +nix_pkg = module_extension( + _nix_pkg_impl, + tag_classes = { + "attr": _attr_tag, + "local_attr": _local_attr_tag, + }, +) diff --git a/core/extensions/repository.bzl b/core/extensions/repository.bzl new file mode 100644 index 000000000..68fec6f66 --- /dev/null +++ b/core/extensions/repository.bzl @@ -0,0 +1,177 @@ +"""Defines the nix_repo module extension. +""" + +load("//:nixpkgs.bzl", "nixpkgs_http_repository") +load("//:util.bzl", "fail_on_err") +load("//private:module_registry.bzl", "registry") +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:partial.bzl", "partial") + +_ACCESSOR = '''\ +def nix_repo(module_name, name): + """Access a Nix repository imported with `nix_repo`. + + Args: + module_name: `String`; Name of the calling Bazel module. + This is needed until Bazel offers unique module identifiers, + see [#17652][bazel-17652]. + name: `String`; Name of the repository. + + Returns: + The resolved label to the repository's entry point. + + [bazel-17652]: https://github.com/bazelbuild/bazel/issues/17652 + """ + resolved = _fail_on_err( + _get_repository(module_name, name), + prefix = "Invalid Nix repository, you must use the nix_repo extension and request a global repository or register a local repository: ", + ) + return resolved +''' + +def _github_repo(github): + tag_set = bool(github.tag) + commit_set = bool(github.commit) + + if tag_set and commit_set: + fail("Duplicate Git revision. Specify only one of `tag` or `commit`.") + + if not tag_set and not commit_set: + fail("Missing Git revision. Specify one of `tag` or `commit`.") + + if tag_set: + archive = "refs/tags/%s.tar.gz" % github.tag + strip_prefix = "{}-{}".format(github.repo, github.tag) + else: + archive = "%s.tar.gz" % github.commit + strip_prefix = "{}-{}".format(github.repo, github.commit) + + url = "https://github.com/%s/%s/archive/%s" % (github.org, github.repo, archive) + + return partial.make( + nixpkgs_http_repository, + url = url, + integrity = github.integrity, + sha256 = github.sha256, + strip_prefix = strip_prefix, + ) + +def _nix_repo_impl(module_ctx): + r = registry.make() + + for mod in module_ctx.modules: + key = fail_on_err(registry.add_module(r, name = mod.name, version = mod.version)) + + for default in mod.tags.default: + fail_on_err( + registry.use_global_repo(r, key = key, name = default.name), + prefix = "Cannot use global default repository: ", + ) + + for github in mod.tags.github: + fail_on_err( + registry.add_local_repo( + r, + key = key, + name = github.name, + repo = _github_repo(github), + ), + prefix = "Cannot import GitHub repository: ", + ) + + for override in mod.tags.override: + prefix = "Cannot override global default repository: " + repo = fail_on_err( + registry.pop_local_repo(r, key = key, name = override.name), + prefix = prefix, + ) + registry.set_default_global_repo(r, name = override.name, repo = repo) + fail_on_err( + registry.use_global_repo(r, key = key, name = default.name), + prefix = prefix, + ) + + for repo_name, repo in registry.get_all_repositories(r).items(): + partial.call(repo, name = repo_name) + + fail_on_err( + registry.hub_repo(r, name = "nixpkgs_repositories", accessor = _ACCESSOR), + prefix = "Failed to generate `nixpkgs_repositories`: ", + ) + +_DEFAULT_ATTRS = { + "name": attr.string( + doc = "Use this global default repository.", + mandatory = True, + ), +} + +_NAME_ATTRS = { + "name": attr.string( + doc = "A unique name for this repository. The name must be unique within the requesting module.", + mandatory = True, + ), +} + +_INTEGRITY_ATTRS = { + "integrity": attr.string( + doc = "Expected checksum in Subresource Integrity format of the file downloaded. This must match the checksum of the file downloaded. _It is a security risk to omit the checksum as remote files can change._ At best omitting this field will make your build non-hermetic. It is optional to make development easier but either this attribute or `sha256` should be set before shipping.", + mandatory = False, + ), + "sha256": attr.string( + doc = "The expected SHA-256 of the file downloaded. This must match the SHA-256 of the file downloaded. _It is a security risk to omit the SHA-256 as remote files can change._ At best omitting this field will make your build non-hermetic. It is optional to make development easier but either this attribute or `integrity` should be set before shipping.", + mandatory = False, + ), +} + +_GITHUB_ATTRS = { + "org": attr.string( + default = "NixOS", + doc = "The GitHub organization hosting the repository.", + mandatory = False, + ), + "repo": attr.string( + default = "nixpkgs", + doc = "The name of the GitHub repository.", + mandatory = False, + ), + "tag": attr.string( + doc = "The Git tag to download. Specify one of `tag` or `commit`.", + mandatory = False, + ), + "commit": attr.string( + doc = "The Git commit to download. Specify one of `tag` or `commit`.", + mandatory = False, + ), +} + +_OVERRIDE_ATTRS = { + "name": attr.string( + doc = "The name of the global default repository to set.", + mandatory = True, + ), +} + +_default_tag = tag_class( + attrs = _DEFAULT_ATTRS, + doc = "Depend on this global default repository.", +) + +_github_tag = tag_class( + attrs = dicts.add(_NAME_ATTRS, _GITHUB_ATTRS, _INTEGRITY_ATTRS), + doc = "Import a Nix repository from Github.", +) + +_override_tag = tag_class( + attrs = _OVERRIDE_ATTRS, + doc = "Define the global default Nix repository. May only be used in the root module or rules_nixpkgs_core.", +) + +nix_repo = module_extension( + _nix_repo_impl, + tag_classes = { + "default": _default_tag, + "github": _github_tag, + "override": _override_tag, + }, +) diff --git a/core/private/BUILD.bazel b/core/private/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/core/private/module_registry.bzl b/core/private/module_registry.bzl new file mode 100644 index 000000000..eeacd9717 --- /dev/null +++ b/core/private/module_registry.bzl @@ -0,0 +1,418 @@ +"""This module implements a generic registry for hub repositories. +""" + +load("@bazel_skylib//lib:partial.bzl", "partial") + +_DUPLICATE_MODULE_ERROR = "Duplicate module '{desc}', previous version '{prev}'." +_DUPLICATE_LOCAL_REPO_ERROR = "Duplicate local repository '{name}', requested by module '{desc}'." +_DUPLICATE_GLOBAL_REPO_ERROR = "Duplicate global repository '{name}'." +_MODULE_NOT_FOUND_ERROR = "Module not found: '{key}'." +_LOCAL_REPO_NOT_FOUND_ERROR = "Local repository '{name}' not found, requested by module '{desc}'." +_GLOBAL_REPO_NOT_REGISTERED_ERROR = "Global repository '{name}' not registered, requested by module '{desc}'." + +def _make_registry(): + """Create a new registry object. + + Returns: + The registry object. + """ + return struct( + _modules = {}, + _global_repositories = {}, + ) + +def _module_key(name, version): + """Generate a key to identify a module. + + Args: + name: String, The name of the module. + version: String, The version of the module. + + Returns: + String, The generated key for the module. + """ + + # TODO[AH] Take the module version into account + # to support multi version overrides. + return name + +def _module_description(name, version): + """Generate a human readable description for a module. + + Args: + name: String, The name of the module. + version: String, The version of the module. + + Returns: + String, The generated description for the module. + """ + if version: + return "{} {}".format(name, version) + else: + return name + +def _make_module(*, name, version, description): + """Create a new module object. + + Args: + name: String, The name of the module. + version: String, The version of the module. + description: String, The description of the module. + + Returns: + The module object. + """ + return struct( + _name = name, + _version = version, + _description = description, + _local_repositories = {}, + _global_repositories = {}, + ) + +def _add_module(r, *, name, version): + """Register a new module. + + Args: + r: The registry object. + name: String, The name of the module to register. + version: String, The version of the module to register. + + Returns: + Tuple, the first element is the key to access the module in the registry, + the second element is an error message in case of failure + or None if successful. + """ + key = _module_key(name, version) + desc = _module_description(name, version) + + if key in r._modules: + return None, _DUPLICATE_MODULE_ERROR.format( + desc = desc, + prev = r._modules[key]._version, + ) + + r._modules[key] = _make_module( + name = name, + version = version, + description = desc, + ) + + return key, None + +def _use_global_repo(r, *, key, name): + """Register the dependency of a module on a global repository. + + Args: + r: The registry object. + key: String, The key of the module in the registry. + name: String, The name of the global repository. + + Returns: + Tuple, the first element is None, + the second element is an error message in case of failure + or None if successful. + """ + module = r._modules.get(key) + if module == None: + return None, _MODULE_NOT_FOUND_ERROR.format(key = key) + + module._global_repositories[name] = True + + return None, None + +def _add_local_repo(r, *, key, name, repo): + """Register a new local repository for the module. + + Args: + r: The registry object. + key: String, The key of the module in the registry. + name: String, The name of the local repository to register. + repo: The repository object to store in the registry. + + Returns: + Tuple, the first element is None, + the second element is an error message in case of failure + or None if successful. + """ + module = r._modules.get(key) + if module == None: + return None, _MODULE_NOT_FOUND_ERROR.format(key = key) + + if name in module._local_repositories: + return None, _DUPLICATE_LOCAL_REPO_ERROR.format(name = name, desc = module._description) + + module._local_repositories[name] = repo + + return None, None + +def _pop_local_repo(r, *, key, name): + """Remove the local repository from the registry and return its value. + + Fail if the repository is missing. + + Args: + r: The registry object. + key: String, The key of the module in the registry. + name: String, The name of the local repository to remove. + + Returns: + Tuple, the first element is the repository object if successful, + the second element is an error message in case of failure + or None if successful. + """ + module = r._modules.get(key) + if module == None: + return None, _MODULE_NOT_FOUND_ERROR.format(key = key) + + if not name in module._local_repositories: + return None, _LOCAL_REPO_NOT_FOUND_ERROR.format(name = name, desc = module._description) + + return module._local_repositories.pop(name), None + +def _set_default_global_repo(r, *, name, repo): + """Register a global repository if it has not been previously registered. + + Args: + r: The registry object. + name: String, The name of the global repository to register. + repo: Any, The repository object to store in the registry. + """ + if name not in r._global_repositories: + r._global_repositories[name] = repo + +def _has_global_repo(r, *, name): + """Check if the global repository exists in the registry. + + Args: + r: The registry object. + name: String, The name of the global repository to check. + + Returns: + Bool, True if the global repository exists, False otherwise. + """ + return name in r._global_repositories + +def _get_global_repo(r, *, name): + """Retrieve a global repository by its name. + + Args: + r: The registry object. + name: String, The name of the global repository to retrieve. + + Returns: + The repository object if found, None otherwise. + """ + return r._global_repositories.get(name) + +def _add_global_repo(r, *, name, repo): + """Register a new global repository. + + Args: + r: The registry object. + name: String, The name of the global repository to register. + repo: Any, The repository object to store in the registry. + + Returns: + Tuple, the first element is None, + the second element is an error message in case of failure + or None if successful. + """ + if name in r._global_repositories: + return None, _DUPLICATE_GLOBAL_REPO_ERROR.format(name = name) + + r._global_repositories[name] = repo + + return None, None + +def _canonical_repo_name(module_name, module_version, repo_name): + """Generate the canonical name for a repository. + + Args: + module_name: String, The name of the module. + module_version: String, The version of the module. + repo_name: String, The name of the repository. + + Returns: + String, The canonical name of the repository. + """ + return "{}_{}_{}".format(module_name, module_version, repo_name) + +def _get_all_repositories(r): + """Retrieve a dictionary containing all repositories. + + Args: + r: The registry object. + + Returns: + Dict, A dictionary containing all repositories, mapping canonical + repository names to stored repository objects. + """ + all_repositories = {} + + for name, repo in r._global_repositories.items(): + all_repositories[name] = repo + + for module_key, module in r._modules.items(): + for repo_name, repo in module._local_repositories.items(): + canonical_name = _canonical_repo_name(module._name, module._version, repo_name) + all_repositories[canonical_name] = repo + + return all_repositories + +def _get_all_module_scopes(r): + """Get mapping from module keys to all repositories used by that module. + + Args: + r: The registry object. + + Returns: + Tuple, the first element is a mapping of module keys + to all repositories used by that module, + the second element is an error message in case of failure + or None if successful. + """ + module_scopes = {} + + for module_key, module in r._modules.items(): + repos = {} + + for repo_name in module._global_repositories: + if not repo_name in r._global_repositories: + return None, _GLOBAL_REPO_NOT_REGISTERED_ERROR.format(name = repo_name, desc = module._description) + + repos[repo_name] = repo_name + + for repo_name, repo in module._local_repositories.items(): + canonical_name = _canonical_repo_name(module._name, module._version, repo_name) + repos[repo_name] = canonical_name + + module_scopes[module_key] = repos + + return module_scopes, None + +_NIXPKGS_REPOSITORIES_DEFS = '''\ +load("@rules_nixpkgs_core//:util.bzl", _fail_on_err = "fail_on_err") + +{accessor} + +def _get_repository(module_name, name): + """Access a registered repository from the module registry. + + Args: + module_name: `String`; Name of the calling Bazel module. + This is needed until Bazel offers unique module identifiers, + see [#17652][bazel-17652]. + name: `String`; Name of the repository. + + Returns: + Pair of (`Label`, optional `String`) + - `Label`; The resolved label to the repository's entry point. + - optional `String`; `None` indicates success, otherwise an error. + + [bazel-17652]: https://github.com/bazelbuild/bazel/issues/17652 + """ + key = module_name + + if key in _MODULES and key in _MODULE_SCOPES: + description = _MODULES[key] + repos = _MODULE_SCOPES[key] + else: + return None, "Unknown module - no module found under the key '{{}}'.".format(key) + + if name in repos: + repo_name = repos[name] + else: + return None, "Unknown repository - no repository named '{{}}' available for module '{{}}'.".format(name, description) + + return Label("@" + repo_name), None + +_MODULES = {{ +{modules} +}} +_MODULE_SCOPES = {{ +{module_scopes} +}} +''' + +def _format_module_scopes(module_scopes): + return "\n".join([ + " {}: {{\n{}\n }},".format(repr(key), "\n".join([ + " {}: {},".format(repr(repo_name), repr(workspace_name)) + for repo_name, workspace_name in sorted(repos.items()) + ])) + for key, repos in sorted(module_scopes.items()) + ]) + +def _format_modules(modules): + return "\n".join([ + " {}: {},".format(repr(key), repr(desc)) + for key, desc in sorted(modules.items()) + ]) + +def _hub_repo_rule_impl(repository_ctx): + module_scopes = {} + + for module_key, encoded_repos in repository_ctx.attr.module_scopes.items(): + repos = {} + + for encoded in encoded_repos: + repo_name, workspace_name = encoded.split("~", 1) + repos[repo_name] = workspace_name + + module_scopes[module_key] = repos + + defs = _NIXPKGS_REPOSITORIES_DEFS.format( + accessor = repository_ctx.attr.accessor, + modules = _format_modules(repository_ctx.attr.modules), + module_scopes = _format_module_scopes(module_scopes), + ) + repository_ctx.file("defs.bzl", content = defs, executable = False) + repository_ctx.file("BUILD.bazel", content = "", executable = False) + +_hub_repo_rule = repository_rule( + _hub_repo_rule_impl, + attrs = { + "accessor": attr.string(), + "modules": attr.string_dict(), + "module_scopes": attr.string_list_dict(), + }, +) + +def _hub_repo(r, *, name, accessor): + module_scopes, err = registry.get_all_module_scopes(r) + if err: + return None, err + + _hub_repo_rule( + name = name, + accessor = accessor, + modules = { + key: module._description + for key, module in r._modules.items() + }, + module_scopes = { + module_key: [ + "{}~{}".format(repo_name, canonical_name) + for repo_name, canonical_name in repos.items() + ] + for module_key, repos in module_scopes.items() + }, + ) + + return None, None + +registry = struct( + make = _make_registry, + add_module = _add_module, + has_global_repo = _has_global_repo, + get_global_repo = _get_global_repo, + add_global_repo = _add_global_repo, + set_default_global_repo = _set_default_global_repo, + use_global_repo = _use_global_repo, + add_local_repo = _add_local_repo, + pop_local_repo = _pop_local_repo, + get_all_repositories = _get_all_repositories, + get_all_module_scopes = _get_all_module_scopes, + hub_repo = _hub_repo, +) diff --git a/core/util.bzl b/core/util.bzl index 96eaf10d3..e142fd41c 100644 --- a/core/util.bzl +++ b/core/util.bzl @@ -2,6 +2,27 @@ load("@bazel_tools//tools/cpp:lib_cc_configure.bzl", "get_cpu_value") load("@bazel_skylib//lib:paths.bzl", "paths") load("@bazel_skylib//lib:versions.bzl", "versions") +def fail_on_err(return_value, prefix = None): + """Fail if the given return value indicates an error. + + Args: + return_value: Pair; If the second element is not `None` this indicates an error. + prefix: optional, String; A prefix for the error message contained in `return_value`. + + Returns: + The first element of `return_value` if no error was indicated. + """ + result, err = return_value + + if err: + if prefix: + msg = prefix + err + else: + msg = err + fail(msg) + + return result + def is_supported_platform(repository_ctx): return repository_ctx.which("nix-build") != None diff --git a/design/bzlmod/README.md b/design/bzlmod/README.md index e9fea4db6..b8b12eff3 100644 --- a/design/bzlmod/README.md +++ b/design/bzlmod/README.md @@ -88,8 +88,7 @@ Nix expressions, most commonly [nixpkgs]. E.g. `name = "nixpkgs-stable"`.\ * [`NIX_PATH` entry][nix-path] to Nix expressions.\ For angle-bracket reference in Nix expression, e.g. `import `. - E.g. `import_name = "nixpkgs"`. - Defaults to `import_name = name`. + E.g. `repositories = {"nixpkgs": ...}`. * (to-consider) Alias tag to map repository to another `NIX_PATH` entry. * (to-consider) Set default repository.\ Allowed in root or `rules_nixpkgs_core`. @@ -253,6 +252,33 @@ Support the use of Nix built packages as Bazel toolchains. [hub-and-spokes module][hub-and-spokes] or [hub repository][hub-repo] for further details. +### Module Scope Repositories May be Added to Bazel + +* Constraint + * A future follow-up of the [Automatic `use_repo` fixups for module + extensions][auto-use-repo] proposal was discussed that could introduce + external workspaces generated by module extensions that are scoped to + specific Bazel modules. The constraint is that repository mappings need to + be calculable without loading the module extension, meaning they must be + fully defined in the `MODULE.bazel` files. +* Impact + * Repositories generated by the rules\_nixpkgs module extensions that are + globally unified, thereby potentially used by multiple Bazel modules, + should have a name that is not prefixed by any Bazel module name, i.e. not + scoped to any particular Bazel module. In the simplest case this can just + be the tag name. In light of the above proposal, these could be directly + imported via `use_repo`. + * Repositories generated by the rules\_nixpkgs module extensions that are + specific to the request Bazel module, i.e. only used by that module, should + be scoped to that particular module, e.g. by using a name that is prefixed + by that Bazel module's name and version. In future, this could be replaced + by the `use_local_repo` mechanism. + * Nix repositories could be passed to Nix package tags as labels imported via + `use_repo` or `use_local_repo` as described above. Contents of Nix packages + could be referenced by label directly in the same manner. + +[auto-use-repo]: https://docs.google.com/document/d/1dj8SN5L6nwhNOufNqjBhYkk5f-BJI_FPYWKxlB3GAmA/edit?disco=AAAArdGBwhc + ### Nixpkgs Repositories or Packages Have No Convenient Canonical Name * Constraint @@ -313,6 +339,10 @@ Support the use of Nix built packages as Bazel toolchains. defaults. * rules\_nixkgs module extensions should support targeted overrides of Nix repositories and packages in transitive module dependencies. + Note, Bazel itself supports this via module overrides from the root module. + Implementation of this feature is deferred until the need arises. If + Bazel's builtin mechanism is sufficient, then this feature will not be + implemented. ### Module Extensions Themselves Don't Generate Output @@ -364,23 +394,18 @@ Support the use of Nix built packages as Bazel toolchains. ### Global Default Repository -rules\_nixpkgs\_core itself will define a global default `nixpkgs` repository. +rules\_nixpkgs\_core itself will define a global default `nixpkgs` repository, +any module can reference this global default repository like so. ```python use_extension("//extensions:repository.bzl", "nix_repo") -nix_repo.global(name = "nixpkgs") -nix_repo.github( - name = "nixpkgs", - tag = "22.11", - sha256 = "ddc3428d9e1a381b7476750ac4dbea7a42885cbbe6e1af44b21d6447c9609a6f", -) +nix_repo.default(name = "nixpkgs") ``` ### Local Repository Any Bazel module can define custom Nix repositories for local use. -The name may not collide with any global repositories. ```python nix_repo.github( @@ -392,11 +417,10 @@ nix_repo.github( ### Repository Override -The root module can define a global `nixpkgs` repository to override the -default set by rules\_nixpkgs\_core. +The root module can override the default set by rules\_nixpkgs\_core. ```python -nix_repo.global(name = "nixpkgs") +nix_repo.override(name = "nixpkgs") nix_repo.http( name = "nixpkgs", url = "https://github.com/NixOS/nixpkgs/archive/1eeea1f1922fb79a36008ba744310ccbf96130e2.tar.gz", @@ -405,19 +429,14 @@ nix_repo.http( ) ``` -The root module can also override a local repository in a specific transitive -dependency module. +rules\_nixpkgs\_core uses the same mechanism to define the global default. -```python -nix_repo.override( - module_name = "some_module", - repository = "nixpkgs-unstable", - replacement = "custom-nixpkgs-unstable", -) -nix_repo.file( - name = "custom-nixpkgs-unstable", - import_name = "nixpkgs", - file = "//nix:custom-nixpkgs-unstable.nix", +``` +nix_repo.override(name = "nixpkgs") +nix_repo.github( + name = "nixpkgs", + tag = "22.11", + sha256 = "ddc3428d9e1a381b7476750ac4dbea7a42885cbbe6e1af44b21d6447c9609a6f", ) ``` @@ -432,8 +451,8 @@ avoid diamond dependency issues, see above. ```python use_extension("@rules_nixpkgs_core//extensions:package.bzl", "nix_pkg") -nix_pkg.attr(name = "jq") -nix_pkg.attr(name = "awk", attr = "gawk") +nix_pkg.attr(attr = "jq") +nix_pkg.attr(attr = "gawk") ``` ### Local Nix Package @@ -446,7 +465,7 @@ rules\_nixkgs will still generate two separate external repositories to import the package for each module. ```python -nix_pkgs.local_attr(name = "jq", repository = "nixpkgs-unstable") +nix_pkgs.local_attr(name = "jq", repo = "nixpkgs-unstable") nix_pkg.local_expr( name = "awk", @@ -456,39 +475,7 @@ gawk-with-extensions.override { extensions = with gawkextlib; [ csv json ]; } """, - repository = "nixpkgs-unstable", -) -``` - -### Package Override - -The root module can override a unified Nix package to globally replace it by a -local Nix package. - -```python -nix_pkgs.override( - attr = "jq", - replacement = "jq-latest", -) -nix_pkgs.local_attr( - name = "jq-latest", - attr = "jq", - repository = "nixpkgs-unstable", -) -``` - -The root module can also override a local package in a specific transitive -dependency module. - -```python -nix_pkgs.override( - module_name = "some_module", - attr = "gawk", - replacement = "awk", -) -nix_pkgs.local_file( - name = "awk", - file = "//nix:gawk-with-extensions.nix", + repo = "nixpkgs-unstable", ) ``` @@ -499,10 +486,10 @@ nix_pkgs.local_file( The `rules_nixpkgs_core` module exposes the module extension `nix_repo` which offers tags to define Nix repositories: -* `github(name, import_name, org, repo, tag, commit)`\ +* `default(name)`\ + * `name`: `String`; Use this global default repository. +* `github(name, org, repo, tag, commit)`\ * `name`: `String`; unique name. - * `import_name`: optional, `String`; `NIX_PATH` entry name.\ - Default: `name`. * `org`: optional, `String`; The GitHub organization hosting the repository.\ Default: `NixOS`. * `repo`: optional, `String`; The name of the GitHub repository.\ @@ -514,10 +501,8 @@ offers tags to define Nix repositories: * `sha256`: optional, `String`; The SHA-256 hash of the downloaded archive. * `integrity`: optional, `String`; Expected checksum of the archive, in Subresource Integrity format. -* `http(name, import_name, url, urls, sha256, integrity, strip_prefix)`\ +* `http(name, url, urls, sha256, integrity, strip_prefix)`\ * `name`: `String`; unique name. - * `import_name`: optional, `String`; `NIX_PATH` entry name.\ - Default: `name`. * `url`: optional, `String`; URL to download from.\ Specify one of `url` or `urls`. * `urls`: optional, `String`; List of URLs to download from.\ @@ -526,29 +511,15 @@ offers tags to define Nix repositories: * `integrity`: optional, `String`; Expected checksum of the archive, in Subresource Integrity format. * `strip_prefix`: optional, `String`; A directory prefix to strip from the extracted files. -* `file(name, import_name, file, file_deps)`\ +* `file(name, file, file_deps)`\ * `name`: `String`; unique name. - * `import_name`: optional, `String`, `NIX_PATH` entry name.\ - Default: `name`. * `file`: `Label`; the file containing the Nix expression. * `file_deps`: optional, List of `Label`, files required by `file`. -* `expr(name, import_name, expression)`\ +* `expr(name, expression)`\ * `name`: `String`; unique name. - * `import_name`: optional, `String`, `NIX_PATH` entry name.\ - Default: `name`. * `expression`: `String`; the Nix expression. -* `alias(name, import_name, actual)`\ - * `name`: `String`; unique name. - * `import_name`: optional, `String`; `NIX_PATH` entry name.\ - Default: `name`. - * `actual`: `String`; Another repository. -* `default(repository)` (only allowed in root or `rules_nixpkgs_core`)\ - * `repository`: `String`; The repository to expose as a global default. -* `override(module_name, name, actual)` (only allowed in root)\ - * `module_name`: `String`; The module in which to apply the override. - * `name`: `String`; The name of the repository to override. - * `actual`: `String`; The name of the repository to replace it with.\ - The root module must define a repository by that name. +* `override(repo)` (only allowed in rules\_nixpkgs\_core and root)\ + * `repo`: `String`; The name of the repository to override. All `name` attributes define a unique name for the given Nix repository within the scope of the requesting module. @@ -560,20 +531,18 @@ The extension generates a hub repository called `nixpkgs_repositories` that exposes a macro from `//:defs.bzl` to access the imported repositories from the scope of the calling module: -* `nix_repo(module_name, name)`\ +* `nix_repo(module, name)`\ Attrs: - * `module_name`: `String`; name of the calling Bazel module.\ + * `module`: `String`; name of the calling Bazel module.\ Needed until Bazel offers an API to infer the calling module. See, [#17652][bazel-17652].\ Note, this is ambiguous for multi-version overrides.\ TODO: Handle multi-version overrides. * `name`: `String`; name of the repository.\ - This is the name used on the `nixpkgs_repository` tag. + This is the name used on the `nix_repo` tag. Returns:\ - `struct(import_name, label)` - * `import_name`: `String`; The name for the `NIX_PATH` entry. - * `label`: The resolved `Label` object to the repository. + The resolved `Label` object to the repository. Users are not expected to invoke `nix_repo` directly. Instead, it will be invoked by the package and toolchain module extensions to access the relevant @@ -584,18 +553,13 @@ repositories. The `rules_nixpkgs_core` module exposes the module extension `nix_pkg` which offers tags to define Nix packages: -* `attr(name, attr, repository)` (globally unified)\ - * `name`: `String`; unique name. - * `attr`: optional, `String`; the attribute path.\ - Default: `name`.\ - * `repository`: optional, `String`; repository to import from.\ - Default: `nixpkgs`.\ - Must be a global repository. -* `local_attr(name, attr, repository, build_file, build_file_content)`\ +* `attr(attr)` (globally unified)\ + * `attr`: `String`; the attribute path.\ +* `local_attr(name, attr, repo, build_file, build_file_content)`\ * `name`: `String`; unique name. * `attr`: optional, `String`; the attribute path.\ Default: `name`. - * `repository`: optional, `String`; repository to import from.\ + * `repo`: optional, `String`; the `nixpkgs` repository to import from.\ Default: `nixpkgs`. * `build_file`: optional, `Label`; `BUILD` file to write into the external workspace.\ @@ -603,38 +567,41 @@ offers tags to define Nix packages: * `build_file_content`: optional, `Label`; `BUILD` file content to write into the external workspace.\ Specify at most one of `build_file` or `build_file_content`. -* `local_file(name, attr, file, file_deps, repository, repositories)`\ +* `local_file(name, attr, file, file_deps, repo, repos)`\ * `name`: `String`; unique name. * `attr`: optional, `String`; the attribute path.\ Default: `name`. * `file`: `Label`; the file containing the Nix expression. * `file_deps`: optional, List of `Label`, files required by `file`. - * `repository`: optional, `String`; use this Nix repository. - * `repositories`: optional, List of `String`; use these Nix repositories. + * `repo`: optional, `String`; use this `nixpkgs` repository. + Equivalent to `repos = {"nixpkgs": repo}`. + Specify only one of `repo` or `repos`. + Default: `nixpkgs`. + * `repos`: optional, Dict of `String`; use these Nix repositories. + The dictionary key represents the name of the `NIX_PATH` entry. + Specify only one of `repo` or `repos`. * `build_file`: optional, `Label`; `BUILD` file to write into the external workspace.\ Specify at most one of `build_file` or `build_file_content`. * `build_file_content`: optional, `Label`; `BUILD` file content to write into the external workspace.\ -* `local_expr(name, attribute, expression, repository, repositories)`\ +* `local_expr(name, attr, expr, repo, repos)`\ * `name`: `String`; unique name. - * `attribute`: optional, `String`; the attribute path.\ + * `attr`: optional, `String`; the attribute path.\ Default: `name`. - * `expression`: `String`; the Nix expression. - * `repository`: optional, `String`; use this Nix repository. - * `repositories`: optional, List of `String`; use these Nix repositories. + * `expr`: `String`; the Nix expression. + * `repo`: optional, `String`; use this `nixpkgs` repository. + Equivalent to `repos = {"nixpkgs": repo}`. + Specify only one of `repo` or `repos`. + Default: `nixpkgs`. + * `repos`: optional, Dict of `String`; use these Nix repositories. + The dictionary key represents the name of the `NIX_PATH` entry. + Specify only one of `repo` or `repos`. * `build_file`: optional, `Label`; `BUILD` file to write into the external workspace.\ Specify at most one of `build_file` or `build_file_content`. * `build_file_content`: optional, `Label`; `BUILD` file content to write into the external workspace.\ -* `override(attr, repository, replacement, module_name)` (only allowed in root)\ - * `attr`: `String`; the attribute path of the package to replace. - * `repository`: optional, `String`; the repository of the package to replace.\ - Default: `nixpkgs`. - * `replacement`: `String`; the name of the replacement package.\ - * `module_name`; optional, `String`; only override a local package in this - module, if specified; otherwise, replace the package globally, All `name` attributes define a unique name for the given Nix repository within the scope of the requesting module. @@ -646,9 +613,9 @@ The extension generates a hub repository called `nixpkgs_packages` that exposes a macro from `//:defs.bzl` to access the imported packages from the scope of the calling module: -* `nix_pkg(module_name, name, label)`\ +* `nix_pkg(module, name, label)`\ Attrs: - * `module_name`: `String`; name of the calling Bazel module.\ + * `module`: `String`; name of the calling Bazel module.\ Needed until Bazel offers an API to infer the calling module. See, [#17652][bazel-17652].\ Note, this is ambiguous for multi-version overrides.\ diff --git a/testing/core/MODULE.bazel b/testing/core/MODULE.bazel index d87c68b4f..b251be3ce 100644 --- a/testing/core/MODULE.bazel +++ b/testing/core/MODULE.bazel @@ -14,19 +14,29 @@ local_path_override( path = "tests/location_expansion/test_repo", ) +nix_repo = use_extension("@rules_nixpkgs_core//extensions:repository.bzl", "nix_repo") +use_repo(nix_repo, "nixpkgs_repositories") +nix_repo.default(name = "nixpkgs") +nix_repo.github( + name = "remote_nixpkgs", + tag = "22.05", + sha256 = "0f8c25433a6611fa5664797cd049c80faefec91575718794c701f3b033f2db01", +) + +nix_pkg = use_extension("@rules_nixpkgs_core//extensions:package.bzl", "nix_pkg") +use_repo(nix_pkg, "nixpkgs_packages") +nix_pkg.attr(attr = "hello") +nix_pkg.local_attr(name = "attribute-test", attr = "hello") +nix_pkg.local_attr(name = "nixpkgs-git-repository-test", attr = "hello", repo = "remote_nixpkgs") + non_module_deps = use_extension("//:non_module_deps.bzl", "non_module_deps") -use_repo(non_module_deps, "nixpkgs") -use_repo(non_module_deps, "remote_nixpkgs") use_repo(non_module_deps, "http_nixpkgs") use_repo(non_module_deps, "nixpkgs_content") -use_repo(non_module_deps, "hello") use_repo(non_module_deps, "expr-test") -use_repo(non_module_deps, "attribute-test") use_repo(non_module_deps, "expr-attribute-test") use_repo(non_module_deps, "extra-args-test") use_repo(non_module_deps, "nix-file-test") use_repo(non_module_deps, "nix-file-deps-test") -use_repo(non_module_deps, "nixpkgs-git-repository-test") use_repo(non_module_deps, "nixpkgs-http-repository-test") use_repo(non_module_deps, "nixpkgs-local-repository-test") use_repo(non_module_deps, "relative-imports") diff --git a/testing/core/WORKSPACE b/testing/core/WORKSPACE index f4cefaa59..a88bf0211 100644 --- a/testing/core/WORKSPACE +++ b/testing/core/WORKSPACE @@ -20,6 +20,16 @@ load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") bazel_skylib_workspace() +local_repository( + name = "nixpkgs_repositories", + path = "workspace_nixpkgs_repositories", +) + +local_repository( + name = "nixpkgs_packages", + path = "workspace_nixpkgs_packages", +) + local_repository( name = "nixpkgs_location_expansion_test_file", path = "tests/location_expansion/test_repo", @@ -30,7 +40,7 @@ load( "nixpkgs_repositories", ) -nixpkgs_repositories() +nixpkgs_repositories(bzlmod = False) local_repository( name = "rules_nixpkgs_java", diff --git a/testing/core/non_module_deps.bzl b/testing/core/non_module_deps.bzl index cae614d64..1e758095b 100644 --- a/testing/core/non_module_deps.bzl +++ b/testing/core/non_module_deps.bzl @@ -4,7 +4,7 @@ load( ) def _non_module_deps_impl(ctx): - nixpkgs_repositories() + nixpkgs_repositories(bzlmod = True) non_module_deps = module_extension( implementation = _non_module_deps_impl, diff --git a/testing/core/tests/BUILD.bazel b/testing/core/tests/BUILD.bazel index 3327e8ba2..2e80bf2ec 100644 --- a/testing/core/tests/BUILD.bazel +++ b/testing/core/tests/BUILD.bazel @@ -1,11 +1,29 @@ load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("@nixpkgs_packages//:defs.bzl", "nix_pkg") load(":location_expansion_unit_test.bzl", "expand_location_unit_test_suite") +load(":module_registry_tests.bzl", "module_registry_test_suite") package(default_testonly = 1) expand_location_unit_test_suite() [ + # All of these tests use the "hello" binary to see + # whether different invocations of `nixpkgs_package` + # produce a valid bazel repository. + sh_test( + name = "run-{0}".format(test), + timeout = "short", + srcs = ["test_bin.sh"], + args = ["$(location {0})".format(nix_pkg("rules_nixpkgs_core_testing", test, "//:bin"))], + data = [nix_pkg("rules_nixpkgs_core_testing", test, "//:bin")], + ) + for test in [ + "hello", + "attribute-test", + "nixpkgs-git-repository-test", + ] +] + [ # All of these tests use the "hello" binary to see # whether different invocations of `nixpkgs_package` # produce a valid bazel repository. @@ -17,14 +35,11 @@ expand_location_unit_test_suite() data = ["@{0}//:bin".format(test)], ) for test in [ - "hello", "expr-test", - "attribute-test", "expr-attribute-test", "extra-args-test", "nix-file-test", "nix-file-deps-test", - "nixpkgs-git-repository-test", "nixpkgs-http-repository-test", "nixpkgs-local-repository-test", "relative-imports", @@ -94,3 +109,5 @@ diff_test( file1 = "//tests:location_expansion/test_file", file2 = "@nixpkgs_location_expansion_test//:out/argstr_external_file", ) + +module_registry_test_suite(name = "module_registry_test_suite") diff --git a/testing/core/tests/module_registry_tests.bzl b/testing/core/tests/module_registry_tests.bzl new file mode 100644 index 000000000..926ce79d1 --- /dev/null +++ b/testing/core/tests/module_registry_tests.bzl @@ -0,0 +1,193 @@ +"""Unit tests for the hub repository registry. +""" + +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("@rules_nixpkgs_core//private:module_registry.bzl", "registry") + +def _add_module_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + key, err = registry.add_module(r, name = "test_module", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + _, err = registry.add_module(r, name = "test_module", version = "1.2.0") + asserts.equals(env, expected = "Duplicate module 'test_module 1.2.0', previous version '1.0.0'.", actual = err, msg = "Duplicate module error expected.") + + return unittest.end(env) + +_add_module_test = unittest.make(_add_module_test_impl) + +def _global_repo_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + key, err = registry.add_module(r, name = "test_module", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + _, err = registry.add_global_repo(r, name = "global_repo", repo = "repo") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + has_global_repo = registry.has_global_repo(r, name = "global_repo") + asserts.true(env, has_global_repo, msg = "Global repo should exist.") + + global_repo = registry.get_global_repo(r, name = "global_repo") + asserts.equals(env, expected = "repo", actual = global_repo, msg = "Unexpected repo value returned.") + + _, err = registry.add_global_repo(r, name = "global_repo", repo = "another_repo") + asserts.equals(env, expected = "Duplicate global repository 'global_repo'.", actual = err, msg = "Duplicate global repo error expected.") + + global_repo = registry.get_global_repo(r, name = "global_repo") + asserts.equals(env, expected = "repo", actual = global_repo, msg = "Unexpected repo value returned.") + + return unittest.end(env) + +_global_repo_test = unittest.make(_global_repo_test_impl) + +def _local_repo_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + key, err = registry.add_module(r, name = "test_module", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + _, err = registry.add_local_repo(r, key = key, name = "local_repo", repo = "repo") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + _, err = registry.add_local_repo(r, key = key, name = "local_repo", repo = "repo") + asserts.equals(env, expected = "Duplicate local repository 'local_repo', requested by module 'test_module 1.0.0'.", actual = err, msg = "Duplicate local repo error expected.") + + repo, err = registry.pop_local_repo(r, key = key, name = "local_repo") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + asserts.equals(env, expected = "repo", actual = repo, msg = "Unexpected repo value returned.") + + _, err = registry.pop_local_repo(r, key = key, name = "local_repo") + asserts.equals(env, expected = "Local repository 'local_repo' not found, requested by module 'test_module 1.0.0'.", actual = err, msg = "Local repo not found error expected.") + + return unittest.end(env) + +_local_repo_test = unittest.make(_local_repo_test_impl) + +def _use_global_repo_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + key, err = registry.add_module(r, name = "test_module", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + _, err = registry.use_global_repo(r, key = key, name = "global_repo") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + _, err = registry.use_global_repo(r, key = "nonexistent_key", name = "global_repo") + asserts.equals(env, expected = "Module not found: 'nonexistent_key'.", actual = err, msg = "Module not found error expected.") + + return unittest.end(env) + +_use_global_repo_test = unittest.make(_use_global_repo_test_impl) + +def _set_default_global_repo_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + key, err = registry.add_module(r, name = "test_module", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + registry.set_default_global_repo(r, name = "default_global_repo", repo = "repo") + + registry.set_default_global_repo(r, name = "default_global_repo", repo = "another_repo") + + global_repo = registry.get_global_repo(r, name = "default_global_repo") + asserts.equals(env, expected = "repo", actual = global_repo, msg = "Unexpected global default repo value returned.") + + return unittest.end(env) + +_set_default_global_repo_test = unittest.make(_set_default_global_repo_test_impl) + +def _get_all_repositories_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + + key, err = registry.add_module(r, name = "test_module", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected when adding module.") + + _, err = registry.add_local_repo(r, key = key, name = "local_repo", repo = "local_repo_obj") + asserts.equals(env, expected = None, actual = err, msg = "No error expected when adding local_repo.") + + _, err = registry.add_global_repo(r, name = "global_repo", repo = "global_repo_obj") + asserts.equals(env, expected = None, actual = err, msg = "No error expected when adding global_repo.") + + all_repositories = registry.get_all_repositories(r) + expected_repositories = { + "global_repo": "global_repo_obj", + "test_module_1.0.0_local_repo": "local_repo_obj", + } + asserts.equals(env, expected = expected_repositories, actual = all_repositories, msg = "Unexpected repositories returned.") + + return unittest.end(env) + +_get_all_repositories_test = unittest.make(_get_all_repositories_test_impl) + +def _get_all_module_scopes_test_impl(ctx): + env = unittest.begin(ctx) + + r = registry.make() + key1, err = registry.add_module(r, name = "test_module1", version = "1.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + key2, err = registry.add_module(r, name = "test_module2", version = "2.0.0") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + # Use global repositories before they are added + _, err = registry.use_global_repo(r, key = key1, name = "global_repo1") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + _, err = registry.use_global_repo(r, key = key2, name = "global_repo2") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + # Add one global repository + _, err = registry.add_global_repo(r, name = "global_repo1", repo = "global_repo1_obj") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + # Test with unregistered global repository + scopes, err = registry.get_all_module_scopes(r) + asserts.equals(env, expected = "Global repository 'global_repo2' not registered, requested by module 'test_module2 2.0.0'.", actual = err, msg = "Global repository not registered error expected.") + + # Add the other global repository + _, err = registry.add_global_repo(r, name = "global_repo2", repo = "global_repo2_obj") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + # Add local repositories + _, err = registry.add_local_repo(r, key = key1, name = "local_repo1", repo = "local_repo1_obj") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + _, err = registry.add_local_repo(r, key = key2, name = "local_repo2", repo = "local_repo2_obj") + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + + scopes, err = registry.get_all_module_scopes(r) + asserts.equals(env, expected = None, actual = err, msg = "No error expected.") + expected_scopes = { + key1: { + "global_repo1": "global_repo1", + "local_repo1": "test_module1_1.0.0_local_repo1", + }, + key2: { + "global_repo2": "global_repo2", + "local_repo2": "test_module2_2.0.0_local_repo2", + }, + } + asserts.equals(env, expected = expected_scopes, actual = scopes, msg = "Scopes do not match expected value.") + + return unittest.end(env) + +_get_all_module_scopes_test = unittest.make(_get_all_module_scopes_test_impl) + +def module_registry_test_suite(name): + unittest.suite( + name, + _add_module_test, + _global_repo_test, + _local_repo_test, + _use_global_repo_test, + _set_default_global_repo_test, + _get_all_repositories_test, + _get_all_module_scopes_test, + ) diff --git a/testing/core/tests/nixpkgs_repositories.bzl b/testing/core/tests/nixpkgs_repositories.bzl index d58d33433..693c8812f 100644 --- a/testing/core/tests/nixpkgs_repositories.bzl +++ b/testing/core/tests/nixpkgs_repositories.bzl @@ -1,3 +1,5 @@ +load("@nixpkgs_repositories//:defs.bzl", "nix_repo") +load("@nixpkgs_packages//:defs.bzl", "nix_pkg") load( "@rules_nixpkgs_core//:nixpkgs.bzl", "nixpkgs_git_repository", @@ -6,19 +8,43 @@ load( "nixpkgs_package", ) -def nixpkgs_repositories(): - nixpkgs_local_repository( - name = "nixpkgs", - nix_file = "//:nixpkgs.nix", - nix_file_deps = ["//:flake.lock"], - ) - - nixpkgs_git_repository( - name = "remote_nixpkgs", - remote = "https://github.com/NixOS/nixpkgs", - revision = "22.05", - sha256 = "0f8c25433a6611fa5664797cd049c80faefec91575718794c701f3b033f2db01", - ) +def nixpkgs_repositories(*, bzlmod): + if bzlmod: + nixpkgs = nix_repo("rules_nixpkgs_core_testing", "nixpkgs") + remote_nixpkgs = nix_repo("rules_nixpkgs_core_testing", "remote_nixpkgs") + else: + nixpkgs = "@nixpkgs" + nixpkgs_local_repository( + name = "nixpkgs", + nix_file = "//:nixpkgs.nix", + nix_file_deps = ["//:flake.lock"], + ) + + remote_nixpkgs = "@remote_nixpkgs" + nixpkgs_git_repository( + name = "remote_nixpkgs", + remote = "https://github.com/NixOS/nixpkgs", + revision = "22.05", + sha256 = "0f8c25433a6611fa5664797cd049c80faefec91575718794c701f3b033f2db01", + ) + + nixpkgs_package( + name = "hello", + # Deliberately not repository, to test whether repositories works. + repositories = {"nixpkgs": nixpkgs}, + ) + + nixpkgs_package( + name = "attribute-test", + attribute_path = "hello", + repository = nixpkgs, + ) + + nixpkgs_package( + name = "nixpkgs-git-repository-test", + attribute_path = "hello", + repositories = {"nixpkgs": remote_nixpkgs}, + ) nixpkgs_http_repository( name = "http_nixpkgs", @@ -37,12 +63,6 @@ def nixpkgs_repositories(): ], ) - nixpkgs_package( - name = "hello", - # Deliberately not repository, to test whether repositories works. - repositories = {"nixpkgs": "@nixpkgs"}, - ) - nixpkgs_package( name = "expr-test", nix_file_content = "let pkgs = import { config = {}; overlays = []; }; in pkgs.hello", @@ -51,17 +71,11 @@ def nixpkgs_repositories(): repositories = {"nixpkgs": "//:nixpkgs.nix"}, ) - nixpkgs_package( - name = "attribute-test", - attribute_path = "hello", - repository = "@nixpkgs", - ) - nixpkgs_package( name = "expr-attribute-test", attribute_path = "hello", nix_file_content = "import { config = {}; overlays = []; }", - repository = "@nixpkgs", + repository = nixpkgs, ) nixpkgs_package( @@ -74,27 +88,21 @@ def nixpkgs_repositories(): "packagePath", "hello", ], - repository = "@nixpkgs", + repository = nixpkgs, ) nixpkgs_package( name = "nix-file-test", attribute_path = "hello", nix_file = "//tests:nixpkgs.nix", - repository = "@nixpkgs", + repository = nixpkgs, ) nixpkgs_package( name = "nix-file-deps-test", nix_file = "//tests:hello.nix", nix_file_deps = ["//tests:pkgname.nix"], - repository = "@nixpkgs", - ) - - nixpkgs_package( - name = "nixpkgs-git-repository-test", - attribute_path = "hello", - repositories = {"nixpkgs": "@remote_nixpkgs"}, + repository = nixpkgs, ) nixpkgs_package( @@ -118,13 +126,13 @@ def nixpkgs_repositories(): "//:nixpkgs.nix", "//tests:relative_imports/nixpkgs.nix", ], - repository = "@nixpkgs", + repository = nixpkgs, ) nixpkgs_package( name = "output-filegroup-test", nix_file = "//tests:output.nix", - repository = "@nixpkgs", + repository = nixpkgs, ) nixpkgs_package( @@ -137,7 +145,7 @@ filegroup( ) """, nix_file = "//tests:output.nix", - repository = "@nixpkgs", + repository = nixpkgs, ) nixpkgs_package( @@ -165,7 +173,7 @@ filegroup( "argstr_external_file", "$(location @nixpkgs_location_expansion_test_file//:test_file)", ], - repository = "@remote_nixpkgs", + repository = remote_nixpkgs, ) # TODO this is currently untested, @@ -181,5 +189,5 @@ filegroup( "//:nixpkgs.nix", "//tests:relative_imports/nixpkgs.nix", ], - repository = "@nixpkgs", + repository = nixpkgs, ) diff --git a/testing/core/workspace_nixpkgs_packages/BUILD.bazel b/testing/core/workspace_nixpkgs_packages/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/testing/core/workspace_nixpkgs_packages/WORKSPACE b/testing/core/workspace_nixpkgs_packages/WORKSPACE new file mode 100644 index 000000000..e69de29bb diff --git a/testing/core/workspace_nixpkgs_packages/defs.bzl b/testing/core/workspace_nixpkgs_packages/defs.bzl new file mode 100644 index 000000000..c6565f93d --- /dev/null +++ b/testing/core/workspace_nixpkgs_packages/defs.bzl @@ -0,0 +1,3 @@ +def nix_pkg(module_name, name, label): + # This is a WORKSPACE mode replacement for @nixpkgs_packages. + return Label("@" + name).relative(label) diff --git a/testing/core/workspace_nixpkgs_repositories/BUILD.bazel b/testing/core/workspace_nixpkgs_repositories/BUILD.bazel new file mode 100644 index 000000000..e69de29bb diff --git a/testing/core/workspace_nixpkgs_repositories/WORKSPACE b/testing/core/workspace_nixpkgs_repositories/WORKSPACE new file mode 100644 index 000000000..e69de29bb diff --git a/testing/core/workspace_nixpkgs_repositories/defs.bzl b/testing/core/workspace_nixpkgs_repositories/defs.bzl new file mode 100644 index 000000000..afe33659c --- /dev/null +++ b/testing/core/workspace_nixpkgs_repositories/defs.bzl @@ -0,0 +1,2 @@ +def nix_repo(): + fail("This is a WORKSPACE mode place-holder for @nixpkgs_repositories. Do not call it in WORKSPACE mode.")