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

Use stack ls dependencies json #1364

Merged
merged 35 commits into from
Jun 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
91bac6e
a bzl JSON parser
erickj Mar 15, 2018
a50a8e9
adds mode and state transition logging
erickj Mar 15, 2018
7ba4197
adds mode and state transition logging
erickj Mar 15, 2018
eb6eb8d
adds hook functions to the json checker
erickj Mar 15, 2018
a667a37
trying to accumulate tokens
erickj Mar 16, 2018
7ff079e
notes and reductions
erickj Mar 17, 2018
44f1752
tmp commit: reductino hooks
erickj Mar 17, 2018
76c4f19
tokenize and reduce
erickj Mar 18, 2018
beb34de
something that might actually work
erickj Mar 18, 2018
39b7b9c
cleanup
erickj Mar 18, 2018
620e87e
testing stuff
erickj Mar 18, 2018
e531c7f
parser rename
erickj Mar 18, 2018
d05988c
adds tests
erickj Mar 18, 2018
31d89c5
adds some hacks for more permissive parsing:
erickj Mar 18, 2018
2196d12
adds some lossy number handling
erickj Mar 19, 2018
959ba3c
adds IGNORE warning to json_rules.bzl
erickj Mar 19, 2018
0d26dfd
nit picky cleanups
erickj Mar 19, 2018
dbb72bb
adds MIT LICENSE
erickj Jan 2, 2019
fe53b34
replaces deprecated native http_archive with load from @bazel_tools
erickj Jan 2, 2019
e954ef2
fixes deprecated '/', see https://github.com/bazelbuild/bazel/issues/…
erickj Jan 2, 2019
ed12dc7
Add 'vendor/bazel_json/' from commit 'e954ef2c28cd92d97304810e8999e11…
aherrmann Jun 22, 2020
3207f62
Exclude vendor from buildifier checks
aherrmann Jun 11, 2020
dd6a6c7
Patch bazel_json load commands
aherrmann Jun 22, 2020
58ebc20
Fix bazel_json unittests
aherrmann Jun 22, 2020
fd9ccea
bazel_json remove WORKSPACE
aherrmann Jun 22, 2020
ed42fd2
Bump minimum stack version
aherrmann Jun 11, 2020
7e363e5
bazel_json bzl_library
aherrmann Jun 22, 2020
c98ff78
use ls dependencies json instead of dot
aherrmann Jun 11, 2020
7f9f8c6
Use ls dependencies json instead of ls dependencies
aherrmann Jun 11, 2020
3ccda9c
stack --external is enabled by default
aherrmann Jun 11, 2020
7f68186
Parse JSON only once
aherrmann Jun 11, 2020
06ab5da
Separate dependency resolution from unpack
aherrmann Jun 12, 2020
761db29
Merge dependencies loop into package_spec loop
aherrmann Jun 12, 2020
dd86d14
Validate stack ls dependencies json output
aherrmann Jun 22, 2020
4eb6b77
bazel_json exports_files
aherrmann Jun 22, 2020
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
6 changes: 6 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ exports_files(
visibility = ["//tests/shellcheck:__pkg__"],
)

buildifier_exclude_patterns = [
"./vendor/**",
]

# Run this to check for errors in BUILD files.
buildifier(
name = "buildifier",
exclude_patterns = buildifier_exclude_patterns,
mode = "check",
)

# Run this to fix the errors in BUILD files.
buildifier(
name = "buildifier-fix",
exclude_patterns = buildifier_exclude_patterns,
mode = "fix",
verbose = True,
)
8 changes: 8 additions & 0 deletions haskell/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ exports_files(
visibility = ["//tests/shellcheck:__pkg__"],
)

bzl_library(
name = "bazel_json",
srcs = [
"//vendor/bazel_json/lib:json_parser.bzl",
],
)

