diff --git a/WORKSPACE b/WORKSPACE index dc5d4fcd5..41eb2c442 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -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, ) diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index e82abf9a1..a5568df9c 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -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. @@ -1071,6 +1084,7 @@ def _parse_components(package, components): """ lib = False exe = [] + sublibs = [] for component in components: if component == "lib": @@ -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:"): @@ -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): @@ -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. @@ -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", @@ -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"] @@ -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( """ @@ -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"] @@ -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, @@ -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( @@ -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"], @@ -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 = { @@ -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)) @@ -2247,6 +2368,7 @@ def stack_snapshot( setup_deps = {}, tools = [], components = {}, + components_dependencies = {}, stack_update = None, verbose = False, netrc = "", @@ -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:` for an executable component, - `exe` is a short-cut for `exe:`. The library component - will have the label `@//:` and an executable + main library component, `exe:` for an executable component, + and `lib:` for a sublibrary. + `exe` is a short-cut for `exe:`. The main library component + will have the label `@//:` as well as the alias `@//`, an executable component will have the label `@-exe//:`, + and a sublibrary component will have the label `@//:` where `` 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. @@ -2462,6 +2597,7 @@ def stack_snapshot( setup_deps = setup_deps, tools = tools, components = components, + components_dependencies = components_dependencies, verbose = verbose, **kwargs ) diff --git a/stackage-pinning-test.yaml b/stackage-pinning-test.yaml index 23761bc6c..538129d10 100644 --- a/stackage-pinning-test.yaml +++ b/stackage-pinning-test.yaml @@ -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" diff --git a/stackage-pinning-test_snapshot.json b/stackage-pinning-test_snapshot.json index c0a7d7886..bcdaa073d 100644 --- a/stackage-pinning-test_snapshot.json +++ b/stackage-pinning-test_snapshot.json @@ -1,7 +1,8 @@ { - "__GENERATED_FILE_DO_NOT_MODIFY_MANUALLY": -1032897202, - "all-cabal-hashes": "https://raw.githubusercontent.com/commercialhaskell/all-cabal-hashes/99d5aec2576de8d4570da4b9aa4c7843a04bbc3e", + "__GENERATED_FILE_DO_NOT_MODIFY_MANUALLY": 1718163903, + "all-cabal-hashes": "https://raw.githubusercontent.com/commercialhaskell/all-cabal-hashes/09ceefc2bc1acf1cc3772d3aaed66e1f54719e05", "resolved": { + "Cabal": {"dependencies":[],"location":{"type":"core"},"name":"Cabal","version":"3.2.1.0"}, "HUnit": {"dependencies":["base","call-stack","deepseq"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/HUnit-1.6.2.0/HUnit-1.6.2.0.tar.gz"},"name":"HUnit","pinned":{"url":["https://hackage.haskell.org/package/HUnit-1.6.2.0/HUnit-1.6.2.0.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/HUnit-1.6.2.0.tar.gz"],"sha256":"b0b7538871ffc058486fc00740886d2f3172f8fa6869936bfe83a5e10bd744ab","cabal-sha256":"1a79174e8af616117ad39464cac9de205ca923da6582825e97c10786fda933a4"},"version":"1.6.2.0"}, "QuickCheck": {"dependencies":["base","containers","deepseq","random","splitmix","template-haskell","transformers"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/QuickCheck-2.14.2/QuickCheck-2.14.2.tar.gz"},"name":"QuickCheck","pinned":{"url":["https://hackage.haskell.org/package/QuickCheck-2.14.2/QuickCheck-2.14.2.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/QuickCheck-2.14.2.tar.gz"],"sha256":"d87b6c85696b601175274361fa62217894401e401e150c3c5d4013ac53cd36f3","cabal-sha256":"4ce29211223d5e6620ebceba34a3ca9ccf1c10c0cf387d48aea45599222ee5aa"},"version":"2.14.2"}, "ansi-terminal": {"dependencies":["base","colour"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/ansi-terminal-0.11/ansi-terminal-0.11.tar.gz"},"name":"ansi-terminal","pinned":{"url":["https://hackage.haskell.org/package/ansi-terminal-0.11/ansi-terminal-0.11.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/ansi-terminal-0.11.tar.gz"],"sha256":"c6611b9e51add41db3f79eac30066c06b33a6ca2a09e586b4b361d7f98303793","cabal-sha256":"97470250c92aae14c4c810d7f664c532995ba8910e2ad797b29f22ad0d2d0194"},"version":"0.11"}, @@ -20,9 +21,10 @@ "hspec-discover": {"dependencies":["base","directory","filepath"],"location":{"type":"archive","url":"https://github.com/hspec/hspec/archive/4a4b27cb1d5284c94228c9c76c5fe79215597fb7.tar.gz"},"name":"hspec-discover","pinned":{"sha256":"fb96ed7dd3e2b792300f3bc8bd2affc6bf78093289815f76ec785fea6d91be68","strip-prefix":"hspec-4a4b27cb1d5284c94228c9c76c5fe79215597fb7/hspec-discover"},"version":"2.7.10"}, "hspec-expectations": {"dependencies":["HUnit","base","call-stack"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/hspec-expectations-0.8.2/hspec-expectations-0.8.2.tar.gz"},"name":"hspec-expectations","pinned":{"url":["https://hackage.haskell.org/package/hspec-expectations-0.8.2/hspec-expectations-0.8.2.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/hspec-expectations-0.8.2.tar.gz"],"sha256":"819607ea1faf35ce5be34be61c6f50f3389ea43892d56fb28c57a9f5d54fb4ef","cabal-sha256":"e2db24881baadc2d9d23b03cb629e80dcbda89a6b04ace9adb5f4d02ef8b31aa"},"version":"0.8.2"}, "mtl": {"dependencies":[],"location":{"type":"core"},"name":"mtl","version":"2.2.2"}, - "primitive": {"dependencies":["base","deepseq","transformers"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/primitive-0.7.1.0/primitive-0.7.1.0.tar.gz"},"name":"primitive","pinned":{"url":["https://hackage.haskell.org/package/primitive-0.7.1.0/primitive-0.7.1.0.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/primitive-0.7.1.0.tar.gz"],"sha256":"6bebecfdf2a57787d9fd5231bfd612b65a92edd7b33a973b2a0f11312b89a3f0","cabal-sha256":"29de6bfd0cf8ba023ceb806203dfbec0e51e3524e75ffe41056f70b4229c6f0f"},"version":"0.7.1.0"}, + "package1": {"dependencies":["Cabal","base"],"location":{"type":"archive","url":"https://github.com/tweag/rules_haskell/raw/e4e74f17f743488f564bd0d69c580106d5b910a5/tests/haskell_cabal_library_sublibrary_name/package1.tar"},"name":"package1","pinned":{"sha256":"302d8ddda8330c825da61fe0a2315c899ab083e641c7716ebdacb5c951682445","strip-prefix":""},"version":"0.1.0.0"}, + "primitive": {"dependencies":["base","deepseq","transformers"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/primitive-0.7.1.0/primitive-0.7.1.0.tar.gz"},"name":"primitive","pinned":{"url":["https://hackage.haskell.org/package/primitive-0.7.1.0/primitive-0.7.1.0.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/primitive-0.7.1.0.tar.gz"],"sha256":"6bebecfdf2a57787d9fd5231bfd612b65a92edd7b33a973b2a0f11312b89a3f0","cabal-sha256":"cef1cfd0eade2be8e276520c897e20ebe3d4a7371d5a06c82648af4181cab326"},"version":"0.7.1.0"}, "quickcheck-io": {"dependencies":["HUnit","QuickCheck","base"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/quickcheck-io-0.2.0/quickcheck-io-0.2.0.tar.gz"},"name":"quickcheck-io","pinned":{"url":["https://hackage.haskell.org/package/quickcheck-io-0.2.0/quickcheck-io-0.2.0.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/quickcheck-io-0.2.0.tar.gz"],"sha256":"fb779119d79fe08ff4d502fb6869a70c9a8d5fd8ae0959f605c3c937efd96422","cabal-sha256":"7bf0b68fb90873825eb2e5e958c1b76126dcf984debb998e81673e6d837e0b2d"},"version":"0.2.0"}, - "random": {"dependencies":["base","bytestring","deepseq","mtl","splitmix"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/random-1.2.0/random-1.2.0.tar.gz"},"name":"random","pinned":{"url":["https://hackage.haskell.org/package/random-1.2.0/random-1.2.0.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/random-1.2.0.tar.gz"],"sha256":"e4519cf7c058bfd5bdbe4acc782284acc9e25e74487208619ca83cbcd63fb9de","cabal-sha256":"30d72df4cc1d2fe2d445c88f0ee9d21965af7ce86660c43a6c32a6a1d90d51c9"},"version":"1.2.0"}, + "random": {"dependencies":["base","bytestring","deepseq","mtl","splitmix"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/random-1.2.0/random-1.2.0.tar.gz"},"name":"random","pinned":{"url":["https://hackage.haskell.org/package/random-1.2.0/random-1.2.0.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/random-1.2.0.tar.gz"],"sha256":"e4519cf7c058bfd5bdbe4acc782284acc9e25e74487208619ca83cbcd63fb9de","cabal-sha256":"195506fedaa7c31c1fa2a747e9b49b4a5d1f0b09dd8f1291f23a771656faeec3"},"version":"1.2.0"}, "setenv": {"dependencies":["base","unix"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/setenv-0.1.1.3/setenv-0.1.1.3.tar.gz"},"name":"setenv","pinned":{"url":["https://hackage.haskell.org/package/setenv-0.1.1.3/setenv-0.1.1.3.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/setenv-0.1.1.3.tar.gz"],"sha256":"e358df39afc03d5a39e2ec650652d845c85c80cc98fe331654deafb4767ecb32","cabal-sha256":"c5916ac0d2a828473cd171261328a290afe0abd799db1ac8c310682fe778c45b"},"version":"0.1.1.3"}, "splitmix": {"dependencies":["base","deepseq"],"location":{"type":"hackage","url":"https://hackage.haskell.org/package/splitmix-0.1.0.3/splitmix-0.1.0.3.tar.gz"},"name":"splitmix","pinned":{"url":["https://hackage.haskell.org/package/splitmix-0.1.0.3/splitmix-0.1.0.3.tar.gz","https://s3.amazonaws.com/hackage.fpcomplete.com/package/splitmix-0.1.0.3.tar.gz"],"sha256":"46009f4b000c9e6613377767b8718bf38476469f2a8e2162d98cc246882d5a35","cabal-sha256":"fc3aae74c467f4b608050bef53aec17904a618731df9407e655d8f3bf8c32d5c"},"version":"0.1.0.3"}, "stm": {"dependencies":[],"location":{"type":"core"},"name":"stm","version":"2.5.0.0"}, diff --git a/tests/stackage_sublibrary/BUILD b/tests/stackage_sublibrary/BUILD new file mode 100644 index 000000000..edeb868d7 --- /dev/null +++ b/tests/stackage_sublibrary/BUILD @@ -0,0 +1,20 @@ +load( + "@rules_haskell//haskell:defs.bzl", + "haskell_test", + "haskell_toolchain_library", +) + +package(default_testonly = 1) + +haskell_toolchain_library(name = "base") + +# We directly depend on the "sublib" sublibrary of the "package1" package. +haskell_test( + name = "stackage_sublibrary", + srcs = ["Main.hs"], + deps = [ + ":base", + "@stackage-pinning-test//package1", + "@stackage-pinning-test//package1:sublib", + ], +) diff --git a/tests/stackage_sublibrary/Main.hs b/tests/stackage_sublibrary/Main.hs new file mode 100644 index 000000000..1e20efaa4 --- /dev/null +++ b/tests/stackage_sublibrary/Main.hs @@ -0,0 +1,5 @@ +module Main where +import SubLib + +main :: IO () +main = print subLibVal