diff --git a/containers/BUILD.bazel b/containers/BUILD.bazel new file mode 100644 index 000000000..39db8bc24 --- /dev/null +++ b/containers/BUILD.bazel @@ -0,0 +1,15 @@ +package(default_visibility = ["//visibility:public"]) + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +exports_files([ + "docker/stream.sh", +]) + +bzl_library( + name = "docker", + srcs = [ + "docker.bzl", + ], + visibility = ["//visibility:public"], +) diff --git a/containers/docker.bzl b/containers/docker.bzl new file mode 100644 index 000000000..7ad7abd2d --- /dev/null +++ b/containers/docker.bzl @@ -0,0 +1,104 @@ +""" +# Docker containerization Bazel Nixpkgs rules + +To run bazel artifacts across systems and platforms, nixpkgs_rules exposed a +docker hook. e.g. + +```starlark +nixpkgs_docker_image( + name = "nix_deps_image", + srcs = [ + "@cc_toolchain_nixpkgs_info////bazel-support", + "@nixpkgs_python_toolchain_python3//bazel-support", + "@nixpkgs_sh_posix_config//bazel-support", + "@rules_haskell_ghc_nixpkgs//bazel-support", + "@nixpkgs_valgrind//bazel-support", + ], + bazel = "@nixpkgs_bazel//bazel-support", + repositories = {"nixpkgs": "@nixpkgs"}, +) +``` + +here, nixpkgs rules dependencies are bundled into a docker container for use and +deployment. + +""" + +def _nixpkgs_docker_image_impl(repository_ctx): + repositories = repository_ctx.attr.repositories + + nix_build_bin = repository_ctx.which("nix-build") + repository_ctx.symlink(nix_build_bin, "nix-build") + + srcs = repository_ctx.attr.srcs + bazel = repository_ctx.attr.bazel + + # HACK! On bazel from nixpkgs, shebangs get mangled in things like + # @bazel_tools//tools/cpp:linux_cc_wrapper.sh.tpl, so that nix store + # paths end up referenced there. + # One needs to ensure those paths are available on the docker image + # as well, we can do that by including bazel + srcs = srcs + [bazel] if bazel else srcs + + contents = [] + for src in srcs: + path_to_default_nix = repository_ctx.path(src.relative("default.nix")) + package = "bazel-support/%s" % src.workspace_name + repository_ctx.symlink(path_to_default_nix.dirname, package) + contents.append("(import ./%s {})" % package) + + repository_ctx.template( + "default.nix", + Label("@io_tweag_rules_nixpkgs//containers:docker/default.nix.tpl"), + substitutions = { + "%{name}": repr(repository_ctx.name), + "%{contents}": "\n ".join(contents), + }, + executable = False, + ) + + args = list(repository_ctx.attr.nixopts) + for repo_label, repo_name in repositories.items(): + absolute_repo = repository_ctx.path(repo_label).dirname + + # Excessive quoting due to nix limitations for ~ in file path + # (see NixOS/nix#7742). + args.extend([ + '"-I"', + "\"%s=\\\"%s\\\"\"" % (repo_name, absolute_repo), + ]) + + repository_ctx.template( + "BUILD", + Label("@io_tweag_rules_nixpkgs//containers:docker/BUILD.bazel.tpl"), + substitutions = { + "%{args_comma_sep}": ",\n ".join(args), + "%{args_space_sep}": " ".join(args), + }, + executable = False, + ) + +_nixpkgs_docker_image = repository_rule( + implementation = _nixpkgs_docker_image_impl, + attrs = { + "nixopts": attr.string_list(), + "repositories": attr.label_keyed_string_dict(), + "srcs": attr.label_list( + doc = 'List of nixpkgs_package to include in the image. E.g. ["@hello//nixpkg"]', + ), + "bazel": attr.label( + doc = """If using bazel from nixpkgs, this a nixpackage_package + based on exactly the same bazel derivation. This is to ensure any paths + for mangled '/usr/env bash' introduced by nix exist in the store. + Example: '.bazel_4'. + """, + ), + }, +) + +def nixpkgs_docker_image(name, **kwargs): + repositories = kwargs.get("repositories") + if repositories: + inversed_repositories = {value: key for key, value in repositories.items()} + kwargs["repositories"] = inversed_repositories + _nixpkgs_docker_image(name = name, **kwargs) diff --git a/containers/docker/BUILD.bazel.tpl b/containers/docker/BUILD.bazel.tpl new file mode 100644 index 000000000..e64f2fe6b --- /dev/null +++ b/containers/docker/BUILD.bazel.tpl @@ -0,0 +1,29 @@ +sh_binary( + name = "stream", + srcs = ["@io_tweag_rules_nixpkgs//containers:docker/stream.sh"], + data = [ + ":default.nix", + ":nix-build", + ], + env = {"NIX_BUILD": "$(location :nix-build)"}, + args = [ + '"./$(location :default.nix)"', + %{args_comma_sep}, + ], +) + +genrule( + name = "image", + srcs = [ + ":default.nix", + ], + outs = ["image.tgz"], + exec_tools = [":nix-build"], + cmd = """ + $(location :nix-build) %{args_space_sep} \ + --arg stream false \ + --out-link $@ \ + "./$(location :default.nix)" + """, + local = True, +) diff --git a/containers/docker/default.nix.tpl b/containers/docker/default.nix.tpl new file mode 100644 index 000000000..ea0aa8abb --- /dev/null +++ b/containers/docker/default.nix.tpl @@ -0,0 +1,46 @@ +{ stream ? true, tag ? null, nixpkgs ? import {} }: +let + dockerImage = if stream + then nixpkgs.dockerTools.streamLayeredImage + else nixpkgs.dockerTools.buildLayeredImage; + + name = %{name}; + + contents = [ + %{contents} + ]; + + manifest = nixpkgs.writeTextFile + { name = "${name}-MANIFEST"; + text = nixpkgs.lib.strings.concatMapStrings (pkg: "${pkg}\n") contents; + destination = "/MANIFEST"; + }; + + usr_bin_env = nixpkgs.runCommand "usr-bin-env" {} '' + mkdir -p "$out/usr/bin/" + ln -s "${nixpkgs.coreutils}/bin/env" "$out/usr/bin/" + ''; +in + dockerImage { + inherit name tag; + + contents = [ + # Contents get copied to the top-level of the image, so we jus putt + # a short manifest file here and get all the store paths as dependencies + manifest + + # Ensure "/usr/bin/env bash" works correctly + nixpkgs.bash + nixpkgs.coreutils + usr_bin_env + + # avoid "commitBuffer: invalid argument (invalid character)" running tests + nixpkgs.glibcLocales + ]; + + extraCommands = "mkdir -m 0777 tmp"; + + config = { + Cmd = [ "${nixpkgs.bashInteractive}/bin/bash" ]; + }; + } diff --git a/containers/docker/stream.sh b/containers/docker/stream.sh new file mode 100755 index 000000000..db8dd5f70 --- /dev/null +++ b/containers/docker/stream.sh @@ -0,0 +1,7 @@ +set -euo pipefail + +run() { + ${NIX_BUILD} --arg stream true "$@" +} + +$(run "$@") diff --git a/core/default.nix.tpl b/core/default.nix.tpl new file mode 100644 index 000000000..9bd8a5b4e --- /dev/null +++ b/core/default.nix.tpl @@ -0,0 +1,13 @@ +{ %{args_with_defaults} }: +let + value_or_function = %{def}; + value = + if builtins.isFunction value_or_function then + let + formalArgs = builtins.functionArgs value_or_function; + actualArgs = builtins.intersectAttrs formalArgs { inherit %{args}; }; + in + value_or_function actualArgs + else value_or_function; +in +value%{maybe_attr} diff --git a/core/nixpkgs.bzl b/core/nixpkgs.bzl index 0a04f24a1..3886b4848 100644 --- a/core/nixpkgs.bzl +++ b/core/nixpkgs.bzl @@ -377,9 +377,25 @@ def nixpkgs_local_repository( **kwargs ) +def _nixopts_args(nixopts): + result = {} + arg_opt, arg_name = None, None + for opt in nixopts: + if opt == "--arg" or opt == "--argstr": + arg_opt, arg_name = opt, None + elif arg_opt: + if arg_name == None: + arg_name = opt + else: + arg_val = opt if arg_opt == "--arg" else "''%s''" % opt + result[arg_name] = arg_val + arg_opt, arg_name = None, None + return result + def _nixpkgs_package_impl(repository_ctx): repository = repository_ctx.attr.repository repositories = repository_ctx.attr.repositories + attribute_path = repository_ctx.attr.attribute_path expr_args = [] @@ -453,26 +469,63 @@ def _nixpkgs_package_impl(repository_ctx): else: # No user supplied build file, we may create the default one. create_build_file_if_needed = True + # Workaround to bazelbuild/bazel#4533 repository_ctx.path("BUILD") if repository_ctx.attr.nix_file and repository_ctx.attr.nix_file_content: fail("Specify one of 'nix_file' or 'nix_file_content', but not both.") - elif repository_ctx.attr.nix_file: + + # Create a default.nix and BUILD file in bazel-support for external + # reference. + if repository_ctx.attr.nix_file: nix_file = cp(repository_ctx, repository_ctx.attr.nix_file) - expr_args.append(repository_ctx.path(nix_file)) + default_nix_substs = { + "%{def}": "import %s" % repository_ctx.path(nix_file), + "%{maybe_attr}": (".%s" % attribute_path) if attribute_path else "", + } elif repository_ctx.attr.nix_file_content: - expr_args.extend(["-E", repository_ctx.attr.nix_file_content]) + default_nix_substs = { + "%{def}": repository_ctx.attr.nix_file_content, + "%{maybe_attr}": (".%s" % attribute_path) if attribute_path else "", + } else: - expr_args.extend(["-E", "import { config = {}; overlays = []; }"]) + default_nix_substs = { + "%{def}": "import { config = {}; overlays = []; }", + "%{maybe_attr}": ".%s" % (attribute_path or repository_ctx.attr.name), + } nix_file_deps = {} for dep_lbl, dep_str in repository_ctx.attr.nix_file_deps.items(): nix_file_deps[dep_str] = cp(repository_ctx, dep_lbl) + nixopts = [ + expand_location( + repository_ctx = repository_ctx, + string = opt, + labels = nix_file_deps, + attr = "nixopts", + ) + for opt in repository_ctx.attr.nixopts + ] + nixopts_args = _nixopts_args(nixopts) + default_nix_substs["%{args_with_defaults}"] = ", ".join([ + "%s ? %s" % kv + for kv in nixopts_args.items() + ]) + default_nix_substs["%{args}"] = " ".join(nixopts_args.keys()) + + repository_ctx.template( + "bazel-support/default.nix", + Label("@rules_nixpkgs_core//:default.nix.tpl"), + substitutions = default_nix_substs, + executable = False, + ) + + repository_ctx.file("bazel-support/BUILD", 'exports_files(["nix-out-link"])\nfilegroup(name="bazel-support", srcs=glob(["nix-out-link*/**/*"], exclude=["BUILD"]))') + expr_args.extend([ - "-A", - repository_ctx.attr.attribute_path if repository_ctx.attr.nix_file or repository_ctx.attr.nix_file_content else repository_ctx.attr.attribute_path or repository_ctx.attr.unmangled_name, + repository_ctx.path("bazel-support/default.nix"), # Creating an out link prevents nix from garbage collecting the store path. # nixpkgs uses `nix-support/` for such house-keeping files, so we mirror them # and use `bazel-support/`, under the assumption that no nix package has