From f602e1bb5f121ac19fc9d266014bc2613f4a94b4 Mon Sep 17 00:00:00 2001 From: Simon Marlow Date: Thu, 1 Oct 2026 12:23:56 +0100 Subject: [PATCH] Make srcs :: { module_name : src_file } This is simpler. Currently when the module name differs from the source file, we do something like srcs = { "Main.hs" : "src/test.hs" } but "Main.hs" is just translated into the module name by the prelude's `haskell_*()` rules, because it needs to know the filename that GHC will dump into `-odir`. So giving it the module name directly is simpler. Furthermore this lets us omit a layer of `export_file()` rules in `_resolve_src()`. --- example/BUCK | 2 +- haskell.bzl | 48 +++++--------------------------- prelude/decls/haskell_common.bzl | 7 ++++- prelude/haskell/compile.bzl | 4 +-- prelude/haskell/haskell.bzl | 7 +++-- prelude/haskell/util.bzl | 9 ++++-- 6 files changed, 26 insertions(+), 51 deletions(-) diff --git a/example/BUCK b/example/BUCK index ff161a4..efa6270 100644 --- a/example/BUCK +++ b/example/BUCK @@ -43,6 +43,6 @@ haskell_library( haskell_test( name = "example-test", - srcs = {"Main.hs": "test/Main.hs"}, + srcs = { "Main" : "test/Main.hs" }, deps = [":example"], ) diff --git a/haskell.bzl b/haskell.bzl index c02d6c4..2797d9d 100644 --- a/haskell.bzl +++ b/haskell.bzl @@ -21,6 +21,8 @@ load("//buck2:alex_happy.bzl", "alex", "happy") load("//buck2:hsc2hs.bzl", "hsc2hs") +load("@prelude//haskell/util.bzl", "src_to_module_name") +load("@prelude//paths.bzl", "paths") # Packages implicitly needed by every Haskell target. AUTO_PACKAGES = ["base", "rts"] @@ -206,15 +208,8 @@ def hs_module_path(path): return path[:-len(ext)] + ".hs" return path -def _resolve_src(name, path, src, deps, hsc_flags): - # `path` is the module-derived path this source should end up at (e.g. - # what its module name maps to); `src` is the actual file, which may - # differ from `path` for a source living outside its module's directory - # layout (see the dict form of `srcs`, below). Whatever we return here - # always ends up in a plain *list* passed to the native rule - srcs as a - # dict is deprecated - so every branch must produce an artifact whose - # own path already matches `path`. - out = hs_module_path(path) +def _resolve_src(name, src, deps, hsc_flags): + out = paths.replace_extension(src, ".hs") if src.endswith(".hsc"): rule_name = name + "-hsc-" + out.replace("/", "_") hsc2hs(name = rule_name, hsc_file = src, out = out, deps = deps, extra_flags = hsc_flags) @@ -227,41 +222,12 @@ def _resolve_src(name, path, src, deps, hsc_flags): rule_name = name + "-happy-" + out.replace("/", "_") happy(name = rule_name, src = src, out = out) return ":" + rule_name - elif path == src or src.endswith("[" + path + "]"): - # Already at the right path: either a real file living exactly - # there, or a sub-target reference (e.g. from thrift_compile() via - # thrift_library() in thrift.bzl) whose bracketed key already - # equals `path` - its own artifact's - # short_path is already correct, so relocating it again would just - # be a redundant copy. - return src else: - # A real (already-.hs) source that doesn't live at its module path - - # relocate it with export_file() (the same ctx.actions.copy_file() - # primitive export_file.bzl itself uses), so the native rule always - # sees a correctly-pathed source. This is what makes a module - # registered correctly in the package db even when nothing in this - # target imports it directly (i.e. it's only consumed by a *different* - # target depending on this one) - confirmed empirically: a plain - # (non-relocated) src with the wrong derived path still compiles - # within its own target (GHC resolves same-target imports from the - # sources' own `module X where` headers, not buck2's bookkeeping), - # but a cross-target `import` of it fails, since haskell_library() - # registers the *derived* path as the exposed module name. - rule_name = name + "-mv-" + path.replace("/", "_") - native.export_file(name = rule_name, src = src, out = path) - return ":" + rule_name + return src -# `srcs` is usually a list, where each file's own path (relative to this -# BUCK package) determines its module name. A dict `{modulePath: file}` is -# also accepted for the rare case where a source doesn't live at the path -# its module name implies (e.g. a shared `plugins/` directory holding -# modules that belong under the main package's namespace) - internally -# resolved to a plain list (see _resolve_src) since dict-form srcs on the -# native rule is deprecated. def _resolve_srcs(name, srcs, deps, hsc_flags): - items = srcs.items() if type(srcs) == type({}) else [(src, src) for src in srcs] - return [_resolve_src(name, path, src, deps, hsc_flags) for path, src in items] + items = srcs.items() if type(srcs) == type({}) else [(src_to_module_name(src), src) for src in srcs] + return { modl: _resolve_src(name, src, deps, hsc_flags) for modl, src in items } def haskell_library( name, diff --git a/prelude/decls/haskell_common.bzl b/prelude/decls/haskell_common.bzl index 5fe337d..5c5444c 100644 --- a/prelude/decls/haskell_common.bzl +++ b/prelude/decls/haskell_common.bzl @@ -18,7 +18,12 @@ def _srcs_arg(): sorted = True, default = [], doc = """ - A list of Haskell sources to be built by this rule. The dictionary option is deprecated. + A list of Haskell sources to be built by this rule. In the list form, + each source file must match the module name with "." replaced by "/", e.g. + `[ "Main.hs", "Other/Module.hs" ]`. + If the source files don't match the module names 1:1, then you + can use the dict form, in which the keys are module names, e.g. + `{ "Main": "src/test.hs", "Other.Module" : "src/Other/Module.hs" }` """, ), } diff --git a/prelude/haskell/compile.bzl b/prelude/haskell/compile.bzl index ac37c10..5789349 100644 --- a/prelude/haskell/compile.bzl +++ b/prelude/haskell/compile.bzl @@ -232,10 +232,10 @@ def compile_args(ctx: AnalysisContext, link_style: LinkStyle, enable_profiling: arg_srcs = [] hidden_srcs = [] - for path, src in srcs_to_pairs(ctx.attrs.srcs): + for _, src in srcs_to_pairs(ctx.attrs.srcs): # hs-boot files aren't expected to be an argument to compiler but does need # to be included in the directory of the associated src file - if is_haskell_src(path): + if is_haskell_src(src): arg_srcs.append(src) else: hidden_srcs.append(src) diff --git a/prelude/haskell/haskell.bzl b/prelude/haskell/haskell.bzl index 70b6e98..f3a5378 100644 --- a/prelude/haskell/haskell.bzl +++ b/prelude/haskell/haskell.bzl @@ -90,6 +90,7 @@ load( "is_haskell_src", "output_extensions", "src_to_module_name", + "module_name_to_file", "srcs_to_pairs", ) load( @@ -369,10 +370,10 @@ def haskell_prebuilt_library_impl(ctx: AnalysisContext) -> list[Provider]: def _srcs_to_objfiles(ctx: AnalysisContext, odir: Artifact, osuf: str) -> list[Artifact]: objfiles = [] - for src, _ in srcs_to_pairs(ctx.attrs.srcs): + for modl, src in srcs_to_pairs(ctx.attrs.srcs): # Don't link boot sources, as they're only meant to be used for compiling. if is_haskell_src(src): - objfiles.append(odir.project(paths.replace_extension(src, "." + osuf))) + objfiles.append(odir.project(module_name_to_file(modl, osuf))) return objfiles _REGISTER_PACKAGE = """\ @@ -437,7 +438,7 @@ def _make_package( hi_link_style = hi_link_style if hi_link_style != None else link_style # Don't expose boot sources, as they're only meant to be used for compiling. - modules = [src_to_module_name(x) for x, _ in srcs_to_pairs(ctx.attrs.srcs) if is_haskell_src(x)] + modules = [x for x, src in srcs_to_pairs(ctx.attrs.srcs) if is_haskell_src(src)] if enable_profiling: # Add the `-p` suffix otherwise ghc will look for objects diff --git a/prelude/haskell/util.bzl b/prelude/haskell/util.bzl index 42546ad..ba0ff0c 100644 --- a/prelude/haskell/util.bzl +++ b/prelude/haskell/util.bzl @@ -42,16 +42,19 @@ def srcs_to_pairs(srcs) -> list[(str, Artifact)]: if type(srcs) == type({}): return srcs.items() else: - return [(src.short_path, src) for src in srcs] + return [(src_to_module_name(src.short_path), src) for src in srcs] -def is_haskell_src(x: str) -> bool: - _, ext = paths.split_extension(x) +def is_haskell_src(x: Artifact) -> bool: + _, ext = paths.split_extension(x.short_path) return ext in HASKELL_EXTENSIONS def src_to_module_name(x: str) -> str: base, _ext = paths.split_extension(x) return base.replace("/", ".") +def module_name_to_file(x: str, ext: str) -> str: + return x.replace(".", "/") + "." + ext + def attr_deps(ctx: AnalysisContext) -> list[Dependency]: return ctx.attrs.deps