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

sublibraries in stack_snapshot rule #1636

Merged
merged 5 commits into from
Nov 25, 2021
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
14 changes: 13 additions & 1 deletion WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,20 @@ stack_snapshot(

stack_snapshot(
name = "stackage-pinning-test",
components = {
"package1": [
"lib:package1",
"lib:sublib",
],
},
components_dependencies = {
"package1": """{"lib:package1": ["lib:sublib"]}""",
},
local_snapshot = "//:stackage-pinning-test.yaml",
packages = ["hspec"],
packages = [
"hspec",
"package1",
],
stack_snapshot_json = "//:stackage-pinning-test_snapshot.json" if not is_windows else None,
)

Expand Down
162 changes: 149 additions & 13 deletions haskell/cabal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,19 @@ def _stack_version_check(repository_ctx, stack_cmd):
stack_minor_version = int(exec_result.stdout.split(".")[1])
return stack_major_version >= 2 and stack_minor_version >= 3

def _resolve_component_target_name(package, component):
if component in ["lib", "lib:%s" % package]:
return package
if component.startswith("lib:"):
return "_{}_lib_{}".format(package, component[4:])
if component == "exe":
return "_{}_exe_{}".format(package, package)
if component.startswith("exe:"):
return "_{}_exe_{}".format(package, component[4:])

# If the string is not a valid component, it may already be a label so we return it as such.
return component

def _parse_components(package, components):
"""Parse and validate a list of Cabal components.

Expand All @@ -1071,6 +1084,7 @@ def _parse_components(package, components):
"""
lib = False
exe = []
sublibs = []

for component in components:
if component == "lib":
Expand All @@ -1079,7 +1093,8 @@ def _parse_components(package, components):
if component == "lib:%s" % package:
lib = True
else:
fail("Sublibrary components are not supported: %s in %s" % (component, package), "components")
sublibs.append(component[4:])

elif component == "exe":
exe.append(package)
elif component.startswith("exe:"):
Expand All @@ -1093,14 +1108,14 @@ def _parse_components(package, components):
if not lib or exe != []:
fail("Invalid core package components: %s" % package, "components")

return struct(lib = lib, exe = exe)
return struct(lib = lib, exe = exe, sublibs = sublibs)

_default_components = {
"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"]),
"alex": struct(lib = False, exe = ["alex"], sublibs = []),
"c2hs": struct(lib = False, exe = ["c2hs"], sublibs = []),
"cpphs": struct(lib = True, exe = ["cpphs"], sublibs = []),
"doctest": struct(lib = True, exe = ["doctest"], sublibs = []),
"happy": struct(lib = False, exe = ["happy"], sublibs = []),
}

def _get_components(components, package):
Expand All @@ -1110,7 +1125,7 @@ def _get_components(components, package):
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 = [])))
return components.get(package, _default_components.get(package, struct(lib = True, exe = [], sublibs = [])))

def _parse_json_field(json, field, ty, errmsg):
"""Read and type-check a field from a JSON object.
Expand Down Expand Up @@ -1918,6 +1933,11 @@ def _stack_snapshot_impl(repository_ctx):
extra_deps = _to_string_keyed_label_list_dict(repository_ctx.attr.extra_deps)
tools = [_label_to_string(label) for label in repository_ctx.attr.tools]

components_dependencies = {
comp: json_parse(deps)
for comp, deps in repository_ctx.attr.components_dependencies.items()
}

# Write out dependency graph as importable Starlark value.
repository_ctx.file(
"packages.bzl",
Expand All @@ -1931,6 +1951,7 @@ packages = {
version = spec["version"],
library = all_components[name].lib,
executables = all_components[name].exe,
sublibs = all_components[name].sublibs,
deps = [
Label("@{}//:{}".format(repository_ctx.name, dep))
for dep in spec["dependencies"]
Expand Down Expand Up @@ -1966,6 +1987,7 @@ load("@rules_haskell//haskell:defs.bzl", "haskell_library", "haskell_toolchain_l
version = spec["version"]
package = "%s-%s" % (name, version)
visibility = visibilities[name]
package_components_dependencies = components_dependencies.get(name, {})
if name in vendored_packages:
build_file_builder.append(
"""
Expand Down Expand Up @@ -2001,6 +2023,13 @@ haskell_library(
_label_to_string(label)
for label in extra_deps.get(name, [])
]

main_lib_deps = [
_resolve_component_target_name(name, c)
for lib in ["lib", "lib:{}".format(name)]
for c in package_components_dependencies.get(lib, [])
]

