packages feed

cabal-buck2 (empty) → 0.1.0.0

raw patch · 39 files changed

+3378/−0 lines, 39 filesdep +Cabaldep +Cabal-syntaxdep +async

Dependencies added: Cabal, Cabal-syntax, async, base, bytestring, cabal-buck2, cabal-install, containers, directory, filepath, process, stm

Files

+ README.md view
@@ -0,0 +1,375 @@+# Buck2 build system for Haskell projects++Short summary: this is a [cabal external command][ext] `cabal buck2`+that allows you to use [Buck2](https://buck2.build) as the build+system for your Cabal project.++# Quick start++First [download a buck2+binary](https://buck2.build/docs/getting_started/install/), unpack it+and put it on your `PATH`.++Then++```+cabal install cabal-buck2+```++Then in the root of your project or package:++```+git clone https://github.com/simonmar/haskell-buck2.git buck2+cabal buck2 --enable-tests+```++Then you can use `buck2` as the build tool, e.g.++```+buck2 build //...+```++To build all the components, or++```+buck2 test //...+```++To run your tests.++Note that you need to re-run `cabal buck2 --enable-tests` if you+modify the `cabal.project` or any of the `.cabal` files.++Not all Cabal features are supported - see [limitations](#limitations).++# Why?++(skip this section if you know why you want buck2)++Why might you want to use `buck2` as the build system compared with+just using `cabal`? Well, first off let me be clear that you *still+need Cabal*, because the Buck2 support doesn't know how to solve+package dependencies or build them. So the workflow consists of first+running `cabal buck2` to solve and build the dependencies, but once you've+done that you can switch to `buck2` for building. The idea is that+`buck2` is a more pleasant experience because:++* It's [faster than Cabal, particularly for rebuilds](#performance).++* It supports different build modes out of the box: the default is to+  build in `dev` mode (unoptimised with dynamic linking) but adding+  `-m opt` gives you optimisation and static linking. Note that Cabal+  doesn't have a purely dynamic build mode: it always uses `-dynamic-too`+  for libraries, which has a significant built-time performance cost.++* The Buck2 build is extensible. If you have anything that needs to be+  generated as part of your build, or any non-standard tooling, then+  hooking that up using Buck2 is far easier than Cabal.  Furthermore+  Buck2 knows how to rebuild things correctly when either the build+  system or the code generator components change.++* It works a lot better than Cabal when you have non-Haskell code (e.g. C/C++ or Rust) in your project, because+  * Buck2 understands dependencies between C/C++ source files and header files (Cabal doesn't: [issue #4306](https://github.com/haskell/cabal/issues/4306)), so when you modify a C/C++ header the correct things are rebuilt.+  * Buck2 builds C/C++ files in parallel, while Cabal doesn't ([issue #7127](https://github.com/haskell/cabal/issues/7127))++* You can use [remote execution and caching](https://buck2.build/docs/users/remote_execution/) (I haven't tried this with `cabal buck2` yet).++Finally, if you have an existing codebase using Buck2 then this is the+basis of something that could "buckify" Cabal packages to integrate+into your build system. It needs a bit of work to be suitable for that+use case, though: `cabal buck2` builds all the external dependencies+and installs them in the Cabal store, whereas to integrate with an+existing build system you would want to satisfy those external+dependencies from the build system itself.++# How complete is it?++I've used it to build a few largish projects, in particular the Cabal+project itself which consists of about 16 packages and a few hundred+source files. It can also build [Glean](https://glean.software), which+has some complex build requirements including custom codegen, FFI &+hsc2hs.++There are a few [limitations](#limitations), however.++# What `cabal buck2` does++You can run `cabal buck2` in a project or a single Cabal package. It+does the following:++1. Solves the `build-depends` constraints of your package(s) and+   builds all the dependencies, much like `cabal build all --only-dependencies`+   would.++2. Generates some files, notably:++   * `BUCK` and `BUCK.cabal.bzl` in each package, these are the Buck2+     build targets++   * `cabal-buck2/autogen` in each package, this is where we put the+     files that Cabal autogenerates, such as `cabal_macros.h` and+     `Paths_<pkg>.hs`.++   * `third-party/haskell`: tells Buck2 about all the prebuilt package+     dependencies, either in the Cabal store or in GHC's package+     DB. In here we also record the GHC version you're using, and the+     paths to any tool dependencies.++# Buck2 quick start++To build your code:++```+buck2 build //...+```++The `//...` is Buck2's syntax for "all targets recursively below the+current directory". You can also build specific target(s), for example+`buck2 build cabal-install:cabal` would build the `cabal` target in+the `cabal-install` package. For more details see [Target+Pattern](https://buck2.build/docs/concepts/target_pattern/) in the+Buck2 docs.++Next you can run your tests:++```+buck2 test //...+```++# Customising the build++`cabal buck2` will generate all the `BUCK` files if they don't exist,+but you can also write your own if you want (`cabal buck2` won't+overwrite them).++The `BUCK` file usually goes in the same directory as your source+files. The targets that `cabal buck2` produces go in the+`BUCK.cabal.bzl` file, and are generated by a call to+`generated_targets()` from the `BUCK` file. This call takes some+arguments that you can use to override or transform the generated+targets - take a look at the comments in the generated code to see+how.++You can also completely override the generated targets and write a+`BUCK` file with your own rules, while still making use of the+pre-built dependencies that `cabal buck2` produces.  For example, the+`BUCK` file for a simple Haskell library might look something like++```+load("//buck2:haskell.bzl", "haskell_library")++haskell_library(+    name = "my-package",+    srcs = [+        "Some/Module.hs",+    ],+    packages = [+        "unordered-containers",+    ],+    visibility = ["PUBLIC"],+)+```++and the `BUCK` file for a test might look like++```+load("//buck2:haskell.bzl", "haskell_test")++haskell_test(+    name = "my-test",+    srcs = {+        "Main.hs" : "my-test.hs",+    },+    deps = [+        "//:my-package",+    ],+    packages = [+        "test-framework",+        "test-framework-hunit",+        "HUnit",+    ],+)+```++You can find docs on how to write `BUCK` files in the Buck2 docs, e.g. [haskell_library](https://buck2.build/docs/prelude/rules/haskell/haskell_library/).++# Build modes++The Buck2 build system has two build modes:++  * `dev`: the default, builds everything with `-O0` and dynamic linking. This is intended to give you the quickest edit-compile-test turnaround.+  * `opt`: enable `-O` and link statically. This takes longer but the code runs faster.++To build with `opt`, use `-m opt`, e.g.++```+buck2 build my-package:my-program -m opt+```++There are other build options that can be selected in a similar way, such as `-m prof` to enable profiling. See `constraints/BUCK` for details.++# Performance++I ran some experiments building the Cabal project itself - 16 packages+and 641 source files (one package, `hackage-security`, is not part of+the project but has to be built locally nonetheless because it depends+on `Cabal-syntax` which *is* part of the project).++Buck2 shines when it comes to rebuilds: the dependency graph is cached+in memory, and it knows when build steps can be omitted because the+inputs haven't changed.++![Buck2 vs Cabal build times](https://raw.githubusercontent.com/simonmar/cabal-buck2/refs/heads/master/perf-chart.svg)++**Caveats**++* Results tend to be +/- a few seconds from run to run+* I didn't dig into the results in any detail+* It's just one set of data points. Different projects and different choices of edits could give different results. However, I did perform a similar+experiment with the [persistent](https://github.com/yesodweb/persistent)+project, and got similar results.++## Raw results and details++### Clean build++* Optimised:+  * Default Cabal build: **280s**+    * `cabal build all --enable-tests --enable-benchmarks -j`+  * Buck2 build (opt mode, including `cabal buck2`): **259s**+    * `cabal buck2 --enable-tests --enable-benchmarks && buck2 build //... -m opt`+    * Not much difference here, as we expect.++* Unoptimised / dynamic:+  * Cabal build with -O0 -dynamic: **136s**+    * `cabal build all --enable-tests --enable-benchmarks -j --disable-optimisation --enable-executable-dynamic`+  * Buck2 build (dev mode, including `cabal buck2`): **78s**+    * `cabal buck2 --enable-tests --enable-benchmarks && buck2 build //... -m dev`+    * Cabal is using `-dynamic-too` for libraries, while Buck2 is building everything purely dynamic.++### Edit + rebuild++Next I made a single edit (added an extension to+`Language.Haskell.Extension`) and rebuilt everything:++* Optimised:+  * Cabal: **197s**+  * Buck2: **179s**++* Unoptimised / dynamic:+  * Cabal: **85s**+  * Buck2: **55s**++# Limitations++## It's an external command, not builtin to `cabal-install`++This has some implications:++* `cabal` passes only the arguments after `buck2`, so global flags+  given before it (`cabal --store-dir=... buck2`) don't reach the+  tool. Use the environment (`CABAL_DIR`) instead.++* Nothing checks that your `cabal` binary matches the version of the+  `cabal-install` library that `cabal-buck2` was built against. Try to+  make sure they match, or confusion will undoubtedly ensue.++## Builds currently use `--make`++The current Buck2 prelude uses `ghc --make` to build each component+(library, executable). Ideally we should expose the full per-module+dependencies to Buck2 so that it can exploit parallelism across+packages for faster builds/rebuilds. It's entirely possible to do+this, indeed the functionality already exists in [Tweag's Haskell/Buck2+integration](https://github.com/tweag/buck2-haskell).++## Custom build type++The `cabal buck2` command doesn't run the actual `Setup.hs` code for a+package with the (legacy) Custom build type. If you rely on this, use+Hooks instead.++## **Template Haskell and `prof`**++A module that defines a splice must live in a *different*+`haskell_library()` from any module that uses it, when profiling (`-m+prof`). If not, the build will likely complain about a link error or a+missing object file at compile-time.++The situation with Template Haskell and profiling is complex, as is+the reason for this limitation.++* Without `-fexternal-interpreter`: GHC loads object code at+  compile-time into its own process. Since GHC is itself a+  dynamically-linked non-profiled executable, the objects it loads+  must be shared, non-profiled, objects. So we have to build all the+  dependencies of the current packages as shared libraries. This is+  fine, except for the current package: GHC expects to find the+  `.dyn_o` objects for the current package in the current `-odir`. But+  Buck2 doesn't work this way: it builds the two instances of the+  package separately. It's not clear if this is easily fixable.++* With `-fexternal-interpreter`, we could load the profiled non-shared+  objects. However, this method uses the RTS runtime linker, which is+  known to have some limitations and can't load some objects,+  particularly on certain architectures. This is the main reason that+  GHC switched to dynamic linking. So we don't go this route.++## No support for Cabal's `foreign-library`++Nothing fundamental blocking this, it's just a TODO.++## Preprocessors like `hspec-discover`++The `hspec-discover` preprocessor is designed to be invoked by GHC via+the `-pgmF` flag to specify a custom preprocessor. The problem is that+`hspec-discover` searches the filesystem to find other source files;+these other source files amount to implicit inputs to the compilation,+but when using Buck2 all inputs must be explicit (this is so that+compilation steps can be executed remotely).++To build an `hspec-discover` test with Buck2, you have to run the+preprocessor using a `genrule()` that takes all the source files as an+input. For example, if your test is in `test/Spec.hs`:++```+filegroup(+    name = "srcs",+    srcs = glob(["**/*.hs"])+)++genrule(+    name = 'spec-gen',+    cmd = "$(location third-party-haskell//:hspec-discover-exe) $(location :srcs)/test/Spec.hs test/Spec.hs ${OUT}",+    out = "test/Spec.hs"+)++haskell_test(+    name = 'spec',+    srcs = {+        'Main.hs': ':spec-gen',+        ...+    },+    ...+)+```++# Acknowledgments++Most of the code and modifications to the standard Buck2 prelude were+developed with the help of Claude Code using Claude Sonnet 5/5.5.++The Haskell support already in the Buck2 prelude was developed by Meta+and is in production use internally for building+[Glean](https://glean.software). This project just fixes a few things+and adds some functionality needed to support building Cabal projects.++# Related projects++[Tweag](https://tweag.io) also worked on a [Haskell integration for+Buck2](https://www.youtube.com/watch?v=bbFnrTAIK9Q). This project has+no code in common with theirs, except for the shared upstream prelude+code. Tweag's integration is more sophisticated and was aimed at using+Buck2's improved scalability to build large Haskell projects.++[ext]: https://cabal.readthedocs.io/en/stable/external-commands.html
+ app/Main.hs view
@@ -0,0 +1,29 @@+-- | @cabal buck2@, as an external command: @cabal@ runs @cabal-buck2@ with the+-- arguments after the command name.+module Main (main) where++import Distribution.Client.CmdBuck2 (buck2Action, buck2Command)+import Distribution.Client.Errors (CabalInstallException)+import Distribution.Client.Setup (defaultGlobalFlags)+import Distribution.Simple.Command+  ( CommandParse (..)+  , CommandUI (commandDefaultFlags)+  , commandParseArgs+  )+import Distribution.Simple.Utils (VerboseException, die', isUserException, topHandler)+import Distribution.Verbosity (defaultVerbosityHandles, mkVerbosity, normal)+import Data.Proxy (Proxy (..))+import System.Environment (getArgs)++main :: IO ()+main = topHandler (isUserException (Proxy @(VerboseException CabalInstallException))) $ do+  args <- getArgs+  -- `cabal help buck2` runs `cabal-buck2 buck2 --help`.+  let args' = case args of+        "buck2" : rest -> rest+        _ -> args+  case commandParseArgs buck2Command True args' of+    CommandHelp help -> putStr (help "cabal")+    CommandList opts -> putStr (unlines opts)+    CommandErrors errs -> die' (mkVerbosity defaultVerbosityHandles normal) (unlines errs)+    CommandReadyToGo (flags, extraArgs) -> buck2Action (flags (commandDefaultFlags buck2Command)) extraArgs defaultGlobalFlags
+ cabal-buck2.cabal view
@@ -0,0 +1,96 @@+cabal-version:      3.8+name:               cabal-buck2+version:            0.1.0.0+synopsis:           Build a Cabal project with buck2 (the `cabal buck2` external command)+license:            BSD-3-Clause+build-type:         Simple+author:             Simon Marlow <marlowsd@gmail.com>+maintainer:         Simon Marlow <marlowsd@gmail.com>+category:           Build Tool+description:+  This is a [Cabal external command](https://cabal.readthedocs.io/en/stable/external-commands.html) `cabal buck2` that allows+  you to use [Buck2](https://buck2.build) as the build system for your+  Cabal project. For more details see the package README.++tested-with:        GHC==9.14.1, GHC==9.12.4, GHC==9.10.3, GHC==9.8.2, GHC==9.6.4, GHC==9.4.8++extra-doc-files:    README.md, changelog.md+extra-source-files:+  test/fixtures/**/*.c+  test/fixtures/**/*.cabal+  test/fixtures/**/*.h+  test/fixtures/**/*.hs+  test/fixtures/**/*.project+  test/fixtures/**/.buckconfig+  test/fixtures/**/.gitkeep+  test/fixtures/**/PACKAGE++homepage:            https://github.com/simonmar/cabal-buck2+bug-reports:         https://github.com/simonmar/cabal-buck2/issues++source-repository head+    type: git+    location: https://github.com/simonmar/cabal-buck2.git++common warnings+  ghc-options: -Wall -Wcompat -Wno-unused-do-bind++library+  import:           warnings+  hs-source-dirs:   src+  default-language: GHC2021+  default-extensions: TypeOperators, NoImportQualifiedPost+  exposed-modules:+    Distribution.Client.CmdBuck2+    Distribution.Client.Buck2.BuildDependencies+    Distribution.Client.Buck2.Configure+    Distribution.Client.Buck2.Generate+    Distribution.Client.Buck2.LocalPackages+    Distribution.Client.Buck2.Prebuilt+    Distribution.Client.Buck2.Schedule+    Distribution.Client.Buck2.Setup+    Distribution.Client.Buck2.Spec+    Distribution.Client.Buck2.Starlark+    Distribution.Client.Buck2.Unpack+    Distribution.Client.Buck2.Write+  build-depends:+    , async         >=2.2.6 && <2.3+    , base          >=4.17 && <4.24+    , bytestring    >=0.11.5 && <0.12+    , Cabal         ==3.18.*+    , Cabal-syntax  ==3.18.*+    , cabal-install ==3.18.1.0+    , containers    >=0.6.7 && <0.7+    , directory     >=1.3.8 && <1.4+    , filepath      ==1.4.*+    , stm           >=2.5.1 && <2.6++executable cabal-buck2+  import:           warnings+  main-is:          Main.hs+  hs-source-dirs:   app+  default-language: GHC2021+  build-depends:+    , base          >=4.17 && <4.24+    , Cabal+    , cabal-buck2+    , cabal-install++-- `cabal test` runs `cabal-buck2` on the projects in `test/fixtures`+-- and checks the files it generates. The tests don't need buck2, and+-- use a private `CABAL_DIR` and the packages installed with GHC only+-- (the fixtures set `active-repositories: :none`), so they need no+-- network access.++test-suite fixtures+  import:             warnings+  type:               exitcode-stdio-1.0+  main-is:            Main.hs+  hs-source-dirs:     test+  default-language:   GHC2021+  build-tool-depends: cabal-buck2:cabal-buck2+  build-depends:+    , base          >=4.17 && <4.24+    , directory+    , filepath+    , process
+ changelog.md view
@@ -0,0 +1,3 @@+# 0.1.0.0++  * For `cabal-install` version 3.18
+ src/Distribution/Client/Buck2/BuildDependencies.hs view
@@ -0,0 +1,73 @@+-- | Building the dependencies of a project, and nothing else: everything+-- @cabal build all --only-dependencies@ would build, except the packages+-- that buck2 builds from source itself (see 'isBuiltLocally').+module Distribution.Client.Buck2.BuildDependencies+  ( buildDependencies+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import qualified Data.Map as Map+import qualified Data.Set as Set++import qualified Distribution.Client.CmdBuild as CmdBuild+import Distribution.Client.CmdErrorMessages (renderCannotPruneDependencies, reportTargetProblems)+import qualified Distribution.Client.InstallPlan as InstallPlan+import Distribution.Client.ProjectConfig (projectConfigWithBuilderRepoContext)+import Distribution.Client.ProjectOrchestration++import Distribution.Client.ProjectPlanning hiding+  ( pruneInstallPlanToTargets -- we want the one from 'ProjectOrchestration'.+  )++import Distribution.Simple.Utils (dieWithException, notice)++import Distribution.Client.Buck2.LocalPackages (isBuiltLocally)+import Distribution.Client.Buck2.Unpack (unpackInplaceSources)+import Distribution.Client.Errors (CabalInstallException (ReportCannotPruneDependencies))++-- | Build every dependency of the selected targets, excluding packages+-- that are forced to build locally (see 'isBuiltLocally').+-- This is similar but not quite the same as the code for @CmdBuild@.+buildDependencies :: Verbosity -> ProjectBaseContext -> [TargetSelector] -> IO ProjectBuildContext+buildDependencies verbosity baseCtx targetSelectors = do+  buildCtx@ProjectBuildContext{elaboratedPlanOriginal, elaboratedShared} <-+    runProjectPreBuildPhase verbosity baseCtx $ \elaboratedPlan -> do+      targets <-+        either (reportTargetProblems verbosity "buck2") return $+          resolveTargetsFromSolver+            CmdBuild.selectPackageTargets+            CmdBuild.selectComponentTarget+            elaboratedPlan+            Nothing+            targetSelectors+      let elaboratedPlan' = pruneInstallPlanToTargets TargetActionBuild targets elaboratedPlan+          excluded =+            Map.keysSet targets+              <> Set.fromList+                [ elabUnitId elab+                | InstallPlan.Configured elab <- InstallPlan.toList elaboratedPlan'+                , isBuiltLocally elab+                ]+      elaboratedPlan'' <-+        either (dieWithException verbosity . ReportCannotPruneDependencies . renderCannotPruneDependencies) return $+          pruneInstallPlanToDependencies excluded elaboratedPlan'+      return (elaboratedPlan'', targets)++  notice verbosity "cabal buck2: building dependencies"+  printPlan verbosity baseCtx buildCtx+  buildOutcomes <- runProjectBuildPhase verbosity baseCtx buildCtx+  runProjectPostBuildPhase verbosity baseCtx buildCtx buildOutcomes++  unpackInplaceSources+    verbosity+    (distDirLayout baseCtx)+    elaboratedShared+    (projectConfigWithBuilderRepoContext verbosity (buildSettings baseCtx))+    [ elab+    | InstallPlan.Configured elab <- InstallPlan.toList elaboratedPlanOriginal+    , isBuiltLocally elab+    , not (elabLocalToProject elab)+    ]+  return buildCtx
+ src/Distribution/Client/Buck2/Configure.hs view
@@ -0,0 +1,216 @@+-- | Configure the components of the packages that buck2 builds from+-- source, to get the 'LocalBuildInfo'.+module Distribution.Client.Buck2.Configure+  ( configureComponents+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import qualified Data.Map as Map++import Control.Concurrent (getNumCapabilities, setNumCapabilities)+import Control.Concurrent.STM+  ( atomically+  , modifyTVar'+  , newTVarIO+  , readTVarIO+  )++import Distribution.Client.DistDirLayout+  ( DistDirLayout (distBuildDirectory)+  )+import qualified Distribution.Client.InLibrary as InLibrary+import qualified Distribution.Client.InstallPlan as InstallPlan+import Distribution.Client.ProjectOrchestration+import Distribution.Client.ProjectPlanning hiding (pruneInstallPlanToTargets)+import Distribution.Client.ProjectPlanning.Types+  ( elabDistDirParams+  , elabExeDependencyPaths+  , elabOrderLibDependencies+  )+import Distribution.Client.Types.ReadyPackage (GenericReadyPackage (ReadyPackage))+import Distribution.Client.Utils (numberOfProcessors)+import Distribution.Types.ParStrat (ParStratInstall, ParStratX (..))++import Distribution.Package (PackageName, packageName)+import Distribution.PackageDescription (PackageDescription)+import qualified Distribution.PackageDescription as PD+import Distribution.Simple.Compiler (PackageDBX (GlobalPackageDB))+import Distribution.Simple.PackageIndex (InstalledPackageIndex)+import qualified Distribution.Simple.PackageIndex as PackageIndex+import Distribution.Simple.Program.Builtin (builtinPrograms)+import Distribution.Simple.Program.Db (prependProgramSearchPathNoLogging, restoreProgramDb, userSpecifyArgss)+import Distribution.Simple.Register (generateRegistrationInfo)+import Distribution.Simple.Utils (info)+import Distribution.Types.InstalledPackageInfo (InstalledPackageInfo)+import Distribution.Types.LocalBuildInfo+  ( LocalBuildInfo+  , componentNameCLBIs+  , distPrefLBI+  , relocatable+  )+import Distribution.Types.UnitId (UnitId)+import Distribution.Utils.Path (makeSymbolicPath)+import Distribution.Verbosity (defaultVerbosityHandles)++import System.Directory (canonicalizePath)+import System.FilePath ((</>))++import Distribution.Client.Buck2.LocalPackages (componentNamesFor, isBuiltLocally, packageSourceDir)+import Distribution.Client.Buck2.Schedule (runDependencyGraph)++-- | Get a real 'LocalBuildInfo' for every component in the+-- 'ProjectBuildContext', keyed by package and component name.  Only+-- components actually selected by the build targets (and whatever+-- they depend on) are configured.+--+-- @installedIndex@ must cover the whole resolved dependency closure of the+-- project. Each library is added to it, configured and registered in-place,+-- before the components that depend on it are configured.+--+-- Configuration happens concurrently as far as possible, because+-- this can take a while for projects with a lot of components to build.+configureComponents+  :: Verbosity+  -> ProjectBaseContext+  -> ProjectBuildContext+  -> InstalledPackageIndex+  -> IO (Map (PackageName, ComponentName) LocalBuildInfo)+configureComponents verbosity baseCtx buildCtx installedIndex =+  configureComponentsConcurrently+    verbosity+    (distDirLayout baseCtx)+    (parStratNumJobs (buildSettingNumJobs (buildSettings baseCtx)))+    (pruneInstallPlanToTargets TargetActionBuild (targetsMap buildCtx) (elaboratedPlanOriginal buildCtx))+    (elaboratedShared buildCtx)+    installedIndex++-- | A real 'LocalBuildInfo' for one local (or quasi-local) *component*.+localBuildInfoFor+  :: Verbosity+  -> DistDirLayout+  -> ElaboratedInstallPlan+  -> ElaboratedSharedConfig+  -> InstalledPackageIndex+  -> ElaboratedConfiguredPackage+  -> IO LocalBuildInfo+localBuildInfoFor verbosity distDirLayout plan shared ipi elab = do+  -- Real Cabal's own 'InLibrary.configure' falls back to *searching* the+  -- working directory for a @<pkgname>.cabal@ file whenever+  -- 'Cabal.configCabalFilePath' isn't set (see its own use of+  -- 'tryFindPackageDesc') - so this has to be the package's own source+  -- directory, matching 'setupHsScriptOptions''s own @srcdir@ in the+  -- real build path ("Distribution.Client.ProjectBuilding.UnpackedPackage"),+  -- not the buck2 command's actual cwd (the project root), or this fails+  -- outright with "No cabal file found" for every package but one that+  -- happens to be sitting at the project root itself.+  pkgDir <- packageSourceDir verbosity distDirLayout elab+  let verbHandles = defaultVerbosityHandles+      -- Builtin preprocessors (alex, happy, hsc2hs, ...) restored as+      -- known-but-unconfigured programs, and the compiler's own+      -- already-configured programs - the same starting point a real+      -- build's own 'Distribution.Client.SetupWrapper' constructs (see+      -- its own comment "Note [Constructing the ProgramDb]") - plus+      -- 'elabExeDependencyPaths'\/'elabProgramPathExtra' prepended onto+      -- its search path. That part isn't optional the way the rest of+      -- "Note [Constructing the ProgramDb]"'s extra layering is: a+      -- component with e.g. @build-tool-depends: alex:alex@ needs+      -- 'InLibrary.configure' below to be able to find *this* build's+      -- own just-built @alex@ (never on a bare @$PATH@ - it only exists+      -- under @dist-newstyle@) and query its version, the same way real+      -- Cabal's own subprocess-based configure does via+      -- 'setupHsScriptOptions''s @useExtraPathEnv@ - just via a search+      -- path prepend instead of a subprocess's environment, since this+      -- runs in-process.+      -- The user-specified program arguments (@ghc-options:@ from+      -- cabal.project, @--ghc-options@, ...) are applied by Cabal's own+      -- top-level @configure@, which 'InLibrary.configure' skips - so+      -- without this the 'LocalBuildInfo' wouldn't have them, unlike one+      -- from a real @Setup configure@.+      progDb =+        userSpecifyArgss (Map.toList (elabProgramArgs elab)) $+          prependProgramSearchPathNoLogging+            (elabExeDependencyPaths elab ++ elabProgramPathExtra elab)+            []+            (restoreProgramDb builtinPrograms (pkgConfigCompilerProgs shared))+      buildType = PD.buildType (elabPkgDescription elab)+      inputs =+        InLibrary.libraryConfigureInputsFromElabPackage+          verbHandles+          buildType+          progDb+          shared+          (ReadyPackage elab)+          ipi+          []+      builddir = makeSymbolicPath (distBuildDirectory distDirLayout (elabDistDirParams shared elab) </> "build")+      commonFlags = setupHsCommonFlags verbosity (Just (makeSymbolicPath pkgDir)) builddir [] False+  cfg <-+    setupHsConfigureFlags+      (fmap makeSymbolicPath . canonicalizePath)+      plan+      (ReadyPackage elab)+      shared+      commonFlags+  InLibrary.configure inputs cfg++-- | If @cname@ names a library component, produce the real, in-place+-- 'InstalledPackageInfo' for it.+libraryInstalledPackageInfo :: Verbosity -> LocalBuildInfo -> PackageDescription -> ComponentName -> IO (Maybe InstalledPackageInfo)+libraryInstalledPackageInfo verbosity lbi pkgDesc cname = case cname of+  CLibName ln+    | Just lib <- listToMaybe [l | l <- PD.allLibraries pkgDesc, PD.libName l == ln]+    , (clbi : _) <- componentNameCLBIs lbi cname ->+        Just <$> generateRegistrationInfo verbosity pkgDesc lib lbi clbi True (relocatable lbi) (distPrefLBI lbi) GlobalPackageDB+  _ -> return Nothing++-- | Obtain the 'LocalBuildInfo' for all the components by configuring+-- them concurrently as far as possible, respecting dependency constraints+-- and the @-jNUM@ flag.+configureComponentsConcurrently+  :: Verbosity+  -> DistDirLayout+  -> Int+  -- ^ Maximum number of components to configure at once (from+  -- @-j@\/@jobs:@, like a @cabal build@ would use).+  -> ElaboratedInstallPlan+  -> ElaboratedSharedConfig+  -> InstalledPackageIndex+  -> IO (Map (PackageName, ComponentName) LocalBuildInfo)+configureComponentsConcurrently verbosity distDirLayout numJobs plan shared installedIndex = do+  numCaps <- getNumCapabilities+  info verbosity $ "cabal buck2: configuring components with up to " ++ show numJobs ++ " job(s)"+  let wantedCaps = min numJobs numberOfProcessors+  when (numCaps < wantedCaps) $+    setNumCapabilities wantedCaps++  let localElabs :: Map UnitId ElaboratedConfiguredPackage+      localElabs =+        Map.fromList+          [ (elabUnitId elab, elab)+          | InstallPlan.Configured elab <- InstallPlan.toList plan+          , isBuiltLocally elab+          ]++  indexVar <- newTVarIO installedIndex+  componentLBIsVar <- newTVarIO Map.empty++  runDependencyGraph numJobs (Map.map elabOrderLibDependencies localElabs) $ \uid -> do+    let elab = localElabs Map.! uid+        pkgDesc = elabPkgDescription elab+    idx <- readTVarIO indexVar+    lbi <- localBuildInfoFor verbosity distDirLayout plan shared idx elab+    for_ (componentNamesFor elab pkgDesc) $ \cname -> do+      mipi <- libraryInstalledPackageInfo verbosity lbi pkgDesc cname+      for_ mipi $ \ipi -> atomically $ modifyTVar' indexVar (PackageIndex.insert ipi)+      atomically $ modifyTVar' componentLBIsVar (Map.insert (packageName pkgDesc, cname) lbi)++  readTVarIO componentLBIsVar++-- | How many jobs a parallel strategy allows to run at once: @-j@ with no+-- number means one per processor, @-jsem N@ is treated as @-jN@.+parStratNumJobs :: ParStratInstall -> Int+parStratNumJobs Serial = 1+parStratNumJobs (NumJobs n) = fromMaybe numberOfProcessors n+parStratNumJobs (UseSem n) = n
+ src/Distribution/Client/Buck2/Generate.hs view
@@ -0,0 +1,658 @@+-- | Generate all the info that buck2 needs to build the components of+-- a package, as a pure function from the 'PackageDescription',+-- 'LocalBuildInfo'(s) and a few other things.+--+-- The buck2 build spec is described by the data types in+-- "Distribution.Client.Buck2.Spec", and will be emitted into+-- @BUCK.cabal.bzl@ and interpreted by @buck2@ to produce the final+-- build targets (the interpreter is @buck2/cabal.bzl@). It's done+-- this way rather than emitting targets directly so that a custom+-- @BUCK@ file can override or customise the targets.+--+-- Here we also produce the content for the autogen files, such as+-- @cabal_macros.h@ and @Paths_<pkg>.hs@.+--+-- All the content we generate here will be written to files later in+-- "Distribution.Client.Buck2.Write".+module Distribution.Client.Buck2.Generate+  ( LocalPackageIndex+  , AutogenFile (..)+  , PackageTargets (..)+  , generatePackageTargets+  , sourceCandidates+  , libTargetName+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import System.FilePath ((<.>), (</>))++import Data.Either (fromLeft)+import qualified Data.Map as Map+import qualified Data.Set as Set++import qualified Distribution.Compat.NonEmptySet as NES+import Distribution.Compiler (CompilerFlavor (GHC))+import qualified Distribution.ModuleName as ModuleName+import Distribution.Package (packageId, packageName)+import Distribution.PackageDescription+  ( Benchmark (benchmarkInterface, benchmarkName)+  , BenchmarkInterface (..)+  , BuildInfo+  , Executable (exeName, modulePath)+  , Library (exposedModules, libBuildInfo, libName)+  , LibraryName (..)+  , PackageDescription+  , TestSuite (testInterface, testName)+  , TestSuiteInterface (..)+  , buildToolDepends+  , cSources+  , cppOptions+  , cxxOptions+  , cxxSources+  , defaultExtensions+  , defaultLanguage+  , extraLibs+  , hcOptions+  , hsSourceDirs+  , includeDirs+  , otherModules+  , pkgBuildableComponents+  , pkgconfigDepends+  , targetBuildDepends+  )+import Distribution.Simple.Compiler (compilerInfo)+import Distribution.Types.Component (Component (..), componentBuildInfo, componentName)+import Distribution.Types.ComponentLocalBuildInfo (ComponentLocalBuildInfo)+import Distribution.Types.ComponentName (ComponentName)+import Distribution.Types.Dependency (depLibraries, depPkgName)+import Distribution.Types.ExeDependency (ExeDependency (..))+import Distribution.Types.LocalBuildInfo (LocalBuildInfo (compiler, hostPlatform, withPrograms), componentNameCLBIs, localUnitId)+import Distribution.Types.PackageName (PackageName, unPackageName)+import Distribution.Types.PkgconfigDependency (PkgconfigDependency (..))+import Distribution.Types.PkgconfigName (unPkgconfigName)+import Distribution.Types.UnqualComponentName (unUnqualComponentName)+import Distribution.Utils.Path (getSymbolicPath)++import Distribution.Simple.Build.Macros (generateCabalMacrosHeader)+import Distribution.Simple.Build.PathsModule (generatePathsModule)+import Distribution.Simple.BuildPaths (autogenPathsModuleName)+import Distribution.Simple.InstallDirs+  ( PathTemplate+  , PathTemplateVariable (TestSuiteNameVar)+  , fromPathTemplate+  , initialPathTemplateEnv+  , substPathTemplate+  , toPathTemplate+  )+import Distribution.Simple.Program.Builtin (ghcProgram)+import Distribution.Simple.Program.Db (lookupProgram)+import Distribution.Simple.Program.Types (programOverrideArgs)+import Distribution.Simple.Utils (ordNub)++import Distribution.Client.Buck2.Spec++-- | Maps every local project package's name to the buck2 cell-relative+-- directory its @BUCK@ file lives in (@.@ for one at the project root), so+-- a dependency on another local package can be turned into a fully+-- qualified target label - plus the set of other packages its main+-- library's @reexported-modules@ re-export from (see 'classifyDeps').+type LocalPackageIndex = Map PackageName (FilePath, [PackageName])++-- | A generated file that lives in the package's @cabal-buck2\/autogen@+-- directory (a component's @cabal_macros.h@, the package's own+-- @Paths_\<pkg\>@ module, a @detailed-0.9@ test-suite's stub @Main@), and+-- needs an @export_file()@ entry in @cabal-buck2\/autogen\/BUCK@ (see+-- "Distribution.Client.Buck2.Write", which writes both) so it can be+-- referenced - by a component's @cabal_component@, or by a hand-written rule+-- elsewhere in the project - as a real, buck2-tracked target instead of an+-- untracked path string.+data AutogenFile = AutogenFile+  { autogenName :: String+  -- ^ The name of its @export_file()@ target.+  , autogenPath :: FilePath+  -- ^ Relative to @cabal-buck2\/autogen@.+  , autogenContents :: String+  }+  deriving (Eq, Ord)++-- | What is generated for one package: its build spec,+-- and the autogen files it needs.+data PackageTargets = PackageTargets+  { ptSpec :: BuildSpec+  , ptComponentCount :: Int+  , ptAutogenFiles :: [AutogenFile]+  }++-- | One component's contribution to a 'PackageTargets': its entry in the+-- spec (none if it was skipped), its autogen files, and the warnings+-- produced along the way.+data ComponentTargets = ComponentTargets+  { ctSpec :: [SpecComponent]+  , ctAutogen :: [AutogenFile]+  , ctWarnings :: [String]+  }++instance Semigroup ComponentTargets where+  ComponentTargets s1 a1 w1 <> ComponentTargets s2 a2 w2 = ComponentTargets (s1 ++ s2) (a1 ++ a2) (w1 ++ w2)++instance Monoid ComponentTargets where+  mempty = ComponentTargets [] [] []++componentTargets :: SpecComponent -> [AutogenFile] -> ComponentTargets+componentTargets component files = mempty{ctSpec = [component], ctAutogen = files}++-- | Generate the buck2 targets for every buildable component of a local+-- package, along with the warnings for the components that had to be+-- skipped (and why). This does no IO: the source files it looks for are+-- given as a set of paths (relative to the package's directory), which+-- must include every path 'sourceCandidates' lists that exists.+generatePackageTargets+  :: LocalPackageIndex+  -> FilePath+  -- ^ The package's own buck2 cell-relative directory (@.@ at the project+  -- root).+  -> Map (PackageName, ComponentName) LocalBuildInfo+  -> Set String+  -- ^ Every @build-tool-depends:@ executable name+  -- "Distribution.Client.Buck2.Prebuilt" resolved a real *external*+  -- binary for (and generated an @export_file()@ target for) - see+  -- 'specComponent'.+  -> Map (PackageName, ComponentName) [PathTemplate]+  -- ^ The project's @test-options:@ for each test-suite (see 'testSuiteArgs').+  -> Set FilePath+  -> PackageDescription+  -> (Maybe PackageTargets, [String])+generatePackageTargets localIndex rootRelPkgDir componentLBIs externalBuildTools projectTestOptions sources pkgDesc =+  (targets, concatMap ctWarnings results)+  where+    generate = generateComponent localIndex componentLBIs externalBuildTools projectTestOptions sources pkgDesc+    comps = pkgBuildableComponents pkgDesc++    -- Worked out ahead of the other components, so that one that+    -- build-depends on a library of this package that won't get a rule+    -- (unresolvable modules) can skip itself too, instead of emitting a+    -- rule whose @deps@ references a target that was never generated -+    -- buck2 fails that outright at analysis time ("Unknown target"), for+    -- the *entire* build, the same class of problem 'resolveModules'+    -- exists to avoid for a single component's own missing source file.+    -- Libraries themselves never depend on this set.+    skippedLibs =+      Set.fromList [libName lib | comp@(CLib lib) <- comps, null (ctSpec (generate Set.empty comp))]++    results = map (generate skippedLibs) comps+    components = concatMap ctSpec results+    targets+      | null components = Nothing+      | otherwise =+          Just+            PackageTargets+              { ptSpec = packageSpec pkgDesc rootRelPkgDir (packageGhcOptions pkgDesc componentLBIs) components+              , ptComponentCount = length components+              , ptAutogenFiles = dedupAutogenFiles (concatMap ctAutogen results)+              }++-- | Look up a component's 'LocalBuildInfo' and+-- 'ComponentLocalBuildInfo' -- 'Nothing' if either lookup fails+-- (e.g. a component that isn't part of the elaborated build plan,+-- such as a test-suite when tests aren't enabled), in which case+-- callers skip the component.+lbiClbiFor :: PackageDescription -> Map (PackageName, ComponentName) LocalBuildInfo -> Component -> Maybe (LocalBuildInfo, ComponentLocalBuildInfo)+lbiClbiFor pkgDesc componentLBIs comp = do+  lbi <- Map.lookup (packageName pkgDesc, componentName comp) componentLBIs+  clbi <- listToMaybe (componentNameCLBIs lbi (componentName comp))+  return (lbi, clbi)++-- | Two components referencing the same autogen file (the package's+-- @Paths_\<pkg\>@ module) each produce it; keep one entry per target, at the+-- position of the first, with the contents of the last.+dedupAutogenFiles :: [AutogenFile] -> [AutogenFile]+dedupAutogenFiles files =+  [ f{autogenContents = latest Map.! autogenName f}+  | f <- nubBy ((==) `on` autogenName) files+  ]+  where+    latest = Map.fromList [(autogenName f, autogenContents f) | f <- files]++generateComponent+  :: LocalPackageIndex+  -> Map (PackageName, ComponentName) LocalBuildInfo+  -> Set String+  -> Map (PackageName, ComponentName) [PathTemplate]+  -> Set FilePath+  -> PackageDescription+  -> Set LibraryName+  -> Component+  -> ComponentTargets+generateComponent localIndex componentLBIs externalBuildTools projectTestOptions sources pkgDesc skippedLibs comp = case comp of+  CLib lib -> library (libTargetName (packageName pkgDesc) (libName lib)) lib+  CExe exe -> ifNotOnSkippedLib (componentBuildInfo comp) (unUnqualComponentName (exeName exe)) Executable $ executable exe+  CTest test -> ifNotOnSkippedLib (componentBuildInfo comp) (unUnqualComponentName (testName test)) TestSuite $ testSuite test+  CBench bench -> ifNotOnSkippedLib (componentBuildInfo comp) (unUnqualComponentName (benchmarkName bench)) Benchmark $ benchmark bench+  CFLib _ -> skip "foreign library (not supported yet)"+  where+    skip = skipBecause []++    -- Skip a component, with the warnings for the problems that led to it+    -- followed by the skip itself.+    skipBecause problems why =+      mempty{ctWarnings = problems ++ ["cabal buck2: skipping " ++ why ++ " in package " ++ unPackageName (packageName pkgDesc)]}++    -- A component that build-depends on one of *this same package's*+    -- own libraries, when that library was itself skipped (see+    -- 'skippedLibs'), can't be built either - it would emit a rule+    -- whose own @deps@ references a target that was never generated,+    -- which buck2 rejects outright ("Unknown target") at analysis time+    -- for the whole build, not just a warning. cabal-testsuite's own+    -- @test-runtime-deps@ executable (build-depends on cabal-testsuite's+    -- own library) is exactly this case.+    ifNotOnSkippedLib bi targetName kind act =+      case [ln | d <- targetBuildDepends bi, depPkgName d == packageName pkgDesc, ln <- NES.toList (depLibraries d), ln `Set.member` skippedLibs] of+        (ln : _) ->+          skip+            ( kindName kind+                ++ " "+                ++ targetName+                ++ " (depends on "+                ++ libTargetName (packageName pkgDesc) ln+                ++ ", itself skipped)"+            )+        [] -> act++    library targetName lib = case lbiClbiFor pkgDesc componentLBIs comp of+      Nothing -> skip ("library " ++ targetName ++ " (no LocalBuildInfo found for it in the elaborated build plan)")+      Just (lbi, clbi) -> case resolveModules sources pkgDesc (Just (lbi, clbi)) bi (exposedModules lib ++ otherModules bi) of+        Left problems -> skipBecause problems ("library " ++ targetName ++ " (couldn't resolve all its modules)")+        Right (srcs, srcAutogen) ->+          componentTargets+            (specComponent localIndex externalBuildTools Library targetName bi){scSrcs = srcs}+            (macrosHeader targetName pkgDesc lbi clbi : srcAutogen)+      where+        bi = libBuildInfo lib++    -- An executable, benchmark or test-suite: all the same shape (a main+    -- module over some others), differing only in the spec's kind and test+    -- arguments.+    executableLike kind targetName lbi clbi bi mainSrc otherSrcs testArgs srcAutogen =+      componentTargets+        (specComponent localIndex externalBuildTools kind targetName bi){scMainIs = Just mainSrc, scSrcs = otherSrcs, scTestArgs = testArgs}+        (macrosHeader targetName pkgDesc lbi clbi : srcAutogen)++    -- The shape shared by an executable, an @exitcode-stdio-1.0@+    -- test-suite and benchmark: a @main-is@ file over the @other-modules@.+    mainIsComponent kind targetName bi mainIs extra = case lbiClbiFor pkgDesc componentLBIs comp of+      Nothing -> skip (kindName kind ++ " " ++ targetName ++ " (no LocalBuildInfo found for it in the elaborated build plan)")+      Just (lbi, clbi) -> case (resolveMainIs sources bi mainIs, resolveModules sources pkgDesc (Just (lbi, clbi)) bi (otherModules bi)) of+        (Right mainSrc, Right (otherSrcs, srcAutogen)) ->+          executableLike kind targetName lbi clbi bi (SrcFile mainSrc) otherSrcs (extra lbi) srcAutogen+        (mainRes, othersRes) ->+          skipBecause (problemsOf mainRes ++ problemsOf othersRes) (kindName kind ++ " " ++ targetName ++ " (couldn't resolve all its modules)")++    executable exe = mainIsComponent Executable targetName bi (getSymbolicPath (modulePath exe)) (const [])+      where+        bi = componentBuildInfo (CExe exe)+        targetName = unUnqualComponentName (exeName exe)++    -- A benchmark's own 'BenchmarkExeV10' is exactly 'TestSuiteExeV10's+    -- shape (a version-tagged main-is path over the same 'BuildInfo') -+    -- and unlike a test-suite, @cabal bench@ has no special "run it and+    -- report a testsuite-style result" semantics of its own, just+    -- "build and run this executable" - so this maps onto the same thing+    -- as an executable.+    benchmark bench = case benchmarkInterface bench of+      BenchmarkExeV10 _ver mainIs -> mainIsComponent Benchmark targetName bi (getSymbolicPath mainIs) (const [])+      _ ->+        skip+          ( "benchmark "+              ++ targetName+              ++ " (only exitcode-stdio-1.0 benchmarks are supported)"+          )+      where+        bi = componentBuildInfo (CBench bench)+        targetName = unUnqualComponentName (benchmarkName bench)++    testSuite test = case testInterface test of+      TestSuiteExeV10 _ver mainIs -> mainIsComponent TestSuite targetName bi (getSymbolicPath mainIs) testArgs+      -- A @detailed-0.9@ test-suite's own module (named via+      -- @test-module:@, not @other-modules:@ - real Cabal synthesises a+      -- whole separate internal sub-library exposing just this one+      -- module, see 'Distribution.Simple.Build.testSuiteLibV09AsLibAndExe')+      -- exports @tests :: IO ['Distribution.TestSuite.Test']@, and real+      -- Cabal's own Setup.hs generates a tiny stub 'Main' importing it+      -- and calling into 'Distribution.Simple.Test.LibV09.stubMain' -+      -- which then blocks reading a @(logFilePath, testSuiteName)@ pair+      -- off *stdin*, written by the parent @cabal test@ process, before+      -- it'll run anything at all (see that module's own 'stubMain').+      -- That stdin handshake has nothing to do with buck2 - a+      -- @haskell_test()@ just execs the compiled binary and checks its+      -- exit code, the same as @exitcode-stdio-1.0@ - so reusing real+      -- Cabal's own stub verbatim would need a wrapper script to feed it+      -- a fake handshake for no real benefit (nothing here ever reads+      -- the machine-readable log it writes). Generates a self-contained+      -- stub instead, calling only 'Distribution.TestSuite's own public+      -- API directly (a real, documented, GHC-version-independent+      -- interface - not reimplementing anything real Cabal doesn't+      -- already expose for exactly this purpose): runs every 'Test'+      -- in turn, printing a human-readable pass\/fail\/error line per+      -- test to stdout, exiting non-zero if anything failed or errored.+      -- Needs no new build-depends beyond what real Cabal already+      -- requires the user to declare for their own @tests@ module to+      -- even type-check (@Distribution.TestSuite@ lives in the @Cabal@+      -- library itself).+      TestSuiteLibV09 _ver testModule -> case lbiClbiFor pkgDesc componentLBIs comp of+        Nothing -> skip ("test-suite " ++ targetName ++ " (no LocalBuildInfo found for it in the elaborated build plan)")+        Just (lbi, clbi) -> case (resolveModules sources pkgDesc (Just (lbi, clbi)) bi [testModule], resolveModules sources pkgDesc (Just (lbi, clbi)) bi (otherModules bi)) of+          (Right (testModSrc, testModAutogen), Right (otherSrcs, otherAutogen)) ->+            let (stubSrc, stubFile) = detailedTestStub targetName testModule+             in executableLike TestSuite targetName lbi clbi bi stubSrc (testModSrc ++ otherSrcs) (testArgs lbi) (stubFile : testModAutogen ++ otherAutogen)+          (testModRes, othersRes) ->+            skipBecause (problemsOf testModRes ++ problemsOf othersRes) ("test-suite " ++ targetName ++ " (couldn't resolve all its modules)")+      _ ->+        skip+          ( "test-suite "+              ++ targetName+              ++ " (only exitcode-stdio-1.0 and detailed-0.9 test-suites are supported)"+          )+      where+        bi = componentBuildInfo (CTest test)+        targetName = unUnqualComponentName (testName test)+        testArgs lbi = testSuiteArgs pkgDesc lbi test (Map.findWithDefault [] (packageName pkgDesc, componentName comp) projectTestOptions)++    problemsOf = fromLeft []++-- | A package's build spec.+packageSpec :: PackageDescription -> FilePath -> [String] -> [SpecComponent] -> BuildSpec+packageSpec pkgDesc rootRelPkgDir projectGhcOptions components =+  BuildSpec+    { specPackageName = unPackageName (packageName pkgDesc)+    , specPackageDir = rootRelPkgDir+    , specGhcOptions = projectGhcOptions+    , specComponents = components+    }++-- | A component of a build spec, from its 'BuildInfo'. The kind-specific+-- parts (main module, other sources, test arguments) are left empty.+specComponent :: LocalPackageIndex -> Set String -> ComponentKind -> String -> BuildInfo -> SpecComponent+specComponent localIndex externalBuildTools kind name bi =+  SpecComponent+    { scKind = kind+    , scName = name+    , scMainIs = Nothing+    , scSrcs = []+    , scTestArgs = []+    , scGhcOptions = hcOptions GHC bi+    , scCppOptions = cppOptions bi+    , scLanguage = prettyShow <$> defaultLanguage bi+    , scExtensions = map prettyShow (defaultExtensions bi)+    , scExtraLibraries = extraLibs bi+    , scDeps = map depSpec (libraryDeps localIndex bi)+    , scBuildTools = mapMaybe buildToolSpec (ordNub [(pn, exe) | ExeDependency pn exe _ <- buildToolDepends bi])+    , scCSources = map getSymbolicPath (cSources bi)+    , scCxxSources = map getSymbolicPath (cxxSources bi)+    , scCxxOptions = cxxOptions bi+    , scIncludeDirs = map getSymbolicPath (includeDirs bi)+    , scPkgconfig = ordNub [unPkgconfigName n | PkgconfigDependency n _ <- pkgconfigDepends bi]+    }+  where+    depSpec (pn, ln) =+      SpecDep+        { depPackage = unPackageName pn+        , depLibrary = case ln of+            LSubLibName n -> Just (unUnqualComponentName n)+            LMainLibName -> Nothing+        , depDir = fst <$> Map.lookup pn localIndex+        }+    -- A tool that's neither built by this project nor resolved to a real+    -- external binary can't be put on PATH by buck2: dropped, silently -+    -- most build-tool-depends are Setup.hs-time tools nothing needs on PATH.+    buildToolSpec (pn, exe) = case Map.lookup pn localIndex of+      Just (dir, _) -> Just (LocalTool n dir)+      Nothing+        | n `Set.member` externalBuildTools -> Just (ExternalTool n)+        | otherwise -> Nothing+      where+        n = unUnqualComponentName exe++-- | The project-supplied GHC options (see 'ghcProgramArgs') for a+-- package. Cabal gives every component of a package the same ones (they+-- come from the package's @cabal.project@ stanza, not from anything+-- per-component), so any one component's will do; empty if none of its+-- components were configured at all.+packageGhcOptions :: PackageDescription -> Map (PackageName, ComponentName) LocalBuildInfo -> [String]+packageGhcOptions pkgDesc componentLBIs =+  fromMaybe [] . listToMaybe $+    [ ghcProgramArgs lbi+    | comp <- pkgBuildableComponents pkgDesc+    , Just (lbi, _) <- [lbiClbiFor pkgDesc componentLBIs comp]+    ]++-- | Extra GHC arguments the *project* supplies rather than the @.cabal@+-- file: @ghc-options:@ under @package@\/@program-options@ in+-- @cabal.project@, or @--ghc-options@ on the command line. Cabal applies+-- these to every GHC invocation for the component, after the @.cabal@+-- file's own @ghc-options@ (so they can override them) - taken from the+-- configured @ghc@ program in the 'LocalBuildInfo', which is exactly+-- where Cabal itself finds them, rather than reading the elaborated plan.+ghcProgramArgs :: LocalBuildInfo -> [String]+ghcProgramArgs lbi =+  -- cabal-install itself adds @-hide-all-packages@ to every GHC invocation+  -- (a workaround for custom Setup scripts that call GHC directly), which+  -- means nothing for a buck2 rule - the rule already passes it, and it's+  -- position-sensitive among GHC's package flags.+  filter (/= "-hide-all-packages") $+    maybe [] programOverrideArgs (lookupProgram ghcProgram (withPrograms lbi))++-- | The arguments @cabal test@ would pass to this test-suite's executable+-- for the project's @test-options:@ (\/@--test-options@), with the same+-- @$pkgid@\/@$test-suite@-style template variables expanded - Cabal's own+-- expansion (in "Distribution.Simple.Test.ExeV10") isn't exported.+testSuiteArgs :: PackageDescription -> LocalBuildInfo -> TestSuite -> [PathTemplate] -> [String]+testSuiteArgs pkgDesc lbi test = map (fromPathTemplate . substPathTemplate env)+  where+    env =+      initialPathTemplateEnv (packageId pkgDesc) (localUnitId lbi) (compilerInfo (compiler lbi)) (hostPlatform lbi)+        ++ [(TestSuiteNameVar, toPathTemplate (unUnqualComponentName (testName test)))]++-- | This component's @cabal_macros.h@, exactly as real Cabal generates it+-- ahead of a build. Produced unconditionally, like real Cabal: harmless for a+-- component that never enables CPP (the header only matters if\/when cpp+-- actually runs). @cabal_component@ (in buck2\/cabal.bzl's rules) is what+-- wires the component up to include it.+macrosHeader :: String -> PackageDescription -> LocalBuildInfo -> ComponentLocalBuildInfo -> AutogenFile+macrosHeader targetName pkgDesc lbi clbi =+  AutogenFile+    { autogenName = targetName ++ "-cabal-macros"+    , autogenPath = targetName </> "cabal_macros.h"+    , autogenContents = generateCabalMacrosHeader pkgDesc lbi clbi+    }++-- | The buck2 target name for one of a package's libraries: the package+-- name itself for the main (unnamed) library, matching every other+-- reference to it (@packages = [...]@, other packages' @build-depends@,+-- ...); the sub-library's own unqualified name otherwise - always unique+-- within one package's BUCK file, since Cabal itself already requires+-- every component name in a package to be distinct.+libTargetName :: PackageName -> LibraryName -> String+libTargetName pn LMainLibName = unPackageName pn+libTargetName _ (LSubLibName n) = unUnqualComponentName n++-- | Every library a component depends on: its @build-depends@ (each of which may name+-- more than one library of a package via @pkg:sublib@ - see+-- 'depLibraries'), closed over the reexports of local packages.+libraryDeps :: LocalPackageIndex -> BuildInfo -> [(PackageName, LibraryName)]+libraryDeps localIndex bi = closeOverReexports [] directDeps+  where+    directDeps =+      ordNub+        [ (depPkgName d, ln)+        | d <- targetBuildDepends bi+        , ln <- NES.toList (depLibraries d)+        ]+    -- A local package's buck2 haskell_library() rule only ever declares+    -- its own real source modules - unlike a real GHC package db entry,+    -- it has no way to also claim modules reexported (`reexported-+    -- modules:` in the .cabal file) from elsewhere. So a component that+    -- depends on a local package with reexports (e.g. `Cabal` re-+    -- exporting a chunk of `Cabal-syntax`) needs the reexport's origin+    -- package added as an explicit direct dependency too, or - once+    -- compile.bzl's `-hide-all-packages` is in effect - GHC can't find+    -- the reexported module at all: "Could not load module ...". This+    -- closure adds those origins (transitively, in case a reexporting+    -- package itself depends on another reexporting package).+    closeOverReexports seen [] = seen+    closeOverReexports seen (p@(pn, _) : rest)+      | p `elem` seen = closeOverReexports seen rest+      | otherwise =+          let origins = maybe [] snd (Map.lookup pn localIndex)+           in closeOverReexports (p : seen) (rest ++ [(o, LMainLibName) | o <- origins])++-- | Resolve each module in @hs-source-dirs@ to its real file, trying+-- @.hs@\/@.lhs@\/@.hsc@ (and the other extensions buck2\/haskell.bzl knows+-- how to preprocess) in turn - returning @(moduleName, source)@ pairs for the+-- dict form of @srcs@, which - unlike the plain-list form - is unaffected+-- by @hs-source-dirs@ not matching the BUCK package's own directory. Files+-- are looked up in @sources@ (see 'sourceCandidates'). The package's+-- @Paths_\<pkg\>@ module (if listed) is special-cased: no such file exists+-- anywhere - Cabal's own Setup.hs generates it fresh on every real build -+-- so 'pathsModuleFile' stands one in, which needs this component's real+-- 'LocalBuildInfo'\/'ComponentLocalBuildInfo' (see 'lbiClbiFor'); without+-- one that module fails to resolve, like a missing source file. Also returns+-- the autogen files picked up along the way - in practice just the+-- @Paths_\<pkg\>@ one, at most once, if that module was among @mods@.+--+-- 'Left' (with a description of each module that couldn't be resolved) if+-- *any* module couldn't be - the caller skips the whole component in that+-- case, rather than emitting a rule that references a source file that+-- doesn't exist: buck2 doesn't merely warn about that, it fails outright+-- while evaluating the @BUCK@ file, which (since a @buck2 build //...@+-- evaluates every @BUCK@ file up front) would otherwise take the *entire*+-- build down over one unresolvable module in one component of one package.+resolveModules :: Set FilePath -> PackageDescription -> Maybe (LocalBuildInfo, ComponentLocalBuildInfo) -> BuildInfo -> [ModuleName.ModuleName] -> Either [String] ([(String, Src)], [AutogenFile])+resolveModules sources pkgDesc mlbiClbi bi mods =+  case partitionEithers (map (resolveOne sources pkgDesc mlbiClbi bi) mods) of+    ([], triples) -> Right ([(n, v) | (n, v, _) <- triples], concat [fs | (_, _, fs) <- triples])+    (problems, _) -> Left problems++resolveOne :: Set FilePath -> PackageDescription -> Maybe (LocalBuildInfo, ComponentLocalBuildInfo) -> BuildInfo -> ModuleName.ModuleName -> Either String (String, Src, [AutogenFile])+resolveOne sources pkgDesc mlbiClbi bi m+  | m == autogenPathsModuleName pkgDesc = case mlbiClbi of+      Nothing ->+        Left $ "cabal buck2: couldn't generate " ++ prettyShow m ++ " (no LocalBuildInfo available for this component)"+      Just (lbi, clbi) ->+        let file = pathsModuleFile pkgDesc lbi clbi m+         in Right (prettyShow m, SrcAutogen (autogenName file), [file])+  | otherwise = case firstExisting sources (moduleCandidates bi m) of+      Just real -> Right (prettyShow m, SrcFile real, [])+      Nothing ->+        Left $+          "cabal buck2: couldn't find a source file for module "+            ++ prettyShow m+            ++ " under "+            ++ intercalate ", " (sourceDirs bi)++-- | Cabal's own Setup.hs generates a @Paths_\<pkg\>@ module fresh at+-- configure\/build time (giving @version@\/@getDataFileName@\/etc) - no+-- real source file for it exists anywhere to find. Generated here from+-- real Cabal's own 'generatePathsModule' (given this component's real+-- 'LocalBuildInfo'\/'ComponentLocalBuildInfo' - see 'lbiClbiFor'), so+-- install-dir\/relocatability logic matches a plain @cabal build@ exactly.+-- It is referenced from @srcs@ by its @export_file()@ target rather than a+-- path: @cabal-buck2\/autogen\/@ has its own @BUCK@ file, so it is a+-- different buck2 package from the one the sources are in, and a file living+-- there can no longer be named by a package-relative path.+pathsModuleFile :: PackageDescription -> LocalBuildInfo -> ComponentLocalBuildInfo -> ModuleName.ModuleName -> AutogenFile+pathsModuleFile pkgDesc lbi clbi m =+  AutogenFile+    { autogenName = ModuleName.toFilePath m+    , autogenPath = ModuleName.toFilePath m <.> "hs"+    , autogenContents = generatePathsModule pkgDesc lbi clbi+    }++-- | A @detailed-0.9@ test-suite's own stub @Main@ - see 'testSuite's own+-- haddock for why this is a from-scratch driver over+-- @Distribution.TestSuite@'s public API, not real Cabal's own+-- @Setup.hs@-generated one (@Distribution.Simple.Test.LibV09.stubMain@,+-- which expects a handshake over stdin buck2 has no way to provide).+-- Returned as the test-suite's @main_is@ source, along with the file,+-- referenced by target for the same reason as 'pathsModuleFile'.+detailedTestStub :: String -> ModuleName.ModuleName -> (Src, AutogenFile)+detailedTestStub targetName testModule = (SrcAutogen name, file)+  where+    name = targetName ++ "-stub-main"+    file = AutogenFile{autogenName = name, autogenPath = targetName </> "Main.hs", autogenContents = contents}+    contents =+      unlines+        [ "-- @generated by `cabal buck2` - do not edit by hand."+        , "module Main (main) where"+        , ""+        , "import Distribution.TestSuite"+        , "import qualified " ++ prettyShow testModule ++ " as CabalBuck2TestModule"+        , "import System.Exit (ExitCode (..), exitWith)"+        , ""+        , "main :: IO ()"+        , "main = do"+        , "  ts <- CabalBuck2TestModule.tests"+        , "  oks <- mapM runTest ts"+        , "  exitWith (if and oks then ExitSuccess else ExitFailure 1)"+        , ""+        , "runTest :: Test -> IO Bool"+        , "runTest (Test ti) = run ti >>= report (name ti)"+        , "runTest (Group _ _ ts) = and <$> mapM runTest ts"+        , "runTest (ExtraOptions _ t) = runTest t"+        , ""+        , "report :: String -> Progress -> IO Bool"+        , "report n (Progress msg next) = putStrLn (n ++ \": \" ++ msg) >> next >>= report n"+        , "report n (Finished Pass) = putStrLn (n ++ \": PASS\") >> return True"+        , "report n (Finished (Fail msg)) = putStrLn (n ++ \": FAIL: \" ++ msg) >> return False"+        , "report n (Finished (Error msg)) = putStrLn (n ++ \": ERROR: \" ++ msg) >> return False"+        ]++-- | The files a module's source could be, in order of preference: in each+-- of the @hs-source-dirs@ in turn, with each extension buck2/haskell.bzl's+-- own srcs-resolution (@_resolve_src@) knows to run through+-- hsc2hs()\/alex()\/happy().+moduleCandidates :: BuildInfo -> ModuleName.ModuleName -> [FilePath]+moduleCandidates bi m = [dir </> ModuleName.toFilePath m <.> ext | dir <- sourceDirs bi, ext <- ["hs", "lhs", "hsc", "x", "y"]]++mainIsCandidates :: BuildInfo -> FilePath -> [FilePath]+mainIsCandidates bi mainIs = [dir </> mainIs | dir <- sourceDirs bi]++firstExisting :: Set FilePath -> [FilePath] -> Maybe FilePath+firstExisting sources = find (`Set.member` sources)++-- | 'Left' if the main-is file couldn't be found - see 'resolveModules'+-- for why the caller must skip the whole component rather than emit a+-- rule pointing at a nonexistent file.+resolveMainIs :: Set FilePath -> BuildInfo -> FilePath -> Either [String] FilePath+resolveMainIs sources bi mainIs =+  maybe (Left ["cabal buck2: couldn't find main-is file " ++ mainIs ++ " under " ++ intercalate ", " (sourceDirs bi)]) Right $+    firstExisting sources (mainIsCandidates bi mainIs)++-- | Every path (relative to the package's directory) that+-- 'generatePackageTargets' may look for a component's source in. The+-- generator does no IO, so callers check which of these exist and pass those+-- in.+sourceCandidates :: PackageDescription -> [FilePath]+sourceCandidates pkgDesc = ordNub (concatMap candidates (pkgBuildableComponents pkgDesc))+  where+    candidates comp = case comp of+      CLib lib -> modules bi (exposedModules lib ++ otherModules bi)+      CExe exe -> mainIs bi (getSymbolicPath (modulePath exe)) ++ modules bi (otherModules bi)+      CTest test -> case testInterface test of+        TestSuiteExeV10 _ main -> mainIs bi (getSymbolicPath main) ++ modules bi (otherModules bi)+        TestSuiteLibV09 _ testModule -> modules bi (testModule : otherModules bi)+        _ -> []+      CBench bench -> case benchmarkInterface bench of+        BenchmarkExeV10 _ main -> mainIs bi (getSymbolicPath main) ++ modules bi (otherModules bi)+        _ -> []+      CFLib _ -> []+      where+        bi = componentBuildInfo comp+    modules bi = concatMap (moduleCandidates bi)+    mainIs = mainIsCandidates++sourceDirs :: BuildInfo -> [FilePath]+sourceDirs bi = case map getSymbolicPath (hsSourceDirs bi) of+  [] -> ["."]+  ds -> ds
+ src/Distribution/Client/Buck2/LocalPackages.hs view
@@ -0,0 +1,129 @@+-- | Which packages of a project @cabal buck2@ builds from source with buck2+-- (rather than leaving to cabal), and facts about them that come from the+-- elaborated install plan rather than from their @.cabal@ files.+module Distribution.Client.Buck2.LocalPackages+  ( isBuiltLocally+  , packageSourceDir+  , componentNamesFor+  , builtLocalPackages+  , wantedBuildTools+  , projectTestOptions+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import qualified Data.Map as Map++import Distribution.Client.DistDirLayout (DistDirLayout (distUnpackedSrcDirectory))+import qualified Distribution.Client.InstallPlan as InstallPlan+import Distribution.Client.ProjectPlanning+  ( ElaboratedConfiguredPackage (..)+  , ElaboratedInstallPlan+  )+import Distribution.Client.ProjectPlanning.Types+  ( BuildStyle (BuildAndInstall)+  , ElaboratedPackageOrComponent (ElabComponent, ElabPackage)+  , elabComponentName+  )+import Distribution.Client.Types.PackageLocation (PackageLocation (..))++import Distribution.Package (PackageName, packageId, packageName)+import Distribution.PackageDescription (PackageDescription)+import qualified Distribution.PackageDescription as PD+import Distribution.Simple.InstallDirs (PathTemplate)+import Distribution.Simple.Utils (die', ordNub)+import Distribution.Types.Component (componentBuildInfo, componentName)+import Distribution.Types.ComponentName (ComponentName (CTestName))+import Distribution.Types.ExeDependency (ExeDependency (..))+import Distribution.Types.UnqualComponentName (UnqualComponentName)+++-- | Does buck2 (rather than cabal) build this package from source? True+-- for every genuinely local package, *plus* every non-local one whose own+-- build was forced 'inplace' by depending on one - when a non-local+-- package is forced inplace it must be built by buck2 from source too,+-- otherwise the build would contain multiple incompatible versions of the+-- local dependency. A real-world example is hackage-security in the cabal+-- project, which is not a local package but depends on the local+-- Cabal-syntax.+isBuiltLocally :: ElaboratedConfiguredPackage -> Bool+isBuiltLocally elab = elabLocalToProject elab || elabBuildStyle elab /= BuildAndInstall++-- | Real on-disk source directory for any package buck2 builds from+-- source - a genuinely local one (always 'LocalUnpackedPackage') or an+-- inplace non-local one, resolved the same way+-- 'Distribution.Client.ProjectPlanning.Types.dataDirEnvVarForPackage'+-- does for the same 'BuildInplaceOnly' case: a plain source checkout+-- uses its own path directly, anything fetched as a tarball\/repo was+-- already unpacked to 'distUnpackedSrcDirectory' to be built inplace in+-- the first place.+packageSourceDir :: Verbosity -> DistDirLayout -> ElaboratedConfiguredPackage -> IO FilePath+packageSourceDir verbosity distDirLayout elab = case elabPkgSourceLocation elab of+  LocalUnpackedPackage dir -> return dir+  _ | elabLocalToProject elab -> unsupported+  LocalTarballPackage{} -> return unpackedPath+  RemoteTarballPackage{} -> return unpackedPath+  RepoTarballPackage{} -> return unpackedPath+  RemoteSourceRepoPackage _ (Just localCheckout) -> return localCheckout+  RemoteSourceRepoPackage{} -> unsupported+  where+    unpackedPath = distUnpackedSrcDirectory distDirLayout (elabPkgSourceId elab)+    unsupported =+      die' verbosity $+        "cabal buck2: local package "+          ++ prettyShow (packageId elab)+          ++ " isn't an unpacked local directory - can't generate a BUCK file for it."++-- | The buildable component names for one elaborated node - either the+-- single component 'elabComponentName' itself names (per-component+-- elaboration, @ElabComponent@), or *every* buildable component of the+-- whole package it configured (whole-package elaboration,+-- @ElabPackage@ - see 'elabComponentName's own haddock, "there could be+-- more, but default this": one @configureFinal@ call in that mode+-- genuinely produces a 'ComponentLocalBuildInfo' for every component of+-- the package internally, regardless of which single one+-- 'elabComponentName' defaults to).+componentNamesFor :: ElaboratedConfiguredPackage -> PackageDescription -> [ComponentName]+componentNamesFor elab pkgDesc = case elabPkgOrComp elab of+  ElabComponent _ -> maybeToList (elabComponentName elab)+  ElabPackage _ -> [componentName comp | comp <- PD.pkgBuildableComponents pkgDesc]++-- | The source directory and (whole-package) 'PackageDescription' of every+-- package buck2 builds from source. Per-component elaboration gives each+-- such package one 'ElaboratedConfiguredPackage' per component, all+-- sharing the same directory and 'PackageDescription', so there is one+-- entry per directory here.+builtLocalPackages :: Verbosity -> DistDirLayout -> ElaboratedInstallPlan -> IO [(FilePath, PackageDescription)]+builtLocalPackages verbosity distDirLayout plan =+  fmap (nubBy ((==) `on` fst)) . sequenceA $+    [ do+      dir <- packageSourceDir verbosity distDirLayout elab+      return (dir, elabPkgDescription elab)+    | InstallPlan.Configured elab <- InstallPlan.toList plan+    , isBuiltLocally elab+    ]++-- | Every @pkg:exe@ named in any component's @build-tool-depends:@ across+-- the given packages.+wantedBuildTools :: [(FilePath, PackageDescription)] -> [(PackageName, UnqualComponentName)]+wantedBuildTools pkgs =+  ordNub+    [ (pn, exeName)+    | (_dir, pkgDesc) <- pkgs+    , comp <- PD.pkgBuildableComponents pkgDesc+    , ExeDependency pn exeName _ <- PD.buildToolDepends (componentBuildInfo comp)+    ]++-- | The project's @test-options:@ for each test-suite that has any - only+-- the elaborated package knows them (they aren't part of the @.cabal@ file+-- or the 'LocalBuildInfo').+projectTestOptions :: ElaboratedInstallPlan -> Map (PackageName, ComponentName) [PathTemplate]+projectTestOptions plan =+  Map.fromList+    [ ((packageName elab, cname), elabTestTestOptions elab)+    | InstallPlan.Configured elab <- InstallPlan.toList plan+    , isBuiltLocally elab+    , not (null (elabTestTestOptions elab))+    , cname@CTestName{} <- componentNamesFor elab (elabPkgDescription elab)+    ]
+ src/Distribution/Client/Buck2/Prebuilt.hs view
@@ -0,0 +1,558 @@+-- | Register all the external packages and tools that the project+-- depends on for buck2 under @third-party/haskell@.+--+-- Here's what we generate:+--+--  * @third-party/haskell/ghc-bin@: symlink to the GHC bin dir+--+--  * @third-party/haskell/ghc-<version>@: a symlink to the GHC lib dir+--+--  * @third-party/haskell/cabal-store@: symlink to the Cabal store+--+--  * @third-party/haskell/build-tools@: contains symlinks to all the build+--    tools this project depends on+--+--  * @third-party/haskell/BUCK@: all the @haskell_prebuilt_library()@ rules+--    for every external package that this project depends on. These+--    are either GHC built-in packages in @ghc-<version>@ or Cabal store+--    packages in @cabal-store@.+--+--  * @third-party/haskell/tools.bzl@: paths and constants for tools we+--    depend on.+module Distribution.Client.Buck2.Prebuilt+  ( generatePrebuilt+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import qualified Data.ByteString as BS+import Data.Char (isHexDigit)+import Data.List (stripPrefix)+import qualified Data.Map as Map++import System.Directory+  ( createDirectoryIfMissing+  , createFileLink+  , doesFileExist+  , doesPathExist+  , removeDirectoryRecursive+  , removeFile+  )+import System.FilePath+  ( isAbsolute+  , isPathSeparator+  , joinPath+  , makeRelative+  , pathSeparator+  , splitDirectories+  , splitPath+  , takeDirectory+  , (<.>)+  , (</>)+  )++import qualified Distribution.Client.InstallPlan as InstallPlan++import Distribution.Client.DistDirLayout+  ( CabalDirLayout (cabalStoreDirLayout)+  , StoreDirLayout (storeDirectory, storePackageDBPath)+  )+import Distribution.Client.ProjectPlanning+  ( ElaboratedConfiguredPackage (elabInstallDirs)+  , ElaboratedInstallPlan+  , ElaboratedSharedConfig (pkgConfigCompiler, pkgConfigCompilerProgs, pkgConfigPlatform)+  )+import Distribution.Client.ProjectPlanning.Types (elabComponentName)+import Distribution.Types.ComponentName (ComponentName (CExeName))++import Distribution.InstalledPackageInfo (parseInstalledPackageInfo)+import Distribution.Package (HasUnitId (installedUnitId), packageName, packageVersion)+import Distribution.Simple.BuildPaths (exeExtension)+import Distribution.Simple.Compiler+  ( Compiler (compilerProperties)+  , compilerVersion+  )+import Distribution.Simple.GHC (getGlobalPackageDB)+import qualified Distribution.Simple.InstallDirs as InstallDirs+import Distribution.Simple.Program.Builtin (ghcProgram)+import Distribution.Simple.Program.Db (lookupProgram)+import Distribution.Simple.Program.Types (programPath)+import Distribution.Simple.Utils (die', notice, ordNub, warn)+import Distribution.Types.InstalledPackageInfo+  ( InstalledPackageInfo+      ( depends+      , extraLibraries+      , hsLibraries+      , includeDirs+      , libraryDirs+      , libraryDynDirs+      , sourceLibName+      )+  )+import Distribution.Types.PackageName (PackageName)+import Distribution.Types.UnitId (UnitId, unUnitId)+import Distribution.Types.UnqualComponentName (UnqualComponentName, unUnqualComponentName)++import Distribution.Client.Buck2.Generate (libTargetName)+import Distribution.Client.Buck2.Starlark++-- | Generate\/refresh @third-party\/haskell@ from the dependency closure+-- of an already-built, already-pruned install plan. Returns:+--+--   * The name of every @wantedBuildTools@ pair this project's own+--     dependency closure resolved a real binary for - each one also+--     gets a real @export_file()@ target in the generated+--     @third-party\/haskell\/BUCK@ (@\/\/third-party\/haskell:\<name\>-exe@),+--     for "Distribution.Client.Buck2.Generate" to reference from a+--     component's own @build_tool_depends@ (see that module's own+--     @buildToolDependsArg@) - a real buck2 dependency edge, not a+--     host-filesystem symlink farm resolved once at generation time (the+--     wanted pair's own *local*-package half, if any, is a completely+--     separate case handled entirely by "Distribution.Client.Buck2.Generate" itself, referencing+--     that package's own real, already-generated @haskell_binary()@+--     target directly - this function only ever sees, and only needs to+--     handle, already-installed *external* dependencies).+--   * The real, already-parsed 'InstalledPackageInfo' for every resolved+--     dependency - callers that need a real+--     'Distribution.Simple.PackageIndex.InstalledPackageIndex' (e.g. to+--     build a genuine 'LocalBuildInfo' via "Distribution.Client.InLibrary",+--     the way "Distribution.Client.Buck2.Configure" does) can build one directly+--     from this via 'PackageIndex.fromList' without a second, independent+--     walk of the same @.conf@ files.+generatePrebuilt+  :: Verbosity+  -> FilePath+  -- ^ project root (the buck2 cell root)+  -> CabalDirLayout+  -> ElaboratedSharedConfig+  -> ElaboratedInstallPlan+  -- ^ 'elaboratedPlanToExecute': already pruned to exactly the (test\/+  -- benchmark-flag-aware) dependency closure that was just built. Never+  -- contains a package buck2 builds from source itself (a local package,+  -- or a non-local one built @inplace@ because it depends on one - see+  -- 'Distribution.Client.Buck2.LocalPackages.isBuiltLocally'): those get a real @haskell_library()@ from+  -- "Distribution.Client.Buck2.Generate" instead, and are excluded from+  -- this plan before it's even built. So every unit id here is either a+  -- GHC global\/boot package or one installed to the cabal store.+  -> [(PackageName, UnqualComponentName)]+  -- ^ Every @pkg:exe@ named in any local component's own+  -- @build-tool-depends:@, across the whole project - see this+  -- function's own return-value haddock above.+  -> IO (Set String, [InstalledPackageInfo])+generatePrebuilt verbosity projectRoot cabalDirLayout shared depsPlan wantedBuildTools = do+  ghcProg <-+    maybe (die' verbosity "cabal buck2: no 'ghc' program configured for this project - internal error.") return $+      lookupProgram ghcProgram (pkgConfigCompilerProgs shared)+  let compiler = pkgConfigCompiler shared+      ghcVersionStr = prettyShow (compilerVersion compiler)+      ghcDynamic = Map.lookup "GHC Dynamic" (compilerProperties compiler) == Just "YES"+      storeLayout = cabalStoreDirLayout cabalDirLayout+      storeDB = storePackageDBPath storeLayout compiler+      storeRootAbs = storeDirectory storeLayout compiler+      targetDir = projectRoot </> "third-party" </> "haskell"+      ghcBinAbs = takeDirectory (programPath ghcProg)++  globalDB <- getGlobalPackageDB verbosity ghcProg+  let globalRootAbs = takeDirectory globalDB++      allUnitIds = ordNub [installedUnitId pkg | pkg <- InstallPlan.toList depsPlan]+      paths =+        RepoPaths+          { rpGhcVersion = ghcVersionStr+          , rpGlobalRootAbs = globalRootAbs+          , rpStoreRootAbs = storeRootAbs+          , rpStoreDbRel = makeRelative storeRootAbs storeDB+          }++  createDirectoryIfMissing True targetDir+  ensureSymlink (targetDir </> ("ghc-" ++ ghcVersionStr)) globalRootAbs+  ensureSymlink (targetDir </> "ghc-bin") ghcBinAbs++  notice verbosity "cabal buck2: resolving prebuilt dependency closure"+  packages <- catMaybes <$> traverse (readPackage verbosity paths storeDB) allUnitIds+  let alexPath = findToolBinary paths shared depsPlan "alex"+      happyPath = findToolBinary paths shared depsPlan "happy"+      -- Every @build-tool-depends: pkg:exe@ pair the *external*+      -- (already-installed) dependency closure can resolve a real+      -- binary for - a local-package pair is never in `depsPlan` at all+      -- (see this function's own haddock on that parameter), so this+      -- silently, correctly resolves to nothing for one; entirely+      -- 'Distribution.Client.Buck2.Generate's own job to notice that case and reference the+      -- local package's real target directly instead.+      buildToolPaths =+        Map.fromList+          [ (unUnqualComponentName exeName, path)+          | (pn, exeName) <- ordNub wantedBuildTools+          , Just path <- [findExeBinaryAbs shared depsPlan pn exeName]+          ]++  -- Needed by any package's own library files, *and* independently by+  -- alex/happy's/any build-tool-depends executable's own binary path -+  -- a build-tool-only dependency (an executable, no library)+  -- contributes no ResolvedPackage at all (see readPackage), so+  -- checking `packages` alone would miss a project that needs alex/+  -- happy/a preprocessor tool but nothing else store-installed.+  when+    ( any ((== StoreDb) . rpDbKind) packages+        || any inStore (alexPath : happyPath : map (toRepoRelative paths) (Map.elems buildToolPaths))+    )+    $ ensureSymlink (targetDir </> "cabal-store") storeRootAbs++  -- An earlier version generated a filtered copy of the store's package db here.+  removeStaleStoreDB (targetDir </> "store-db")++  notice verbosity "cabal buck2: generating third-party/haskell/BUCK"+  writeBuckFile targetDir paths packages buildToolPaths++  writeToolsFile targetDir ghcVersionStr ghcDynamic alexPath happyPath++  return (Map.keysSet buildToolPaths, map rpInfo packages)++-- | The repo-relative anchors every generated path is expressed against:+-- the symlinks 'generatePrebuilt' just created, plus the GHC version+-- string (needed for both the global db's own relative path and+-- shared-library sonames).+data RepoPaths = RepoPaths+  { rpGhcVersion :: String+  , rpGlobalRootAbs :: FilePath+  , rpStoreRootAbs :: FilePath+  , rpStoreDbRel :: FilePath+  -- ^ The store's package db, relative to 'rpStoreRootAbs'.+  }++-- | Which of the two package dbs a unit id's @.conf@ lives in - decided+-- entirely by the unit id's own shape, no searching required (unlike the+-- Python predecessor, which had to glob for the store root, not knowing+-- its ABI-tag suffix in advance): GHC's global\/boot packages have a plain+-- @name-version@ id; a package installed to the store gets a+-- hash-suffixed one. (An @-inplace@ id - a package buck2 builds itself -+-- never gets here: see 'generatePrebuilt's plan parameter.)+data PkgDbKind = GlobalDb | StoreDb+  deriving (Eq)++classifyUnitId :: UnitId -> PkgDbKind+classifyUnitId uid+  | hasStoreHashSuffix s = StoreDb+  | otherwise = GlobalDb+  where+    s = unUnitId uid+    hasStoreHashSuffix rs = case break (== '-') (reverse rs) of+      (revSuffix, '-' : _) -> length revSuffix >= 20 && all isHexDigit revSuffix+      _ -> False++-- | A resolved package: its metadata plus which package db it came from+-- (needed to pick the right @db =@ value in the generated rule, and to+-- know where to find its @.conf@ file).+data ResolvedPackage = ResolvedPackage+  { rpInfo :: InstalledPackageInfo+  , rpUnitId :: UnitId+  , rpDbKind :: PkgDbKind+  , rpStaticLibs :: [FilePath]+  , rpProfiledLibs :: [FilePath]+  , rpSharedLibs :: [(String, FilePath)]+  }++confPath :: FilePath -> FilePath -> UnitId -> FilePath+confPath globalRootAbs storeDB uid = case classifyUnitId uid of+  GlobalDb -> globalRootAbs </> "package.conf.d" </> unUnitId uid <.> "conf"+  StoreDb -> storeDB </> unUnitId uid <.> "conf"++readPackage :: Verbosity -> RepoPaths -> FilePath -> UnitId -> IO (Maybe ResolvedPackage)+readPackage verbosity paths storeDB uid = do+  let dbKind = classifyUnitId uid+      path = confPath (rpGlobalRootAbs paths) storeDB uid+  exists <- doesFileExist path+  if not exists+    then do+      warn verbosity $+        "cabal buck2: no .conf file for "+          ++ unUnitId uid+          ++ " (expected at "+          ++ path+          ++ "); packages are only found in GHC's global package db and the cabal store, not in other package dbs"+      return Nothing+    else do+      contents <- BS.readFile path+      case parseInstalledPackageInfo contents of+        Left errs -> do+          warn verbosity $ "cabal buck2: couldn't parse " ++ path ++ ": " ++ intercalate "; " (toList errs)+          return Nothing+        Right (_warnings, ipi0) -> do+          -- '${pkgroot}' is the directory *containing* the package db (one+          -- level above the .conf file's own db directory, e.g.+          -- ".../lib" for a ".../lib/package.conf.d/<uid>.conf" file) -+          -- not the db directory itself.+          let ipi = mungePkgroot (takeDirectory (takeDirectory path)) ipi0+              isRts = prettyShow (packageName ipi) == "rts"+          staticLibs <- findLibs paths (libraryDirs ipi) ["lib" ++ stem <.> "a" | stem <- hsLibraries ipi]+          -- GHC doesn't build profiled RTS libraries the normal way - see+          -- gen-haskell-prebuilt.py's own note on this, which this+          -- inherits without fully understanding why either.+          profiledLibs <-+            if isRts+              then return []+              else findLibs paths (libraryDirs ipi) ["lib" ++ stem ++ "_p" <.> "a" | stem <- hsLibraries ipi]+          sharedLibs <- findSharedLibs paths ipi+          return $ Just (ResolvedPackage ipi uid dbKind staticLibs profiledLibs sharedLibs)++-- | Resolve each candidate filename against @dirs@ in turn, keeping only+-- the ones that actually exist on disk (unlike gen-haskell-prebuilt.py's+-- Python predecessor, this doesn't assume every @hs-libraries@ stem has a+-- library of every flavour - e.g. rts's second stem, \"Cffi\", only ever+-- ships as a static archive, never as a @.so@).+findLibs :: RepoPaths -> [FilePath] -> [String] -> IO [FilePath]+findLibs paths dirs fnames = catMaybes <$> traverse (findOne paths dirs) fnames++findOne :: RepoPaths -> [FilePath] -> String -> IO (Maybe FilePath)+findOne paths dirs fname = go dirs+  where+    go [] = return Nothing+    go (d : ds) = do+      let absPath = d </> fname+      exists <- doesFileExist absPath+      if exists+        then return (toRepoRelative paths absPath)+        else go ds++findSharedLibs :: RepoPaths -> InstalledPackageInfo -> IO [(String, FilePath)]+findSharedLibs paths ipi = catMaybes <$> traverse oneStem (hsLibraries ipi)+  where+    dirs = libraryDynDirs ipi ++ libraryDirs ipi+    oneStem stem = do+      let soname = "lib" ++ stem ++ "-ghc" ++ rpGhcVersion paths ++ ".so"+      mpath <- findOne paths dirs soname+      return ((,) soname <$> mpath)++-- | @.conf@ files use @${pkgroot}@ (the directory containing the package+-- db) as a portable stand-in for their own absolute location - GHC's own+-- @ghc-pkg@ expands this when it serves package info, but reading the+-- file directly (as 'parseInstalledPackageInfo' does here) doesn't, so+-- library-dirs\/include-dirs come back as literal, unusable+-- @"${pkgroot}/..."@ strings unless expanded by hand. Mirrors+-- 'Distribution.Simple.Program.HcPkg.mungePackagePaths', which isn't+-- exported.+mungePkgroot :: FilePath -> InstalledPackageInfo -> InstalledPackageInfo+mungePkgroot pkgroot ipi =+  ipi+    { libraryDirs = map munge (libraryDirs ipi)+    , libraryDynDirs = map munge (libraryDynDirs ipi)+    , includeDirs = map munge (includeDirs ipi)+    }+  where+    munge p = maybe p collapseDotDot (stripPkgroot p)+    stripPkgroot p = case splitPath p of+      (root : rest) -> case stripPrefix "${pkgroot}" root of+        Just [sep] | isPathSeparator sep -> Just (pkgroot </> joinPath rest)+        _ -> Nothing+      _ -> Nothing++-- | @${pkgroot}@ substitution routinely produces a @.../package.conf.d/+-- ../lib/...@ path (since @${pkgroot}@ is the db directory, and the real+-- libraries live next to it, not under it) - harmless as a real filesystem+-- path, but buck2 rejects any @attrs.source()@ containing a literal+-- @".."@ component ("expected a normalized path"), so it has to be+-- collapsed before it ever reaches a generated rule. Unlike+-- 'System.FilePath.normalise' (which only tidies separators\/dots, not+-- @".."@ segments - not safe in general with symlinks in play, but safe+-- here since every path this is applied to is already fully resolved:+-- real, non-symlink directories under GHC's own libdir or the cabal+-- store).+collapseDotDot :: FilePath -> FilePath+collapseDotDot = joinPath . reverse . foldl' step [] . splitDirectories+  where+    step (top : rest) ".." | top /= ".." && top /= [pathSeparator] = rest+    step stack "." = stack+    step stack seg = seg : stack++-- | Point @link@ at @target@, creating or repointing it as needed.+ensureSymlink :: FilePath -> FilePath -> IO ()+ensureSymlink link target = do+  exists <- doesPathExist link+  when exists $ removeFile link+  createFileLink target link++-- | Remove the filtered package db that earlier versions generated.+removeStaleStoreDB :: FilePath -> IO ()+removeStaleStoreDB dir = do+  exists <- doesPathExist dir+  when exists $ removeDirectoryRecursive dir++writeBuckFile :: FilePath -> RepoPaths -> [ResolvedPackage] -> Map String FilePath -> IO ()+writeBuckFile targetDir paths packages buildToolPaths =+  writeFile (targetDir </> "BUCK") (renderFile header [] calls)+  where+    header = "@generated by `cabal buck2` - do not edit by hand.\nRe-run `cabal buck2` to update."+    uidToTarget = Map.fromList [(rpUnitId p, targetName p) | p <- packages]+    calls =+      [prebuiltCall paths uidToTarget p | p <- packages]+        ++ [buildToolExportCall name relPath | (name, relPath) <- Map.toList (Map.mapMaybe (toRepoRelative paths) buildToolPaths)]++-- | One @export_file()@ per resolved @build-tool-depends@ executable -+-- gives it a real buck2 target ('DefaultInfo' with a single default+-- output, basename forced to the tool's own bare name, executable bit+-- forced on regardless of whatever @copy_file@'s own default happens to+-- preserve) that any component's own @build_tool_depends@ can reference+-- directly, the same @export_file()@ primitive this project already+-- uses for autogen files - see "Distribution.Client.Buck2.Generate"'s+-- own @buildToolDependsArg@ for the consuming side, and+-- @buck2\/prelude\/decls\/haskell_common.bzl@'s own comment on the attr+-- for why this needs to be a real target at all (not a host-filesystem+-- symlink, superseded by this).+buildToolExportCall :: String -> FilePath -> Call+buildToolExportCall name relPath =+  call+    "export_file"+    [ ("name", str (name ++ "-exe"))+    , ("src", str relPath)+    , ("out", str name)+    , ("mode", str "copy")+    , ("executable_bit_override", VBool True)+    , ("visibility", strList ["PUBLIC"])+    ]++-- | Unlike a local package (one @haskell_library()@ per library, main or+-- named sub-library alike - see 'libTargetName'), a *prebuilt* one used+-- to get exactly one @haskell_prebuilt_library()@ per package name,+-- regardless of how many of its libraries were actually in the+-- dependency closure - a real bug, not just a theoretical gap: a package+-- with an internal sub-library (e.g. @attoparsec@'s own+-- @attoparsec-internal@) resolves to *two* units here, and buck2+-- rejected the second @haskell_prebuilt_library()@ outright as a+-- duplicate target the first time this was tried against a real,+-- large project. 'sourceLibName' (parsed straight from the @.conf@,+-- the exact same 'LibraryName' a local package's own 'libName' would+-- give) is what 'libTargetName' needs to tell them apart, the same way+-- it already does for local packages.+targetName :: ResolvedPackage -> String+targetName p = libTargetName (packageName (rpInfo p)) (sourceLibName (rpInfo p))++prebuiltCall :: RepoPaths -> Map UnitId String -> ResolvedPackage -> Call+prebuiltCall paths uidToTarget p =+  call+    "haskell_prebuilt_library"+    ( [ ("name", str (targetName p))+      , ("version", str (prettyShow (packageVersion (rpInfo p))))+      , ("id", str (unUnitId (rpUnitId p)))+      , ("db", str (if rpDbKind p == GlobalDb then globalDbRel else "cabal-store" </> rpStoreDbRel paths))+      , ("static_libs", strList (rpStaticLibs p))+      ]+        ++ [("profiled_static_libs", strList (rpProfiledLibs p)) | not (null (rpProfiledLibs p))]+        ++ [("pic_profiled_static_libs", strList (rpProfiledLibs p)) | not (null (rpProfiledLibs p))]+        ++ [("shared_libs", VDict [(soname, str sopath) | (soname, sopath) <- rpSharedLibs p])]+        ++ [("cxx_header_dirs", strList headerDirs) | not (null headerDirs)]+        ++ [("exported_linker_flags", strList extraLinkerFlags) | not (null extraLinkerFlags)]+        ++ [("deps", strList depTargets) | not (null depTargets)]+        ++ [("visibility", strList ["PUBLIC"])]+    )+  where+    info = rpInfo p+    globalDbRel = ("ghc-" ++ rpGhcVersion paths) </> "package.conf.d"+    headerDirs = mapMaybe (toRepoRelative paths) (includeDirs info)+    extraLinkerFlags = ["-l" ++ lib | lib <- extraLibraries info]+    depTargets = ordNub [":" ++ t | d <- depends info, Just t <- [Map.lookup d uidToTarget]]++-- | Convert an absolute path under GHC's libdir or the cabal store into+-- one relative to @third-party\/haskell@, via whichever symlink+-- 'generatePrebuilt' pointed there actually contains it - the same+-- translation gen-haskell-prebuilt.py's own @abs_to_rel@ did.+toRepoRelative :: RepoPaths -> FilePath -> Maybe FilePath+toRepoRelative paths path+  | not (isAbsolute path) = Nothing+  | otherwise =+      relTo (rpGlobalRootAbs paths) ("ghc-" ++ rpGhcVersion paths) path+        <|> relTo (rpStoreRootAbs paths) "cabal-store" path++relTo :: FilePath -> FilePath -> FilePath -> Maybe FilePath+relTo root repoRelPrefix path =+  let r = makeRelative root path+   in if r /= path then Just (repoRelPrefix </> r) else Nothing++-- | The store-installed binary path (repo-relative, like everything else+-- 'toRepoRelative' produces) for a build-tool dependency package - e.g.+-- @alex@\/@happy@, needed by buck2/alex_happy.bzl to preprocess @.x@\/@.y@+-- sources - or 'Nothing' if the project doesn't need it at all. A+-- simplified 'CmdListBin.elaboratedPackage'\/@bin_file'@: alex\/happy are+-- always external Hackage dependencies, never a local package, so the+-- inplace-build-style branch that logic also has to handle never applies+-- here, and (being plain, single-executable packages) their own+-- executable is always named after the package itself, with no need to+-- resolve a target selector to find out which component that is.+findToolBinary :: RepoPaths -> ElaboratedSharedConfig -> ElaboratedInstallPlan -> String -> Maybe FilePath+findToolBinary paths shared plan toolName =+  listToMaybe+    [ rel+    | pkg <- InstallPlan.toList plan+    , Just elab <- [configuredOrInstalled pkg]+    , prettyShow (packageName elab) == toolName+    , let absPath = InstallDirs.bindir (elabInstallDirs elab) </> toolName <.> exeExtension (pkgConfigPlatform shared)+    , Just rel <- [toRepoRelative paths absPath]+    ]++-- | The real, absolute binary path for one @pkg:exe@ pair - 'Nothing' if+-- that package isn't in the resolved dependency closure at all, or has+-- no matching executable+-- component. Unlike 'findToolBinary' (which assumes the executable is+-- named after its own package - true for alex\/happy, both self-named+-- single-executable packages), this matches the package and executable+-- names independently, since a @build-tool-depends: pkg:exe@ entry can+-- legitimately name them differently.+--+-- A package brought in purely as a @build-tool-depends@ needs commonly+-- elaborate to *two* distinct plan nodes for the same package name - a+-- whole-package one (@elabComponentName@ @Nothing@ or some other+-- component) and a real per-executable one (@elabComponentName ==+-- Just (CExeName exeName)@) - and only the latter's own+-- 'elabInstallDirs' has a real, populated @bindir@ (confirmed the hard+-- way: the former's own @bindir@ computes to a real-looking but+-- entirely empty directory, no error, just @createProcess: posix_spawnp:+-- does not exist@ at the point of actually trying to run it). Preferring+-- an exact component-name match - falling back to any match sharing the+-- package name only if none exists, the same as 'findToolBinary' always+-- did - fixes this without needing to know in advance which shape a+-- given package's own plan happens to take.+findExeBinaryAbs :: ElaboratedSharedConfig -> ElaboratedInstallPlan -> PackageName -> UnqualComponentName -> Maybe FilePath+findExeBinaryAbs shared plan pn exeName =+  listToMaybe (exact ++ fallback)+  where+    candidates =+      [ elab+      | pkg <- InstallPlan.toList plan+      , Just elab <- [configuredOrInstalled pkg]+      , packageName elab == pn+      ]+    binPathFor elab = InstallDirs.bindir (elabInstallDirs elab) </> unUnqualComponentName exeName <.> exeExtension (pkgConfigPlatform shared)+    exact = [binPathFor elab | elab <- candidates, elabComponentName elab == Just (CExeName exeName)]+    fallback = [binPathFor elab | elab <- candidates]++-- | Same package, in either of the two states a *non-local* dependency+-- that's actually going to be used can be in: 'Configured' (needs+-- building this run) or 'Installed' (already built and installed from a+-- previous run, nothing to do - which is what alex/happy settle into on+-- any @cabal buck2@ after the first, once their build is cached). Unlike+-- 'installedUnitId' (a 'HasUnitId' method, already defined uniformly+-- across all three 'GenericPlanPackage' constructors), there's no+-- existing helper for this, since most other call sites here only need+-- the unit id, not the full 'ElaboratedConfiguredPackage'.+configuredOrInstalled :: InstallPlan.GenericPlanPackage ipkg srcpkg -> Maybe srcpkg+configuredOrInstalled (InstallPlan.Configured spkg) = Just spkg+configuredOrInstalled (InstallPlan.Installed spkg) = Just spkg+configuredOrInstalled InstallPlan.PreExisting{} = Nothing++inStore :: Maybe FilePath -> Bool+inStore = maybe False ("cabal-store" `isPrefixOf`)++writeToolsFile :: FilePath -> String -> Bool -> Maybe FilePath -> Maybe FilePath -> IO ()+writeToolsFile targetDir ghcVersionStr ghcDynamic alexPath happyPath =+  writeFile (targetDir </> "tools.bzl") $+    unlines+      [ "# @generated by `cabal buck2` - do not edit by hand."+      , "# Re-run `cabal buck2` to update."+      , ""+      , "GHC_VERSION = " ++ show ghcVersionStr+      , "GHC_BIN_DIR = \"third-party/haskell/ghc-bin\""+      , "GHC_DYNAMIC = " ++ (if ghcDynamic then "True" else "False")+      , ""+      , "ALEX = " ++ show ("third-party/haskell/" ++ fromMaybe "missing" alexPath)+      , "HAPPY = " ++ show ("third-party/haskell/" ++ fromMaybe "missing" happyPath)+      ]
+ src/Distribution/Client/Buck2/Schedule.hs view
@@ -0,0 +1,73 @@+-- | Running a dependency graph of jobs concurrently.+module Distribution.Client.Buck2.Schedule+  ( runDependencyGraph+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import qualified Data.Map as Map+import qualified Data.Set as Set++import qualified Control.Concurrent.Async as Async+import Control.Concurrent.STM+  ( atomically+  , modifyTVar'+  , newTVarIO+  , readTVar+  , retry+  , writeTVar+  )++import Distribution.Simple.Utils (ordNub)++-- | @runDependencyGraph n deps process@ runs @process k@ once for every key+-- @k@ of @deps@, on up to @n@ threads, but only after @process@ has finished+-- for each of the keys @deps@ lists for @k@. Dependencies that aren't keys of+-- @deps@ are ignored. Returns when every job has finished; if one throws, the+-- rest are cancelled and the exception is rethrown. The graph must be+-- acyclic.+--+-- Jobs are scheduled by their direct dependencies alone, so a job is+-- available to run as soon as the jobs it depends on are done.+runDependencyGraph :: Ord k => Int -> Map k [k] -> (k -> IO ()) -> IO ()+runDependencyGraph numWorkers deps process = do+  let jobs = Map.keysSet deps+      dependenciesOf = Map.map (ordNub . filter (`Set.member` jobs)) deps+      dependents = Map.fromListWith (++) [(d, [k]) | (k, ds) <- Map.toList dependenciesOf, d <- ds]+      totalJobs = Map.size deps++  remainingVar <- newTVarIO (Map.map length dependenciesOf)+  readyVar <- newTVarIO [k | (k, []) <- Map.toList dependenciesOf]+  finishedVar <- newTVarIO (0 :: Int)++  let finishJob k = do+        remaining <- readTVar remainingVar+        let (remaining', newlyReady) =+              foldl'+                ( \(rs, ready) d ->+                    let n = Map.findWithDefault 0 d rs - 1+                     in (Map.insert d n rs, if n == 0 then d : ready else ready)+                )+                (remaining, [])+                (Map.findWithDefault [] k dependents)+        writeTVar remainingVar remaining'+        modifyTVar' readyVar (newlyReady ++)+        modifyTVar' finishedVar (+ 1)++      nextJob = do+        ready <- readTVar readyVar+        case ready of+          (k : rest) -> writeTVar readyVar rest >> return (Just k)+          [] -> do+            finished <- readTVar finishedVar+            if finished == totalJobs then return Nothing else retry++      worker = do+        mk <- atomically nextJob+        for_ mk $ \k -> do+          process k+          atomically (finishJob k)+          worker++  Async.replicateConcurrently_ numWorkers worker
+ src/Distribution/Client/Buck2/Setup.hs view
@@ -0,0 +1,45 @@+-- | Set up the project for building with @buck2@+module Distribution.Client.Buck2.Setup+  ( checkBuck2Prelude+  , ensureBuckconfigAndPackage+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import System.Directory (doesDirectoryExist, doesFileExist)+import System.FilePath ((</>))++import Distribution.Simple.Utils (die', notice)+++-- | Check that the @buck2@ support code is at @buck2\/@+checkBuck2Prelude :: Verbosity -> FilePath -> IO ()+checkBuck2Prelude verbosity projectRoot = do+  exists <- doesDirectoryExist (projectRoot </> "buck2")+  unless exists $+    die' verbosity $+      unlines+        [ "No 'buck2/' directory found in the project root."+        , "Clone the buck2 prelude and support scripts with:"+        , ""+        , "    git clone https://github.com/simonmar/haskell-buck2 buck2"+        , ""+        , "then re-run 'cabal buck2'."+        ]++-- | Copy @.buckconfig@ and @PACKAGE@ if they don't exist+ensureBuckconfigAndPackage :: Verbosity -> FilePath -> IO ()+ensureBuckconfigAndPackage verbosity projectRoot = do+  copyIfMissing verbosity (exampleDir </> ".buckconfig") (projectRoot </> ".buckconfig")+  copyIfMissing verbosity (exampleDir </> "PACKAGE") (projectRoot </> "PACKAGE")+  where+    exampleDir = projectRoot </> "buck2" </> "example"++copyIfMissing :: Verbosity -> FilePath -> FilePath -> IO ()+copyIfMissing verbosity src dest = do+  exists <- doesFileExist dest+  unless exists $ do+    contents <- readFile src+    writeFile dest contents+    notice verbosity $ "cabal buck2: created " ++ dest ++ " (from " ++ src ++ ")"
+ src/Distribution/Client/Buck2/Spec.hs view
@@ -0,0 +1,94 @@+-- | The build spec: a description of a package's components from+-- which buck2\/cabal.bzl creates the buck2 rules.+-- "Distribution.Client.Buck2.Write" writes the spec into the+-- @BUCK.cabal.bzl@ file.+module Distribution.Client.Buck2.Spec+  ( specSchemaVersion+  , BuildSpec (..)+  , ComponentKind (..)+  , kindName+  , SpecComponent (..)+  , Src (..)+  , SpecDep (..)+  , SpecBuildTool (..)+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++-- | The version of the build spec format; must match @SCHEMA_VERSION@ in+-- buck2\/cabal.bzl.+specSchemaVersion :: Int+specSchemaVersion = 1++-- | A package's build spec: everything the generated rules are built from+-- that comes from Cabal rather than from buck2 conventions.+data BuildSpec = BuildSpec+  { specPackageName :: String+  , specPackageDir :: FilePath+  -- ^ Relative to the buck2 cell root, @.@ at the root.+  , specGhcOptions :: [String]+  -- ^ Supplied by the project, not the @.cabal@ file.+  , specComponents :: [SpecComponent]+  }++data ComponentKind = Library | Executable | TestSuite | Benchmark+  deriving (Eq)++-- | As it appears in the spec and in messages: @library@, @executable@,+-- @test-suite@ or @benchmark@.+kindName :: ComponentKind -> String+kindName Library = "library"+kindName Executable = "executable"+kindName TestSuite = "test-suite"+kindName Benchmark = "benchmark"++-- | One component of a build spec. List fields are empty when the+-- corresponding @.cabal@ field is.+data SpecComponent = SpecComponent+  { scKind :: ComponentKind+  , scName :: String+  -- ^ Also the name of the buck2 target.+  , scMainIs :: Maybe Src+  -- ^ The main module's source; not for a library.+  , scSrcs :: [(String, Src)]+  -- ^ Every other module (by module name) and its source.+  , scTestArgs :: [String]+  -- ^ The project's @test-options@ for a test-suite, with template variables+  -- expanded.+  , scGhcOptions :: [String]+  , scCppOptions :: [String]+  , scLanguage :: Maybe String+  , scExtensions :: [String]+  , scExtraLibraries :: [String]+  , scDeps :: [SpecDep]+  , scBuildTools :: [SpecBuildTool]+  , scCSources :: [FilePath]+  , scCxxSources :: [FilePath]+  , scCxxOptions :: [String]+  , scIncludeDirs :: [FilePath]+  , scPkgconfig :: [String]+  }++-- | A source file of a component: either a real file in the package+-- (relative to its directory), or one generated into the package's+-- @cabal-buck2\/autogen@ directory, named by its @export_file()@ entry there+-- (see 'Distribution.Client.Buck2.Generate.AutogenFile').+data Src = SrcFile FilePath | SrcAutogen String++-- | A library a component depends on.+data SpecDep = SpecDep+  { depPackage :: String+  , depLibrary :: Maybe String+  -- ^ The sub-library, if the main library isn't the one depended on.+  , depDir :: Maybe FilePath+  -- ^ The package's directory (relative to the cell root) if it's built by+  -- this project.+  }++-- | An executable that a component's @build-tool-depends@ needs on @PATH@.+data SpecBuildTool+  = -- | Built by this project, in the given directory.+    LocalTool {toolExe :: String, toolDir :: FilePath}+  | -- | An already-installed binary, exported from @third-party\/haskell@.+    ExternalTool {toolExe :: String}
+ src/Distribution/Client/Buck2/Starlark.hs view
@@ -0,0 +1,116 @@+-- | A minimal Starlark (BUCK file) pretty-printer.+--+-- This deliberately doesn't attempt to represent the whole Starlark+-- language: it covers exactly the subset (string/int/bool scalars, lists,+-- dicts, @load@ statements and top-level rule calls) that generated+-- @BUCK@\/@.bzl@ files need, so every generator in "Distribution.Client.Buck2"+-- builds output through the same renderer instead of hand-formatting+-- strings.+module Distribution.Client.Buck2.Starlark+  ( Value (..)+  , str+  , strList+  , Call (..)+  , call+  , renderCall+  , renderLoad+  , renderBinding+  , renderFile+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++-- | A Starlark expression, restricted to what BUCK files actually need:+-- scalars and (recursively) lists and dicts of them.+data Value+  = VStr String+  | VInt Int+  | VBool Bool+  | VList [Value]+  | VDict [(String, Value)]++str :: String -> Value+str = VStr++strList :: [String] -> Value+strList = VList . map VStr++-- | A single top-level rule invocation, e.g. @haskell_library(name = ..., ...)@.+data Call = Call+  { callFn :: String+  , callArgs :: [(String, Value)]+  }++call :: String -> [(String, Value)] -> Call+call = Call++indent :: Int -> String+indent n = replicate (4 * n) ' '++-- | Render like Python's @repr@ for a single-quoted string: this is what+-- both buck2/gen-haskell-prebuilt.py's generated output and every+-- hand-written @.bzl@ file in buck2/ already use.+renderStr :: String -> String+renderStr s = '\'' : concatMap escape s ++ "'"+  where+    escape '\\' = "\\\\"+    escape '\'' = "\\'"+    escape '\n' = "\\n"+    escape '\t' = "\\t"+    escape c = [c]++renderValue :: Int -> Value -> String+renderValue _ (VStr s) = renderStr s+renderValue _ (VInt n) = show n+renderValue _ (VBool b) = if b then "True" else "False"+renderValue _ (VList []) = "[]"+renderValue ind (VList xs) =+  "[\n"+    ++ concat [indent (ind + 1) ++ renderValue (ind + 1) x ++ ",\n" | x <- xs]+    ++ indent ind+    ++ "]"+renderValue _ (VDict []) = "{}"+renderValue ind (VDict kvs) =+  "{\n"+    ++ concat+      [ indent (ind + 1) ++ renderStr k ++ ": " ++ renderValue (ind + 1) v ++ ",\n"+      | (k, v) <- kvs+      ]+    ++ indent ind+    ++ "}"++-- | Render a rule call as a standalone top-level statement, one argument+-- per line (matching the style buck2/gen-haskell-prebuilt.py's own+-- generated BUCK file uses), terminated by a blank line.+renderCall :: Call -> String+renderCall (Call fn args) =+  fn+    ++ "(\n"+    ++ concat+      [ indent 1 ++ k ++ " = " ++ renderValue 1 v ++ ",\n"+      | (k, v) <- args+      ]+    ++ ")\n"++-- | Render a top-level @NAME = value@ statement.+renderBinding :: String -> Value -> String+renderBinding name v = name ++ " = " ++ renderValue 0 v ++ "\n"++-- | Render a @load("target", "name1", "name2")@ statement.+renderLoad :: String -> [String] -> String+renderLoad target names =+  "load(" ++ intercalate ", " (renderStr target : map renderStr names) ++ ")\n"++-- | Render a full generated file: a header comment, the accumulated+-- @load@ statements, then each rule call in turn, separated by blank+-- lines.+renderFile :: String -> [(String, [String])] -> [Call] -> String+renderFile header loads calls =+  unlines (map ("# " ++) headerLines)+    ++ "\n"+    ++ concatMap (uncurry renderLoad) loads+    ++ "\n"+    ++ intercalate "\n" (map renderCall calls)+  where+    headerLines = lines header
+ src/Distribution/Client/Buck2/Unpack.hs view
@@ -0,0 +1,101 @@+-- | Unpacking the source of packages that buck2 builds, without building+-- anything.+module Distribution.Client.Buck2.Unpack+  ( unpackInplaceSources+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import System.Directory (doesDirectoryExist, doesFileExist, renameDirectory)+import System.FilePath ((<.>), (</>))++import Distribution.Client.DistDirLayout (DistDirLayout (..))+import Distribution.Client.FetchUtils (fetchPackage)+import Distribution.Client.ProjectPlanning+  ( ElaboratedConfiguredPackage (..)+  , ElaboratedSharedConfig+  )+import Distribution.Client.ProjectPlanning.Types (elabDistDirParams)+import qualified Distribution.Client.Tar as Tar+import Distribution.Client.GlobalFlags (RepoContext)+import Distribution.Client.Types (PackageLocation (..))++import Distribution.Package (PackageId, packageId, packageName)+import Distribution.Simple.Utils (createDirectoryIfMissingVerbose, die', info, writeFileAtomic)+import Distribution.Verbosity (Verbosity)++import qualified Data.ByteString.Lazy as LBS++-- | Fetch (if not already downloaded) and unpack the source of each given+-- package to where an inplace build of it would find it (under+-- 'distUnpackedSrcDirectory'). Packages whose source isn't a tarball (a+-- user-managed local directory, a source-repository checkout) need no+-- unpacking and are left alone.+unpackInplaceSources+  :: Verbosity+  -> DistDirLayout+  -> ElaboratedSharedConfig+  -> ((RepoContext -> IO ()) -> IO ())+  -> [ElaboratedConfiguredPackage]+  -> IO ()+unpackInplaceSources verbosity distDirLayout sharedConfig withRepoCtx pkgs =+  unless (null pkgs) $+    withRepoCtx $ \repoctx ->+      for_ pkgs $ \pkg -> do+        mtarball <- case elabPkgSourceLocation pkg of+          LocalTarballPackage tarball -> return (Just tarball)+          loc@RemoteTarballPackage{} -> fetched repoctx loc+          loc@RepoTarballPackage{} -> fetched repoctx loc+          _ -> return Nothing+        for_ mtarball $ \tarball ->+          unpackTarball+            verbosity+            distDirLayout+            tarball+            (packageId pkg)+            (elabPkgDescriptionOverride pkg)+            (distBuildDirectory distDirLayout (elabDistDirParams sharedConfig pkg))+  where+    fetched repoctx loc = do+      loc' <- fetchPackage verbosity repoctx loc+      return $ case loc' of+        RemoteTarballPackage _ tarball -> Just tarball+        RepoTarballPackage _ _ tarball -> Just tarball+        _ -> Nothing++-- | Unpack a package's tarball to 'distUnpackedSrcDirectory', unless that's+-- already been done. If the index has a newer revision of the @.cabal@ file+-- than the tarball, that's the one used.+unpackTarball+  :: Verbosity+  -> DistDirLayout+  -> FilePath+  -> PackageId+  -> Maybe LBS.ByteString+  -> FilePath+  -- ^ The package's build directory+  -> IO ()+unpackTarball verbosity distDirLayout tarball pkgid cabalFileOverride buildDir = do+  let srcdir = distUnpackedSrcDirectory distDirLayout pkgid+      srcrootdir = distUnpackedSrcRootDirectory distDirLayout+  exists <- doesDirectoryExist srcdir+  unless exists $ do+    createDirectoryIfMissingVerbose verbosity True srcrootdir+    info verbosity $ "Extracting " ++ tarball ++ " to " ++ srcrootdir ++ "..."+    Tar.extractTarGzFile srcrootdir (prettyShow pkgid) tarball++    let cabalFile = srcdir </> prettyShow (packageName pkgid) <.> "cabal"+    cabalFileExists <- doesFileExist cabalFile+    unless cabalFileExists $+      die' verbosity $+        "The package description file " ++ cabalFile ++ " was not found in " ++ tarball+    for_ cabalFileOverride $ \text -> do+      info verbosity $ "Updating " ++ cabalFile ++ " with the latest revision from the index."+      writeFileAtomic cabalFile text++    -- Some packages ship pre-processed files in a @dist@ directory inside the+    -- tarball: move it to where the build directory is.+    let shippedDist = srcdir </> "dist"+    shippedDistExists <- doesDirectoryExist shippedDist+    when shippedDistExists $ renameDirectory shippedDist buildDir
+ src/Distribution/Client/Buck2/Write.hs view
@@ -0,0 +1,239 @@+-- | Writes the generated files to support building with buck2+module Distribution.Client.Buck2.Write+  ( writeAllPackages+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import System.Directory (createDirectoryIfMissing, doesFileExist)+import System.FilePath (makeRelative, takeDirectory, takeFileName, (</>))++import qualified Data.Map as Map+import qualified Data.Set as Set++import qualified Distribution.ModuleName as ModuleName+import Distribution.Package (packageName)+import Distribution.PackageDescription+  ( Library (exposedModules, reexportedModules)+  , PackageDescription+  , library+  )+import Distribution.Simple.InstallDirs (PathTemplate)+import Distribution.Types.ComponentName (ComponentName)+import Distribution.Types.LocalBuildInfo (LocalBuildInfo)+import Distribution.Types.ModuleReexport+  ( ModuleReexport (moduleReexportOriginalName, moduleReexportOriginalPackage)+  )+import Distribution.Types.PackageName (PackageName)++import Distribution.Simple.Utils (notice, ordNub, warn)++import Distribution.Client.Buck2.Generate+import Distribution.Client.Buck2.Spec+import Distribution.Client.Buck2.Starlark++-- | Writes the following files for each package:+--+--   * @BUCK.cabal.bzl@ is fully regenerated on every run (it's marked+--     @\@generated@ and never hand-edited) and holds the package's build+--     spec, with a single @generated_targets()@ macro that creates the+--     rules from it. It would be nicer to put this under @cabal-buck2@+--     with the other generated files, but unfortunately buck2 makes+--     that hard by not allowing relative @load()@ declarations.+--   * @BUCK@ is created only if it doesn't already exist, as a two-line+--     file that loads and calls that macro. This is the file a user is+--     free to hand-edit - to add extra targets or customise the+--     target generation.+--   * @cabal-buck2\/autogen@ contains the autogenerated files needed+--     to compile the package, such as @cabal_macros.h@ and @Paths_<pkg>.hs@.+--   * @cabal-buck2\/autogen\/BUCK@ (see 'writeAutogenBuck') has+--     @export_file()@ rules for the autogen files, so they can be+--     easily referenced from anywhere else.+writeAllPackages :: Verbosity -> FilePath -> Map (PackageName, ComponentName) LocalBuildInfo -> Set String -> Map (PackageName, ComponentName) [PathTemplate] -> [(FilePath, PackageDescription)] -> IO ()+writeAllPackages verbosity projectRoot componentLBIs externalBuildTools projectTestOptions pkgs = do+  traverse_ (writeOnePackage verbosity localIndex projectRoot componentLBIs externalBuildTools projectTestOptions) pkgs+  where+    localIndex :: LocalPackageIndex+    localIndex =+      Map.fromList+        [ (packageName pkgDesc, (rootRelativeDir projectRoot pkgDir, reexportOrigins pkgDesc))+        | (pkgDir, pkgDesc) <- pkgs+        ]+    -- Every module exposed by any local package's main library, to+    -- resolve a `reexported-modules:` entry that names only the bare+    -- module, not an explicit `origin-package:Module` - Cabal itself+    -- resolves that form by searching the reexporting package's own+    -- build-depends for whichever one actually defines it, which for a+    -- \*local* origin this index can do too (an external origin doesn't+    -- need this: its real .conf file already declares the reexport+    -- directly to ghc-pkg).+    moduleOwners :: Map.Map ModuleName.ModuleName PackageName+    moduleOwners =+      Map.fromList+        [ (m, packageName pkgDesc)+        | (_, pkgDesc) <- pkgs+        , Just lib <- [library pkgDesc]+        , m <- exposedModules lib+        ]+    reexportOrigins pkgDesc =+      ordNub+        [ pn+        | Just lib <- [library pkgDesc]+        , reexport <- reexportedModules lib+        , Just pn <- [originPackage reexport]+        , pn /= packageName pkgDesc+        ]+    originPackage reexport = case moduleReexportOriginalPackage reexport of+      Just pn -> Just pn+      Nothing -> Map.lookup (moduleReexportOriginalName reexport) moduleOwners++rootRelativeDir :: FilePath -> FilePath -> FilePath+rootRelativeDir projectRoot pkgDir = case makeRelative projectRoot pkgDir of+  "" -> "."+  rel -> rel++writeOnePackage :: Verbosity -> LocalPackageIndex -> FilePath -> Map (PackageName, ComponentName) LocalBuildInfo -> Set String -> Map (PackageName, ComponentName) [PathTemplate] -> (FilePath, PackageDescription) -> IO ()+writeOnePackage verbosity localIndex projectRoot componentLBIs externalBuildTools projectTestOptions (pkgDir, pkgDesc) = do+  sources <- Set.fromList <$> filterM (doesFileExist . (pkgDir </>)) (sourceCandidates pkgDesc)+  let (mtargets, warnings) = generatePackageTargets localIndex (rootRelativeDir projectRoot pkgDir) componentLBIs externalBuildTools projectTestOptions sources pkgDesc+      pkgName = packageName pkgDesc+  traverse_ (warn verbosity) warnings+  case mtargets of+    Nothing -> warn verbosity $ "cabal buck2: no buck2 targets generated for package " ++ show pkgName+    Just targets -> do+      let bzlPath = pkgDir </> "BUCK.cabal.bzl"+          buckPath = pkgDir </> "BUCK"+      writeFile bzlPath (renderGeneratedBzl pkgName (ptSpec targets))+      buckExists <- doesFileExist buckPath+      unless buckExists $ writeFile buckPath renderBuckWrapper+      writeAutogenBuck pkgDir pkgName targets+      notice verbosity $+        "cabal buck2: generated "+          ++ (rootRelativeDir projectRoot pkgDir </> "BUCK.cabal.bzl")+          ++ " ("+          ++ show (ptComponentCount targets)+          ++ " component(s))"+          ++ (if buckExists then "" else ", created " ++ (rootRelativeDir projectRoot pkgDir </> "BUCK"))++-- | Generate an @export_file()@ rule for each autogen file, so that+-- the files can be easily referenced from somewhere else, including+-- subdirs.+writeAutogenBuck :: FilePath -> PackageName -> PackageTargets -> IO ()+writeAutogenBuck pkgDir pkgName targets+  | null files = return ()+  | otherwise = do+      createDirectoryIfMissing True autogenDir+      for_ files $ \f -> do+        createDirectoryIfMissing True (takeDirectory (autogenDir </> autogenPath f))+        writeFile (autogenDir </> autogenPath f) (autogenContents f)+      writeFile (autogenDir </> "BUCK") (renderFile header [] exportCalls)+  where+    files = ptAutogenFiles targets+    autogenDir = pkgDir </> "cabal-buck2" </> "autogen"+    header =+      "@generated by `cabal buck2` from "+        ++ prettyShow pkgName+        ++ ".cabal - do not edit by hand.\nRe-run `cabal buck2` after editing the .cabal file to refresh this file."+    exportCalls =+      [ call+        "export_file"+        [ ("name", str (autogenName f))+        , ("src", str (autogenPath f))+        , ("out", str (takeFileName (autogenPath f)))+        , -- For @Paths_<pkg>.hs@ it's important the exported file has the+          -- same name, because the buck2 Haskell rules derive the+          -- module name from it.+          ("visibility", strList ["PUBLIC"])+        ]+      | f <- files+      ]++renderGeneratedBzl :: PackageName -> BuildSpec -> String+renderGeneratedBzl pkgName spec =+  unlines+    [ "# @generated by `cabal buck2` from " ++ prettyShow pkgName ++ ".cabal - do not edit by hand."+    , "# Re-run `cabal buck2` after editing the .cabal file to refresh this file."+    , ""+    ]+    ++ renderLoad "//buck2:cabal.bzl" ["cabal_targets"]+    ++ "\n"+    ++ renderBinding "local_build_spec" (specValue spec)+    ++ "\n"+    -- kwargs: customisation passed by the BUCK file (see cabal_targets()+    -- in buck2/cabal.bzl).+    ++ "def generated_targets(**kwargs):\n    cabal_targets(local_build_spec, **kwargs)\n"++-- | A build spec as the Starlark dict that buck2\/cabal.bzl reads (see that+-- file for the schema). Optional keys are left out when empty.+specValue :: BuildSpec -> Value+specValue spec =+  VDict $+    [ ("schema", VInt specSchemaVersion)+    , ("package", VDict [("name", str (specPackageName spec)), ("dir", str (specPackageDir spec))])+    ]+      ++ listField "ghc_options" (specGhcOptions spec)+      ++ [("components", VList (map componentValue (specComponents spec)))]++componentValue :: SpecComponent -> Value+componentValue c =+  VDict $+    [("kind", str (kindName (scKind c))), ("name", str (scName c))]+      ++ [("main_is", srcValue src) | Just src <- [scMainIs c]]+      ++ [("srcs", VDict [(m, srcValue src) | (m, src) <- scSrcs c]) | not (null (scSrcs c))]+      ++ listField "test_args" (scTestArgs c)+      ++ listField "ghc_options" (scGhcOptions c)+      ++ listField "cpp_options" (scCppOptions c)+      ++ [("language", str lang) | Just lang <- [scLanguage c]]+      ++ listField "extensions" (scExtensions c)+      ++ listField "extra_libraries" (scExtraLibraries c)+      ++ valuesField "deps" (map depValue (scDeps c))+      ++ valuesField "build_tools" (map buildToolValue (scBuildTools c))+      ++ listField "c_sources" (scCSources c)+      ++ listField "cxx_sources" (scCxxSources c)+      ++ listField "cxx_options" (scCxxOptions c)+      ++ listField "include_dirs" (scIncludeDirs c)+      ++ listField "pkgconfig" (scPkgconfig c)++srcValue :: Src -> Value+srcValue (SrcFile path) = str path+srcValue (SrcAutogen name) = VDict [("autogen", str name)]++depValue :: SpecDep -> Value+depValue d =+  VDict $+    [("package", str (depPackage d))]+      ++ [("library", str lib) | Just lib <- [depLibrary d]]+      ++ [("dir", str dir) | Just dir <- [depDir d]]++buildToolValue :: SpecBuildTool -> Value+buildToolValue (LocalTool exe dir) = VDict [("exe", str exe), ("dir", str dir)]+buildToolValue (ExternalTool exe) = VDict [("exe", str exe), ("external", VBool True)]++-- | A list-valued field, omitted if empty.+listField :: String -> [String] -> [(String, Value)]+listField _ [] = []+listField k xs = [(k, strList xs)]++valuesField :: String -> [Value] -> [(String, Value)]+valuesField _ [] = []+valuesField k xs = [(k, VList xs)]++-- | Content of @BUCK@ in a package's directory+renderBuckWrapper :: String+renderBuckWrapper =+  unlines+    [ "# Hand-maintained: add extra targets below, or stop calling"+    , "# generated_targets() to fully take over this package's BUCK rules."+    , "#"+    , "# generated_targets() can be customised (see cabal_targets() in"+    , "# buck2/cabal.bzl), e.g."+    , "#"+    , "#     generated_targets("+    , "#         defaults = {\"*\": {\"compiler_flags\": [\"-O2\"]}},"+    , "#         overrides = {\"my-test\": {\"test_args\": [\"--quick\"]}},"+    , "#     )"+    , "load(\":BUCK.cabal.bzl\", \"generated_targets\")"+    , ""+    , "generated_targets()"+    ]
+ src/Distribution/Client/CmdBuck2.hs view
@@ -0,0 +1,153 @@+-- | cabal-install CLI command: buck2+--+-- Sets up (or refreshes) a buck2 build for the current project, using the+-- prelude and support scripts checked out at @buck2\/@ (a checkout of+-- <https://github.com/simonmar/haskell-buck2>, see @buck2\/README.md@).+--+-- There are 6 main pieces to this, each with a small API:+--+--   1. "Distribution.Client.Buck2.BuildDependencies": Build every+--      dependency (never the local packages themselves), the same as+--      @cabal build all --only-dependencies@ would.+--+--   2. "Distribution.Client.Buck2.Setup": Create+--      @.buckconfig@\/@PACKAGE@ if they don't exist yet. These are+--      boilerplate copied from @buck2\/example@.+--+--   3. "Distribution.Client.Buck2.Prebuilt": Tell @buck2@ about all+--      the library and tool dependencies. These are all recorded+--      under @third-party\/haskell@.+--+--   4. "Distribution.Client.Buck2.Configure": Configure every+--      component of the local packages, to get the 'LocalBuildInfo'.+--+--   5. "Distribution.Client.Buck2.Generate": Generate buck2 targets+--      for each local component to be built. A pure function of+--      'PackageDescription', 'LocalBuildInfo' and a few other things.+--+--   6. "Distribution.Client.Buck2.Write": Write the generated buck2+--      targets for each package to @BUCK.cabal.bzl@, and the autogen+--      files into @cabal-buck2/autogen@ in each package's directory.+module Distribution.Client.CmdBuck2+  ( buck2Command+  , buck2Action+  ) where++import Distribution.Client.Compat.Prelude+import Prelude ()++import Distribution.Client.DistDirLayout (DistDirLayout (distProjectRootDirectory))+import Distribution.Client.NixStyleOptions+  ( NixStyleFlags (..)+  , cfgVerbosity+  , defaultNixStyleFlags+  , nixStyleOptions+  )+import Distribution.Client.ProjectOrchestration+import Distribution.Client.ScriptUtils+  ( AcceptNoTargets (..)+  , TargetContext (..)+  , updateContextAndWriteProjectFile+  , withContextAndSelectors+  )+import Distribution.Client.Setup+  ( GlobalFlags+  , InstallFlags (installOnlyDeps)+  )++import Distribution.Simple.Command (CommandUI (..), usageAlternatives)+import Distribution.Simple.Flag (toFlag)+import qualified Distribution.Simple.PackageIndex as PackageIndex+import Distribution.Simple.Utils (die', notice)+import Distribution.Verbosity (normal)++import Distribution.Client.Buck2.BuildDependencies (buildDependencies)+import Distribution.Client.Buck2.Configure (configureComponents)+import Distribution.Client.Buck2.LocalPackages+  ( builtLocalPackages+  , projectTestOptions+  , wantedBuildTools+  )+import Distribution.Client.Buck2.Prebuilt (generatePrebuilt)+import Distribution.Client.Buck2.Setup+  ( checkBuck2Prelude+  , ensureBuckconfigAndPackage+  )+import Distribution.Client.Buck2.Write (writeAllPackages)++-- | The @cabal buck2@ CLI command+buck2Command :: CommandUI (NixStyleFlags ())+buck2Command =+  CommandUI+    { commandName = "buck2"+    , commandSynopsis = "Set up (or refresh) a buck2 build for this project."+    , commandUsage = usageAlternatives "buck2" ["[FLAGS]"]+    , commandDescription = Just $ \_ ->+        "Builds every dependency of the project (as `cabal build all "+          ++ "--only-dependencies` would), then generates the buck2 build "+          ++ "files (.buckconfig, PACKAGE, third-party/haskell, and a "+          ++ "BUCK.cabal.bzl for each local package) needed to build the "+          ++ "project with buck2 instead of cabal. Requires a checkout of "+          ++ "https://github.com/simonmar/haskell-buck2 at ./buck2. See "+          ++ "buck2/README.md for details.\n\n"+          ++ "Flags that would normally be passed to `cabal build`/`cabal "+          ++ "configure` (-f, --enable-profiling, --enable-tests, etc.) are "+          ++ "honoured here too, and apply to the dependency build."+    , commandNotes = Nothing+    , commandDefaultFlags = defaultNixStyleFlags ()+    , commandOptions = nixStyleOptions (const [])+    }++-- | Implement @cabal buck2@+buck2Action :: NixStyleFlags () -> [String] -> GlobalFlags -> IO ()+buck2Action flags extraArgs globalFlags = do+  unless (null extraArgs) $+    die' verbosity ("'cabal buck2' doesn't take any extra arguments: " ++ unwords extraArgs)++  withContextAndSelectors verbosity RejectNoTargets Nothing depsFlags ["all"] globalFlags BuildCommand $+    \targetCtx ctx targetSelectors -> do+      baseCtx <- case targetCtx of+        ProjectContext -> return ctx+        GlobalContext -> return ctx+        ScriptContext path exemeta -> updateContextAndWriteProjectFile ctx path exemeta++      let projectRoot = distProjectRootDirectory (distDirLayout baseCtx)+      checkBuck2Prelude verbosity projectRoot++      buildCtx <- buildDependencies verbosity baseCtx targetSelectors++      ensureBuckconfigAndPackage verbosity projectRoot++      localPkgs <- builtLocalPackages verbosity (distDirLayout baseCtx) (elaboratedPlanOriginal buildCtx)++      (externalBuildTools, resolvedDeps) <-+        generatePrebuilt+          verbosity+          projectRoot+          (cabalDirLayout baseCtx)+          (elaboratedShared buildCtx)+          (elaboratedPlanToExecute buildCtx)+          (wantedBuildTools localPkgs)++      -- 'generatePrebuilt' already found and parsed every real @.conf@+      -- file of the resolved dependency closure.+      componentLBIs <- configureComponents verbosity baseCtx buildCtx (PackageIndex.fromList resolvedDeps)++      writeAllPackages+        verbosity+        projectRoot+        componentLBIs+        externalBuildTools+        (projectTestOptions (elaboratedPlanOriginal buildCtx))+        localPkgs++      notice verbosity $+        unlines+          [ "cabal buck2: done. You can now:"+          , "    buck2 build //...          # build everything"+          , "    buck2 test //...           # test everything"+          , "    buck2 build //... -m opt   # build everything in opt mode"+          ]+  where+    verbosity = cfgVerbosity normal flags+    depsFlags = flags{installFlags = (installFlags flags){installOnlyDeps = toFlag True}}
+ test/Main.hs view
@@ -0,0 +1,261 @@+-- | Runs @cabal-buck2@ on the fixture projects in @test/fixtures@ and checks+-- the files it generates. None of these needs a buck2 binary, or a buck2+-- prelude: the fixtures' @buck2@ directory only has to exist.+module Main (main) where++import Control.Exception (SomeException, bracket, displayException, try)+import Control.Monad (forM_, unless, when)+import Data.List (isInfixOf)+import System.Directory+  ( copyFile+  , createDirectoryIfMissing+  , doesDirectoryExist+  , findExecutable+  , getTemporaryDirectory+  , listDirectory+  , removeDirectoryRecursive+  )+import System.Environment (getEnvironment, lookupEnv)+import System.Exit (ExitCode (..), exitFailure)+import System.FilePath ((</>))+import System.IO (hPutStrLn, stderr)+import System.Process (CreateProcess (..), proc, readCreateProcessWithExitCode, getCurrentPid)++main :: IO ()+main = do+  exe <- maybe (fail "cabal-buck2 not found on PATH") return =<< findExecutable "cabal-buck2"+  results <- mapM (runTest exe) tests+  let failures = [name | (name, False) <- zip (map fst tests) results]+  unless (null failures) $ do+    hPutStrLn stderr $ "FAILED: " ++ unwords failures+    exitFailure++tests :: [(String, Project -> IO ())]+tests =+  [ ("basic", basic)+  , ("disabled-stanzas", disabledStanzas)+  , ("missing-module", missingModule)+  ]++-- | A copy of a fixture project, and how to run @cabal-buck2@ in it.+data Project = Project+  { projectDir :: FilePath+  , runBuck2 :: [String] -> IO (ExitCode, String)+  -- ^ The exit code and the output (stdout and stderr).+  }++runTest :: FilePath -> (String, Project -> IO ()) -> IO Bool+runTest exe (name, test) = do+  tmp <- getTemporaryDirectory+  pid <- getCurrentPid+  let dir = tmp </> ("cabal-buck2-test-" ++ show pid ++ "-" ++ name)+  bracket (setup dir) (const (cleanup dir)) $ \project -> do+    r <- try (test project)+    case r of+      Right () -> putStrLn ("PASS " ++ name) >> return True+      Left e -> do+        hPutStrLn stderr ("FAIL " ++ name ++ ": " ++ displayException (e :: SomeException))+        return False+  where+    -- The fixture is named after the test, except for tests that reuse one.+    fixture = case name of+      "disabled-stanzas" -> "basic"+      _ -> name+    setup dir = do+      cleanup dir+      copyTree ("test" </> "fixtures" </> fixture) (dir </> "project")+      env <- getEnvironment+      -- The compiler to use: $HC if it's set (as haskell-ci does), otherwise+      -- whatever `ghc` is on the PATH.+      compilerArgs <- maybe [] (\hc -> ["-w", hc]) <$> lookupEnv "HC"+      -- A private cabal directory and config, so that the user's store and+      -- config (which $CABAL_DIR and $CABAL_CONFIG may point to, as in+      -- haskell-ci) are neither used nor changed.+      let cabalDir = dir </> "cabal-dir"+          cabalEnv =+            [("CABAL_DIR", cabalDir), ("CABAL_CONFIG", cabalDir </> "config")]+              ++ [kv | kv@(k, _) <- env, k `notElem` ["CABAL_DIR", "CABAL_CONFIG"]]+          run args = do+            (code, out, err) <-+              readCreateProcessWithExitCode+                (proc exe (compilerArgs ++ args)){cwd = Just (dir </> "project"), env = Just cabalEnv}+                ""+            return (code, out ++ err)+      createDirectoryIfMissing True cabalDir+      writeFile (cabalDir </> "config") ""+      return Project{projectDir = dir </> "project", runBuck2 = run}+    cleanup dir = do+      exists <- doesDirectoryExist dir+      when exists $ removeDirectoryRecursive dir++copyTree :: FilePath -> FilePath -> IO ()+copyTree from to = do+  createDirectoryIfMissing True to+  names <- listDirectory from+  forM_ names $ \n -> do+    isDir <- doesDirectoryExist (from </> n)+    if isDir then copyTree (from </> n) (to </> n) else copyFile (from </> n) (to </> n)++-- * Assertions++-- | Run @cabal-buck2@ and check that it succeeded.+buck2 :: Project -> [String] -> IO String+buck2 project args = do+  (code, out) <- runBuck2 project args+  when (code /= ExitSuccess) $ failure ("cabal-buck2 " ++ unwords args ++ " failed:\n" ++ out)+  return out++failure :: String -> IO a+failure = ioError . userError++readIn :: Project -> FilePath -> IO String+readIn project path = do+  s <- readFile (projectDir project </> path)+  length s `seq` return s++assertContains :: String -> String -> String -> IO ()+assertContains what needle haystack =+  unless (needle `isInfixOf` haystack) $+    failure (what ++ ": expected to contain " ++ show needle ++ ", but it is:\n" ++ haystack)++assertNotContains :: String -> String -> String -> IO ()+assertNotContains what needle haystack =+  when (needle `isInfixOf` haystack) $+    failure (what ++ ": expected not to contain " ++ show needle ++ ", but it is:\n" ++ haystack)++-- * Tests++-- | The main mapping rules, end to end: a plain library (lib-pkg), a second+-- package (exe-pkg) whose library depends on it, an executable with+-- @c-sources@, an exitcode-stdio-1.0 test-suite and a benchmark (both only+-- generated because tests and benchmarks are enabled), a @detailed-0.9@+-- test-suite (which gets a generated stub @Main@), and a manual flag gating+-- @cpp-options@.+basic :: Project -> IO ()+basic project = do+  _ <- buck2 project ["--enable-tests", "--enable-benchmarks", "-f+loud"]++  -- The generated file is a build spec - a plain dict describing each+  -- component as Cabal sees it - interpreted by buck2/cabal.bzl, which+  -- decides which rules, labels and flags that becomes.+  libBzl <- readIn project ("lib-pkg" </> "BUCK.cabal.bzl")+  let lib = assertContains "lib-pkg/BUCK.cabal.bzl"+      noLib = assertNotContains "lib-pkg/BUCK.cabal.bzl"+  lib "local_build_spec" libBzl+  lib "'kind': 'library'" libBzl+  lib "'name': 'lib-pkg'" libBzl+  noLib "haskell_library(" libBzl+  -- Project-level options and test options are only recorded where they+  -- apply.+  noLib "'ghc_options'" libBzl+  noLib "'test_args'" libBzl++  exeBzl <- readIn project ("exe-pkg" </> "BUCK.cabal.bzl")+  let exe = assertContains "exe-pkg/BUCK.cabal.bzl"+  forM_ ["library", "executable", "test-suite", "benchmark"] $ \kind ->+    exe ("'kind': '" ++ kind ++ "'") exeBzl+  exe "'name': 'exe-pkg-bench'" exeBzl++  -- A dependency on another local package carries that package's directory+  -- (from which the target label is made).+  exe "'package': 'lib-pkg'" exeBzl+  exe "'dir': 'lib-pkg'" exeBzl++  -- C sources and include directories are recorded as written in the .cabal+  -- file; cabal.bzl turns them into a cxx_library().+  exe "'cbits/helper.c'" exeBzl+  exe "'include_dirs'" exeBzl+  -- The manual flag's cpp-options.+  exe "'-DLOUD'" exeBzl++  -- `ghc-options:` and `test-options:` from cabal.project (not the .cabal+  -- file) reach the spec: the former once per package, the latter as+  -- `test_args` with template variables expanded per test-suite. The+  -- `-hide-all-packages` that cabal-install always adds (a workaround for+  -- custom Setup.hs scripts) is deliberately not copied over.+  exe "'ghc_options'" exeBzl+  exe "'-fno-ignore-asserts'" exeBzl+  assertNotContains "exe-pkg/BUCK.cabal.bzl" "-hide-all-packages" exeBzl+  exe "'test_args'" exeBzl+  exe "'--opt-one'" exeBzl+  exe "'--opt-two=exe-pkg-test'" exeBzl+  exe "'exe-pkg-detailed-test'" exeBzl++  -- Paths_<pkg>.hs and the detailed-0.9 stub Main both live under+  -- cabal-buck2/autogen/, which has its own BUCK file (see below): the spec+  -- names them, and cabal.bzl refers to them by that file's export_file()+  -- target.+  exe "'Paths_exe_pkg': {" exeBzl+  exe "'autogen': 'Paths_exe_pkg'" exeBzl+  exe "'autogen': 'exe-pkg-detailed-test-stub-main'" exeBzl++  -- The hand-editable BUCK wrapper is created (only once) and loads the+  -- generated file, whose entry point passes customisation through to+  -- cabal.bzl's cabal_targets().+  wrapper <- readIn project ("exe-pkg" </> "BUCK")+  assertContains "exe-pkg/BUCK" "generated_targets" wrapper+  exe "def generated_targets(**kwargs):" exeBzl++  -- The detailed-0.9 test-suite's stub Main is our own generated driver+  -- (not Cabal's stdin-driven one), importing the user's named test-module+  -- directly.+  let autogen = "exe-pkg" </> "cabal-buck2" </> "autogen"+  stub <- readIn project (autogen </> "exe-pkg-detailed-test" </> "Main.hs")+  assertContains "stub Main" "import qualified DetailedTests as CabalBuck2TestModule" stub++  -- cabal_macros.h and Paths_<pkg>.hs come from Cabal's own generators, not+  -- hand-rolled stand-ins.+  macros <- readIn project (autogen </> "exe-pkg" </> "cabal_macros.h")+  assertContains "cabal_macros.h" "CURRENT_PACKAGE_KEY" macros+  paths <- readIn project (autogen </> "Paths_exe_pkg.hs")+  assertContains "Paths_exe_pkg.hs" "version =" paths++  -- cabal-buck2/autogen/BUCK exports every autogen file as a real target+  -- via export_file(): both what makes cabal_component's $(location ...)+  -- reference a buck2-tracked dependency, and what lets a hand-written BUCK+  -- file elsewhere refer to e.g. Paths_<pkg>.+  autogenBuck <- readIn project (autogen </> "BUCK")+  let ab = assertContains "cabal-buck2/autogen/BUCK"+  ab "name = 'exe-pkg-cabal-macros'" autogenBuck+  ab "name = 'Paths_exe_pkg'" autogenBuck+  ab "name = 'exe-pkg-detailed-test-stub-main'" autogenBuck+  -- Each export_file() must set `out` to the real file's basename: without+  -- it, `out` defaults to the rule's name, the artifact loses its extension,+  -- and buck2 silently stops treating it as a Haskell source.+  ab "out = 'Paths_exe_pkg.hs'" autogenBuck+  ab "out = 'cabal_macros.h'" autogenBuck+  ab "out = 'Main.hs'" autogenBuck++-- | A plain run, without @--enable-tests@ or @--enable-benchmarks@, must+-- succeed even though the package has test-suites and a benchmark. The+-- benchmark depends on @stm@, which nothing else in the fixture uses: since+-- its stanza isn't enabled, @stm@ is (correctly) absent from the dependency+-- plan that the installed package index is built from, so configuring the+-- benchmark anyway would fail with "the given installed package instance+-- does not exist". The disabled components just get no rule.+disabledStanzas :: Project -> IO ()+disabledStanzas project = do+  _ <- buck2 project []+  exeBzl <- readIn project ("exe-pkg" </> "BUCK.cabal.bzl")+  let exe = "exe-pkg/BUCK.cabal.bzl"+  assertContains exe "'kind': 'library'" exeBzl+  assertContains exe "'kind': 'executable'" exeBzl+  assertNotContains exe "'exe-pkg-bench'" exeBzl+  assertNotContains exe "'kind': 'test-suite'" exeBzl+  assertNotContains exe "'kind': 'benchmark'" exeBzl++-- | A component whose sources can't all be found is skipped with a warning+-- saying why (a rule that names a missing file would take down the whole+-- buck2 build), and so is every component of the same package that depends+-- on it. Everything else is still generated.+missingModule :: Project -> IO ()+missingModule project = do+  out <- buck2 project []+  assertContains "output" "for module Absent" out+  assertContains "output" "skipping library broken-pkg" out+  assertContains "output" "skipping executable uses-lib" out++  bzl <- readIn project ("broken-pkg" </> "BUCK.cabal.bzl")+  assertContains "broken-pkg/BUCK.cabal.bzl" "'name': 'standalone'" bzl+  assertNotContains "broken-pkg/BUCK.cabal.bzl" "'name': 'uses-lib'" bzl+  assertNotContains "broken-pkg/BUCK.cabal.bzl" "'kind': 'library'" bzl
+ test/fixtures/basic/.buckconfig view
@@ -0,0 +1,4 @@+# Placeholder: this test only checks `cabal buck2`'s generated files, it+# never actually invokes a real buck2 binary, so this doesn't need to be+# a functional buck2 config - it only needs to exist so that+# `ensureBuckconfigAndPackage` sees it's already there and leaves it alone.
+ test/fixtures/basic/PACKAGE view
@@ -0,0 +1,1 @@+# Placeholder - see .buckconfig.
+ test/fixtures/basic/buck2/.gitkeep view
+ test/fixtures/basic/cabal.project view
@@ -0,0 +1,7 @@+packages: lib-pkg exe-pkg++package exe-pkg+  ghc-options: -fno-ignore-asserts+  test-options: --opt-one --opt-two=$test-suite++active-repositories: :none
+ test/fixtures/basic/exe-pkg/app/Main.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Greet (greet)++main :: IO ()+main = putStrLn greet
+ test/fixtures/basic/exe-pkg/bench/Bench.hs view
@@ -0,0 +1,4 @@+module Main (main) where++main :: IO ()+main = putStrLn "ok"
+ test/fixtures/basic/exe-pkg/cbits/helper.c view
@@ -0,0 +1,3 @@+#include <helper.h>++int exe_pkg_helper(void) { return 42; }
+ test/fixtures/basic/exe-pkg/cbits/helper.h view
@@ -0,0 +1,1 @@+int exe_pkg_helper(void);
+ test/fixtures/basic/exe-pkg/detailed-test/DetailedTests.hs view
@@ -0,0 +1,16 @@+module DetailedTests (tests) where++import Distribution.TestSuite++tests :: IO [Test]+tests =+  return+    [ Test+        TestInstance+          { run = return (Finished Pass)+          , name = "always-passes"+          , tags = []+          , options = []+          , setOption = \_ _ -> Left "no options"+          }+    ]
+ test/fixtures/basic/exe-pkg/exe-pkg.cabal view
@@ -0,0 +1,51 @@+cabal-version:      2.4+name:                exe-pkg+version:             0.1.0.0+build-type:          Simple++flag loud+  description: Enable extra-loud output+  default:     False+  manual:      True++library+  exposed-modules:   Greet+  other-modules:     Paths_exe_pkg+  autogen-modules:   Paths_exe_pkg+  hs-source-dirs:    src+  build-depends:     base, lib-pkg+  default-language:  Haskell2010+  default-extensions: CPP+  if flag(loud)+    cpp-options:     -DLOUD++executable exe-pkg-exe+  main-is:            Main.hs+  hs-source-dirs:     app+  c-sources:          cbits/helper.c+  include-dirs:       cbits+  build-depends:      base, exe-pkg+  default-language:   Haskell2010++test-suite exe-pkg-test+  type:               exitcode-stdio-1.0+  main-is:            Spec.hs+  hs-source-dirs:     test+  build-depends:      base, exe-pkg+  default-language:   Haskell2010++benchmark exe-pkg-bench+  type:               exitcode-stdio-1.0+  main-is:            Bench.hs+  hs-source-dirs:     bench+  -- stm: used by nothing else in this fixture - see+  -- disabled-stanzas.test.hs+  build-depends:      base, exe-pkg, stm+  default-language:   Haskell2010++test-suite exe-pkg-detailed-test+  type:               detailed-0.9+  test-module:        DetailedTests+  hs-source-dirs:     detailed-test+  build-depends:      base, Cabal, exe-pkg+  default-language:   Haskell2010
+ test/fixtures/basic/exe-pkg/src/Greet.hs view
@@ -0,0 +1,10 @@+module Greet (greet) where++import MyLib (myLib)++greet :: String+#ifdef LOUD+greet = myLib ++ "!!!"+#else+greet = myLib+#endif
+ test/fixtures/basic/exe-pkg/test/Spec.hs view
@@ -0,0 +1,4 @@+module Main (main) where++main :: IO ()+main = putStrLn "ok"
+ test/fixtures/basic/lib-pkg/lib-pkg.cabal view
@@ -0,0 +1,10 @@+cabal-version:      2.4+name:                lib-pkg+version:             0.1.0.0+build-type:          Simple++library+  exposed-modules:   MyLib+  hs-source-dirs:    src+  build-depends:     base+  default-language:  Haskell2010
+ test/fixtures/basic/lib-pkg/src/MyLib.hs view
@@ -0,0 +1,4 @@+module MyLib (myLib) where++myLib :: String+myLib = "lib-pkg"
+ test/fixtures/missing-module/.buckconfig view
@@ -0,0 +1,4 @@+# Placeholder: this test only checks `cabal buck2`'s generated files, it+# never actually invokes a real buck2 binary, so this doesn't need to be+# a functional buck2 config - it only needs to exist so that+# `ensureBuckconfigAndPackage` sees it's already there and leaves it alone.
+ test/fixtures/missing-module/PACKAGE view
@@ -0,0 +1,1 @@+# Placeholder - see .buckconfig.
+ test/fixtures/missing-module/broken-pkg/app/Standalone.hs view
@@ -0,0 +1,3 @@+module Main where+main :: IO ()+main = return ()
+ test/fixtures/missing-module/broken-pkg/app/UsesLib.hs view
@@ -0,0 +1,4 @@+module Main where+import Present ()+main :: IO ()+main = return ()
+ test/fixtures/missing-module/broken-pkg/broken-pkg.cabal view
@@ -0,0 +1,22 @@+cabal-version:      2.4+name:               broken-pkg+version:            0.1.0.0+build-type:         Simple++library+  exposed-modules:  Present, Absent+  hs-source-dirs:   src+  build-depends:    base+  default-language: Haskell2010++executable uses-lib+  main-is:          UsesLib.hs+  hs-source-dirs:   app+  build-depends:    base, broken-pkg+  default-language: Haskell2010++executable standalone+  main-is:          Standalone.hs+  hs-source-dirs:   app+  build-depends:    base+  default-language: Haskell2010
+ test/fixtures/missing-module/broken-pkg/src/Present.hs view
@@ -0,0 +1,1 @@+module Present where
+ test/fixtures/missing-module/buck2/.gitkeep view
+ test/fixtures/missing-module/cabal.project view
@@ -0,0 +1,3 @@+packages: broken-pkg++active-repositories: :none