Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add nixpkgs_cc_configure() #40

Merged
merged 3 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,61 @@ filegroup(
</tbody>
</table>

### nixpkgs_cc_configure

Tells Bazel to use compilers and linkers from Nixpkgs for the CC
toolchain. By default, Bazel autodetects a toolchain on the current
`PATH`. Overriding this autodetection makes builds more hermetic and
is considered a best practice.

Example:

```bzl
nixpkgs_cc_configure(repository = "@nixpkgs//:default.nix")
```

<table class="table table-condensed table-bordered table-params">
<colgroup>
<col class="col-param" />
<col class="param-description" />
</colgroup>
<thead>
<tr>
<th colspan="2">Attributes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>nix_file_content</code></td>
<td>
<p><code>String; optional</code></p>
<p>An expression for a Nix environment derivation.</p>
</td>
</tr>
<tr>
<td><code>repository</code></td>
<td>
<p><code>Label; optional</code></p>
<p>A repository label identifying which Nixpkgs to use.</p>
</td>
</tr>
<tr>
<td><code>repositories</code></td>
<td>
<p><code>String-keyed label dict; optional</code></p>
<p>A dictionary mapping `NIX_PATH` entries to repository labels.</p>
<p>Setting it to
<pre><code>repositories = { "myrepo" : "//:myrepo" }</code></pre>
for example would replace all instances
of <code>&lt;myrepo&gt;</code> in the called nix code by the
path to the target <code>"//:myrepo"</code>. See the
<a href="https://nixos.org/nix/manual/#env-NIX_PATH">relevant
section in the nix manual</a> for more information.</p>
<p>Specify one of `path` or `repositories`.</p>
</td>
</tr>
</tbody>
</table>

## Migration

Expand Down
9 changes: 8 additions & 1 deletion WORKSPACE
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
workspace(name = "io_tweag_rules_nixpkgs")

load("//nixpkgs:nixpkgs.bzl", "nixpkgs_git_repository", "nixpkgs_package")
load(
"//nixpkgs:nixpkgs.bzl",
"nixpkgs_cc_configure",
"nixpkgs_git_repository",
"nixpkgs_package"
)

# For tests

Expand Down Expand Up @@ -74,3 +79,5 @@ filegroup(
)
""",
)

nixpkgs_cc_configure(repository = "@remote_nixpkgs//:default.nix")
2 changes: 1 addition & 1 deletion nixpkgs/BUILD.pkg
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package(default_visibility = [ "//visibility:public" ])
package(default_visibility = ["//visibility:public"])

filegroup(
name = "bin",
Expand Down
142 changes: 106 additions & 36 deletions nixpkgs/nixpkgs.bzl
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
load("@bazel_tools//tools/cpp:cc_configure.bzl", "cc_autoconf_impl")

"""Rules for importing Nixpkgs packages."""

def _nixpkgs_git_repository_impl(repository_ctx):
Expand Down Expand Up @@ -97,17 +99,22 @@ def _nixpkgs_package_impl(repository_ctx):
# as we can do.
timeout = 1073741824

res = repository_ctx.execute(nix_build, quiet = False, timeout = timeout,
environment=dict(NIX_PATH=nix_path))
if res.return_code == 0:
output_path = res.stdout.splitlines()[-1]
else:
_execute_error(res, "Cannot build Nix attribute `{}`"
.format(repository_ctx.attr.attribute_path))

exec_result = _execute_or_fail(
repository_ctx,
nix_build,
failure_message = "Cannot build Nix attribute '{}'.".format(
repository_ctx.attr.attribute_path
),
quiet = False,
timeout = timeout,
environment=dict(NIX_PATH=nix_path),
)
output_path = exec_result.stdout.splitlines()[-1]
# Build a forest of symlinks (like new_local_package() does) to the
# Nix store.
_symlink_children(repository_ctx, output_path)
for target in _find_children(repository_ctx, output_path):
basename = target.rpartition("/")[-1]
repository_ctx.symlink(target, basename)


_nixpkgs_package = repository_rule(
Expand All @@ -125,6 +132,7 @@ _nixpkgs_package = repository_rule(
local = True,
)


def nixpkgs_package(*args, **kwargs):
# Because of https://github.com/bazelbuild/bazel/issues/5356 we can't
# directly pass a dict from strings to labels to the rule (which we'd like
Expand All @@ -142,26 +150,104 @@ def nixpkgs_package(*args, **kwargs):
else:
_nixpkgs_package(*args, **kwargs)

def _symlink_children(repository_ctx, target_dir):
"""Create a symlink to all children of `target_dir` in the current
build directory."""

def _nixpkgs_cc_autoconf_impl(repository_ctx):
# Calling repository_ctx.path() on anything but a regular file
# fails. So the roundabout way to do the same thing is to find
# a regular file we know is in the workspace (i.e. the WORKSPACE
# file itself) and then use dirname to get the path of the workspace
# root.
workspace_file_path = repository_ctx.path(
Label("@nixpkgs_cc_toolchain//:WORKSPACE")
)
workspace_root = _execute_or_fail(
repository_ctx,
["dirname", workspace_file_path],
).stdout.rstrip()

# Make a list of all available tools in the Nix derivation. Override
# the Bazel autoconfiguration with the tools we found.
bin_contents = _find_children(repository_ctx, workspace_root + "/bin")
overriden_tools = {
tool: repository_ctx.path(Label("@nixpkgs_cc_toolchain//:bin/" + tool))
for entry in bin_contents
for tool in [entry.rpartition("/")[-1]] # Compute basename
}
cc_autoconf_impl(repository_ctx, overriden_tools = overriden_tools)

_nixpkgs_cc_autoconf = repository_rule(
implementation = _nixpkgs_cc_autoconf_impl
)


def nixpkgs_cc_configure(
repository = None,
repositories = None,
nix_file_content = """
with import <nixpkgs> {}; buildEnv {
name = "bazel-cc-toolchain";
paths = [ gcc binutils ];
}
"""):
"""Use a CC toolchain from Nixpkgs.

By default, Bazel auto-configures a CC toolchain from commands (e.g.
`gcc`) available in the environment. To make builds more hermetic, use
this rule to specific explicitly which commands the toolchain should
use.
"""
if repository and repositories or not repository and not repositories:
fail("Specify one of repository or repositories (but not both).")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this logic in both nixpkgs_package and here. Actually, nixpkgs_package silently overwrites repository if repositories has a "nixpkgs" field, which is a bug. Should be factored out.

elif repository:
repositories = {"nixpkgs": repository}

nixpkgs_package(
name = "nixpkgs_cc_toolchain",
repositories = repositories,
nix_file_content = nix_file_content,
build_file_content = """exports_files(glob(["bin/*"]))""",
)
# Following lines should match
# https://github.com/bazelbuild/bazel/blob/master/tools/cpp/cc_configure.bzl#L93.
_nixpkgs_cc_autoconf(name = "local_config_cc")
native.bind(name = "cc_toolchain", actual = "@local_config_cc//:toolchain")
native.register_toolchains("@local_config_cc//:all")


def _execute_or_fail(repository_ctx, arguments, failure_message = "", *args, **kwargs):
"""Call repository_ctx.execute() and fail if non-zero return code."""
result = repository_ctx.execute(arguments, *args, **kwargs)
if result.return_code:
outputs = dict(
failure_message = failure_message,
arguments = arguments,
return_code = result.return_code,
stderr = result.stderr,
)
fail("""
{failure_message}
Command: {arguments}
Return code: {return_code}
Error output:
{stderr}
""").format(**outputs)
return result


def _find_children(repository_ctx, target_dir):
find_args = [
_executable_path(repository_ctx, "find"),
"-L",
target_dir,
"-maxdepth", "1",
# otherwise the directory is printed as well
"-mindepth", "1",
# filenames can contain \n
"-print0",
]
find_res = repository_ctx.execute(find_args)
if find_res.return_code == 0:
for target in find_res.stdout.rstrip("\0").split("\0"):
basename = target.rpartition("/")[-1]
repository_ctx.symlink(target, basename)
else:
_execute_error(find_res)

exec_result = _execute_or_fail(repository_ctx, find_args)
return exec_result.stdout.rstrip("\0").split("\0")


def _executable_path(repository_ctx, exe_name, extra_msg=""):
"""Try to find the executable, fail with an error."""
Expand All @@ -170,19 +256,3 @@ def _executable_path(repository_ctx, exe_name, extra_msg=""):
fail("Could not find the `{}` executable in PATH.{}\n"
.format(exe_name, " " + extra_msg if extra_msg else ""))
return path


def _execute_error(exec_result, msg):
"""Print a nice error message for a failed `execute`."""
fail("""
execute() error: {msg}
status code: {code}
stdout:
{stdout}
stderr:
{stderr}
""".format(
msg=msg,
code=exec_result.return_code,
stdout=exec_result.stdout,
stderr=exec_result.stderr))
6 changes: 6 additions & 0 deletions tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ package(default_testonly = 1)
timeout = "short",
)
]

# Test nixpkgs_cc_configure() by building some CC code.
cc_binary(
name = "cc-test",
srcs = ["cc-test.cc"],
)
1 change: 1 addition & 0 deletions tests/cc-test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
int main() { return 0; }