library_tools = [
"_%s_exe_%s" % (dep, exe)
for dep in spec["dependencies"]
Expand Down Expand Up @@ -2033,7 +2062,7 @@ haskell_cabal_library(
haddock = repr(repository_ctx.attr.haddock),
flags = repository_ctx.attr.flags.get(name, []),
dir = package,
deps = library_deps,
deps = library_deps + main_lib_deps,
setup_deps = setup_deps,
tools = library_tools,
visibility = visibility,
Expand All @@ -2048,6 +2077,11 @@ haskell_cabal_library(
),
)
for exe in all_components[name].exe:
exe_component_deps = [
_resolve_component_target_name(name, comp_dep)
for comp in ["exe:{}".format(exe)] + (["exe"] if exe == name else [])
for comp_dep in package_components_dependencies.get(comp, [])
]
build_file_builder.append(
"""
haskell_cabal_binary(
Expand All @@ -2056,6 +2090,7 @@ haskell_cabal_binary(
flags = {flags},
srcs = glob(["{dir}/**"]),
deps = {deps},
setup_deps = {setup_deps},
tools = {tools},
visibility = ["@{workspace}-exe//{name}:__pkg__"],
cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"],
Expand All @@ -2067,15 +2102,55 @@ haskell_cabal_binary(
exe = exe,
flags = repository_ctx.attr.flags.get(name, []),
dir = package,
deps = library_deps + ([name] if all_components[name].lib else []),
deps = library_deps + exe_component_deps + ([name] if all_components[name].lib else []),
setup_deps = setup_deps,
tools = library_tools,
verbose = repr(repository_ctx.attr.verbose),
),
)
for sublib in all_components[name].sublibs:
sublib_component_deps = [
_resolve_component_target_name(name, c)
for c in package_components_dependencies.get("lib:".format(sublib), [])
]
build_file_builder.append(
"""
haskell_cabal_library(
name = "_{name}_lib_{sublib}",
package_name = "{name}",
version = "{version}",
haddock = {haddock},
sublibrary_name = "{sublib}",
flags = {flags},
srcs = glob(["{dir}/**"]),
deps = {deps},
setup_deps = {setup_deps},
tools = {tools},
visibility = {visibility},
cabalopts = ["--ghc-option=-w", "--ghc-option=-optF=-w"],
verbose = {verbose},
)
""".format(
workspace = repository_ctx.name,
name = name,
version = version,
haddock = repr(repository_ctx.attr.haddock),
sublib = sublib,
flags = repository_ctx.attr.flags.get(name, []),
dir = package,
deps = library_deps + sublib_component_deps,
setup_deps = setup_deps,
tools = library_tools,
verbose = repr(repository_ctx.attr.verbose),
visibility = visibility,
),
)
build_file_content = "\n".join(build_file_builder)
repository_ctx.file("BUILD.bazel", build_file_content, executable = False)

# Create aliases to the libraries.
_stack_sublibraries(repository_ctx, all_components)

_stack_snapshot_unpinned = repository_rule(
_stack_snapshot_unpinned_impl,
attrs = {
Expand Down Expand Up @@ -2105,12 +2180,58 @@ _stack_snapshot = repository_rule(
"setup_deps": attr.string_list_dict(),
"tools": attr.label_list(),
"components": attr.string_list_dict(),
"components_dependencies": attr.string_dict(),
"stack": attr.label(),
"stack_update": attr.label(),
"verbose": attr.bool(default = False),
},
)

def _stack_sublibraries(repository_ctx, all_components):
workspace = repository_ctx.name
for (package, components) in all_components.items():
main_lib_str = ""
sublibraries_str = ""
if components.lib:
main_lib_str = """\
alias(
name = "{package}",
actual = "@{workspace}//:{package}",
visibility = packages["{package}"].visibility,
)
""".format(
package = package,
workspace = workspace,
)
if components.sublibs:
sublibraries_str = """\
[
alias(
name = sublib,
actual = "@{workspace}//:_{package}_lib_" + sublib,
visibility = packages["{package}"].visibility,
)
for sublib in packages["{package}"].sublibs
]
""".format(
workspace = workspace,
package = package,
)
if main_lib_str or sublibraries_str:
repository_ctx.file(
package + "/BUILD.bazel",
executable = False,
content = """\
load("@{workspace}//:packages.bzl", "packages")
{main_lib_str}
{sublibraries_str}
""".format(
workspace = workspace,
main_lib_str = main_lib_str,
sublibraries_str = sublibraries_str,
),
)

def _stack_executables_impl(repository_ctx):
workspace = repository_ctx.name[:-len("-exe")]
all_components = json.decode(repository_ctx.read(repository_ctx.attr.components_json))
Expand Down Expand Up @@ -2247,6 +2368,7 @@ def stack_snapshot(
setup_deps = {},
tools = [],
components = {},
components_dependencies = {},
stack_update = None,
verbose = False,
netrc = "",
Expand Down Expand Up @@ -2401,12 +2523,25 @@ def stack_snapshot(
the tools are executed as part of the build.
components: Defines which Cabal components to build for each package.
A dict from package name to list of components. Use `lib` for the
library component and `exe:<exe-name>` for an executable component,
`exe` is a short-cut for `exe:<package-name>`. The library component
will have the label `@<workspace>//:<package>` and an executable
main library component, `exe:<exe-name>` for an executable component,
and `lib:<sublib-name>` for a sublibrary.
`exe` is a short-cut for `exe:<package-name>`. The main library component
will have the label `@<workspace>//:<package>` as well as the alias `@<workspace>//<package>`, an executable
component will have the label `@<workspace>-exe//<package>:<exe-name>`,
and a sublibrary component will have the label `@<workspace>//<package>:<sublib-name>`
where `<workspace>` is the name given to the `stack_snapshot`
invocation.
components_dependencies: Internal dependencies between package components.
For each package, these dependencies are described as a string representing a JSON dictionary of lists.
(WARNING: this will likely change in the future).
The most common case is the following, where the main library of a package depends on sublibraries:

```
components_dependencies = {
"package-name": \"""{"lib:package-name": ["lib:sublib1", "lib:sublib2"]}\""",
},
```

stack: The stack binary to use to enumerate package dependencies.
haddock: Whether to generate haddock documentation.
verbose: Whether to show the output of the build.
Expand Down Expand Up @@ -2462,6 +2597,7 @@ def stack_snapshot(
setup_deps = setup_deps,
tools = tools,
components = components,
components_dependencies = components_dependencies,
verbose = verbose,
**kwargs
)
Expand Down
3 changes: 3 additions & 0 deletions stackage-pinning-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ packages:
- hspec-contrib
- hspec-core
- hspec-discover

- archive: https://github.com/tweag/rules_haskell/raw/e4e74f17f743488f564bd0d69c580106d5b910a5/tests/haskell_cabal_library_sublibrary_name/package1.tar
sha256: "302d8ddda8330c825da61fe0a2315c899ab083e641c7716ebdacb5c951682445"
Loading