# @bazel_tools//tools does not define a bzl_library itself, instead we are
# supposed to define our own using the @bazel_tools//tools:bzl_srcs filegroup.
# See https://github.com/bazelbuild/skydoc/issues/166
Expand All @@ -57,6 +64,7 @@ bzl_library(
srcs = glob(["**/*.bzl"]),
visibility = ["//visibility:public"],
deps = [
":bazel_json",
":bazel_tools",
"@bazel_skylib//lib:collections",
"@bazel_skylib//lib:dicts",
Expand Down
202 changes: 120 additions & 82 deletions haskell/cabal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
load("@bazel_skylib//lib:dicts.bzl", "dicts")
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
load("//vendor/bazel_json/lib:json_parser.bzl", "json_parse")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load(":cc.bzl", "cc_interop_info")
load(":private/actions/info.bzl", "library_info_output_groups")
Expand Down Expand Up @@ -827,7 +828,8 @@ def _stack_version_check(repository_ctx, stack_cmd):
exec_result = _execute_or_fail_loudly(repository_ctx, [stack_cmd, "--numeric-version"])

stack_major_version = int(exec_result.stdout.split(".")[0])
return stack_major_version >= 2
stack_minor_version = int(exec_result.stdout.split(".")[1])
return stack_major_version >= 2 and stack_minor_version >= 3

def _parse_components(package, components):
"""Parse and validate a list of Cabal components.
Expand Down Expand Up @@ -874,13 +876,40 @@ def _parse_components(package, components):
return struct(lib = lib, exe = exe)

_default_components = {
"alex": ["exe"],
"c2hs": ["exe"],
"cpphs": ["lib", "exe"],
"doctest": ["lib", "exe"],
"happy": ["exe"],
"alex": struct(lib = False, exe = ["alex"]),
"c2hs": struct(lib = False, exe = ["c2hs"]),
"cpphs": struct(lib = True, exe = ["cpphs"]),
"doctest": struct(lib = True, exe = ["doctest"]),
"happy": struct(lib = False, exe = ["happy"]),
}

def _get_components(components, package):
"""Look-up the components of a package.

If the package is not listed in the user-defined components then it
will be taken from the `_default_components`. If it is not listed
there then it will default to a library and no executable components.
"""
return components.get(package, _default_components.get(package, struct(lib = True, exe = [])))

def _validate_package_specs(package_specs):
found_ty = type(package_specs)
if found_ty != "list":
fail("Unexpected output format for `stack ls dependencies json`. Expected 'list', but got '%s'." % found_ty)

def _validate_package_spec(package_spec):
fields = [
("name", "string"),
("version", "string"),
("dependencies", "list"),
]
for (field, ty) in fields:
if not field in package_spec:
fail("Unexpected output format for `stack ls dependencies json`. Missing field '%s'." % field)
found_ty = type(package_spec[field])
if found_ty != ty:
fail("Unexpected output format for `stack ls dependencies json`. Expected field '%s' of type '%s', but got type '%s'." % (field, ty, found_ty))

def _compute_dependency_graph(repository_ctx, snapshot, core_packages, versioned_packages, unversioned_packages, vendored_packages, user_components):
"""Given a list of root packages, compute a dependency graph.

Expand Down Expand Up @@ -916,67 +945,95 @@ def _compute_dependency_graph(repository_ctx, snapshot, core_packages, versioned
if not versioned_packages and not unversioned_packages and not vendored_packages:
return all_packages

# Unpack all given packages, then compute the transitive closure
# and unpack anything in the transitive closure as well.
# Create a dummy package depending on all requested packages.
resolve_package = "rules-haskell-stack-resolve"
repository_ctx.file(
"{name}/{name}.cabal".format(name = resolve_package),
executable = False,
content = """\
name: {name}
cabal-version: >= 1.2
version: 1.0
library
build-depends:
{packages}
""".format(
name = resolve_package,
packages = ",\n ".join(core_packages + unversioned_packages + vendored_packages.keys() + [
_chop_version(pkg)
for pkg in versioned_packages
]),
),
)

# Create a stack.yaml capturing user overrides to the snapshot.
stack_yaml_content = struct(**{
"resolver": str(snapshot),
"packages": [resolve_package] + [
# Determines path to vendored package's root directory relative to
# stack.yaml. Note, this requires that the Cabal file exists in the
# package root and is called `<name>.cabal`.
truly_relativize(
str(repository_ctx.path(label.relative(name + ".cabal")).dirname),
relative_to = str(repository_ctx.path("stack.yaml").dirname),
)
for (name, label) in vendored_packages.items()
],
"extra-deps": versioned_packages,
"flags": {
pkg: {
flag[1:] if flag.startswith("-") else flag: not flag.startswith("-")
for flag in flags
}
for (pkg, flags) in repository_ctx.attr.flags.items()
},
}).to_json()
repository_ctx.file("stack.yaml", content = stack_yaml_content, executable = False)

# Invoke stack to calculate the transitive dependencies.
stack_cmd = repository_ctx.path(repository_ctx.attr.stack)
if not _stack_version_check(repository_ctx, stack_cmd):
fail("Stack version not recent enough. Need version 2.1 or newer.")
fail("Stack version not recent enough. Need version 2.3 or newer.")
stack = [stack_cmd]

if versioned_packages:
_execute_or_fail_loudly(repository_ctx, stack + ["unpack"] + versioned_packages)
stack = [stack_cmd, "--resolver", snapshot]
if unversioned_packages:
_execute_or_fail_loudly(repository_ctx, stack + ["unpack"] + unversioned_packages)
exec_result = _execute_or_fail_loudly(repository_ctx, ["ls"])
unpacked_sdists = exec_result.stdout.splitlines()

# Determines path to vendored package's root directory relative to stack.yaml.
# Note, this requires that the Cabal file exists in the package root and is
# called `<name>.cabal`.
vendored_sdists = [
truly_relativize(
str(repository_ctx.path(label.relative(name + ".cabal")).dirname),
relative_to = str(repository_ctx.path("stack.yaml").dirname),
)
for (name, label) in vendored_packages.items()
]
package_flags = {
pkg_name: {
flag[1:] if flag.startswith("-") else flag: not flag.startswith("-")
for flag in flags
}
for (pkg_name, flags) in repository_ctx.attr.flags.items()
}
stack_yaml_content = struct(resolver = "none", packages = unpacked_sdists + vendored_sdists, flags = package_flags).to_json()
repository_ctx.file("stack.yaml", content = stack_yaml_content, executable = False)
exec_result = _execute_or_fail_loudly(
repository_ctx,
stack + ["ls", "dependencies", "--global-hints", "--separator=-"],
stack + ["ls", "dependencies", "json", "--global-hints", "--external"],
)
transitive_unpacked_sdists = []
indirect_unpacked_sdists = []
remaining_components = dicts.add(_default_components, user_components)
for package in exec_result.stdout.splitlines():
name = _chop_version(package)
version = _version(package)
package_specs = json_parse(exec_result.stdout)
_validate_package_specs(package_specs)

aherrmann marked this conversation as resolved.
Show resolved Hide resolved
# Collect package metadata
remaining_components = dict(**user_components)
for package_spec in package_specs:
_validate_package_spec(package_spec)
name = package_spec["name"]
aherrmann marked this conversation as resolved.
Show resolved Hide resolved
if name == resolve_package:
continue
version = package_spec["version"]
package = "%s-%s" % (name, version)
vendored = vendored_packages.get(name, None)
is_core_package = name in _CORE_PACKAGES
all_packages[name] = struct(
name = name,
components = _parse_components(
name,
remaining_components.pop(name, ["lib"]),
),
components = _get_components(remaining_components, name),
version = version,
versioned_name = package,
flags = repository_ctx.attr.flags.get(name, []),
deps = [],
tools = [],
deps = [
dep
for dep in package_spec["dependencies"]
if _get_components(remaining_components, dep).lib
],
tools = [
(dep, exe)
for dep in package_spec["dependencies"]
for exe in _get_components(remaining_components, dep).exe
],
vendored = vendored,
is_core_package = is_core_package,
sdist = None if is_core_package or vendored != None else package,
)
remaining_components.pop(name, None)

if is_core_package or vendored != None:
continue
Expand All @@ -987,42 +1044,19 @@ Could not resolve version of {}. It is not in the snapshot.
Specify a fully qualified package name of the form <package>-<version>.
""".format(package))

transitive_unpacked_sdists.append(package)
if package not in unpacked_sdists:
indirect_unpacked_sdists.append(name)

for package in remaining_components.keys():
if not package in _default_components:
fail("Unknown package: %s" % package, "components")

# We removed the version numbers prior to calling `unpack`. This
# way, stack will fetch the package sources from the snapshot
# rather than from Hackage. See #1027.
if indirect_unpacked_sdists:
_execute_or_fail_loudly(repository_ctx, stack + ["unpack"] + indirect_unpacked_sdists)
stack_yaml_content = struct(resolver = "none", packages = transitive_unpacked_sdists + vendored_sdists, flags = package_flags).to_json()
repository_ctx.file("stack.yaml", stack_yaml_content, executable = False)
# Unpack all remote packages.
remote_packages = [
package.name
for package in all_packages.values()
if package.sdist != None
]
if remote_packages:
_execute_or_fail_loudly(repository_ctx, stack + ["--resolver", snapshot, "unpack"] + remote_packages)

# Compute dependency graph.
exec_result = _execute_or_fail_loudly(
repository_ctx,
stack + ["dot", "--global-hints", "--external"],
)
for line in exec_result.stdout.splitlines():
tokens = [w.strip('";') for w in line.split(" ")]

# All lines of the form `"foo" -> "bar";` declare edges of the
# dependency graph in the Graphviz format.
if len(tokens) == 3 and tokens[1] == "->":
[src, _, dest] = tokens
if src in all_packages and dest in all_packages:
if all_packages[dest].components.lib:
all_packages[src].deps.append(dest)
if all_packages[dest].components.exe:
all_packages[src].tools.extend([
(dest, exe)
for exe in all_packages[dest].components.exe
])
return all_packages

def _invert(d):
Expand Down Expand Up @@ -1088,14 +1122,18 @@ def _stack_snapshot_impl(repository_ctx):
versioned_packages.append(package)
else:
unversioned_packages.append(package)
user_components = {
name: _parse_components(name, components)
for (name, components) in repository_ctx.attr.components.items()
}
all_packages = _compute_dependency_graph(
repository_ctx,
snapshot,
core_packages,
versioned_packages,
unversioned_packages,
vendored_packages,
repository_ctx.attr.components,
user_components,
)

extra_deps = _to_string_keyed_label_list_dict(repository_ctx.attr.extra_deps)
Expand Down
20 changes: 20 additions & 0 deletions vendor/bazel_json/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Copyright 2019 Erick Johnson <[email protected]>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2 changes: 2 additions & 0 deletions vendor/bazel_json/lib/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package(default_visibility = ["//:__pkg__"])
exports_files(["json_parser.bzl"])
Loading