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 +375/−0
- app/Main.hs +29/−0
- cabal-buck2.cabal +96/−0
- changelog.md +3/−0
- src/Distribution/Client/Buck2/BuildDependencies.hs +73/−0
- src/Distribution/Client/Buck2/Configure.hs +216/−0
- src/Distribution/Client/Buck2/Generate.hs +658/−0
- src/Distribution/Client/Buck2/LocalPackages.hs +129/−0
- src/Distribution/Client/Buck2/Prebuilt.hs +558/−0
- src/Distribution/Client/Buck2/Schedule.hs +73/−0
- src/Distribution/Client/Buck2/Setup.hs +45/−0
- src/Distribution/Client/Buck2/Spec.hs +94/−0
- src/Distribution/Client/Buck2/Starlark.hs +116/−0
- src/Distribution/Client/Buck2/Unpack.hs +101/−0
- src/Distribution/Client/Buck2/Write.hs +239/−0
- src/Distribution/Client/CmdBuck2.hs +153/−0
- test/Main.hs +261/−0
- test/fixtures/basic/.buckconfig +4/−0
- test/fixtures/basic/PACKAGE +1/−0
- test/fixtures/basic/buck2/.gitkeep +0/−0
- test/fixtures/basic/cabal.project +7/−0
- test/fixtures/basic/exe-pkg/app/Main.hs +6/−0
- test/fixtures/basic/exe-pkg/bench/Bench.hs +4/−0
- test/fixtures/basic/exe-pkg/cbits/helper.c +3/−0
- test/fixtures/basic/exe-pkg/cbits/helper.h +1/−0
- test/fixtures/basic/exe-pkg/detailed-test/DetailedTests.hs +16/−0
- test/fixtures/basic/exe-pkg/exe-pkg.cabal +51/−0
- test/fixtures/basic/exe-pkg/src/Greet.hs +10/−0
- test/fixtures/basic/exe-pkg/test/Spec.hs +4/−0
- test/fixtures/basic/lib-pkg/lib-pkg.cabal +10/−0
- test/fixtures/basic/lib-pkg/src/MyLib.hs +4/−0
- test/fixtures/missing-module/.buckconfig +4/−0
- test/fixtures/missing-module/PACKAGE +1/−0
- test/fixtures/missing-module/broken-pkg/app/Standalone.hs +3/−0
- test/fixtures/missing-module/broken-pkg/app/UsesLib.hs +4/−0
- test/fixtures/missing-module/broken-pkg/broken-pkg.cabal +22/−0
- test/fixtures/missing-module/broken-pkg/src/Present.hs +1/−0
- test/fixtures/missing-module/buck2/.gitkeep +0/−0
- test/fixtures/missing-module/cabal.project +3/−0
+ 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.++++**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