packages feed

cabal-buck2-0.2.0.0: src/Distribution/Client/CmdBuck2.hs

-- | 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.ProjectConfig (ProjectConfig (..), ProjectConfigShared (..))
import Distribution.Client.ProjectOrchestration
import Distribution.Client.Targets (UserConstraint (..), UserConstraintScope (..))
import Distribution.Solver.Types.ConstraintSource (ConstraintSource (ConstraintSourceUnknown))
import Distribution.Solver.Types.PackageConstraint (PackageProperty (PackagePropertyVersion))
import Distribution.Types.PackageName (mkPackageName)
import Distribution.Version (alterVersion, earlierVersion, intersectVersionRanges, orLaterVersion)
import Distribution.Client.ScriptUtils
  ( AcceptNoTargets (..)
  , TargetContext (..)
  , updateContextAndWriteProjectFile
  , withContextAndSelectors
  )
import Distribution.Client.Setup
  ( GlobalFlags
  , InstallFlags (installKeepGoing, installOnlyDeps)
  )

import Distribution.Simple.Command (CommandUI (..), usageAlternatives)
import Distribution.Simple.Flag (fromFlagOrDefault, toFlag)
import qualified Distribution.Simple.PackageIndex as PackageIndex
import Distribution.Simple.Utils (cabalVersion, die', notice)
import Distribution.Verbosity (normal)

import Distribution.Client.Buck2.BuildDependencies (buildDependencies)
import Distribution.Client.Buck2.Cache (cacheSetting, configureCache)
import Distribution.Client.Buck2.Configure (configureComponents)
import Distribution.Client.Buck2.Flags (Buck2Flags, buck2FlagOptions, defaultBuck2Flags, dependencyMode)
import Distribution.Client.Buck2.LocalPackages
  ( builtLocalPackages
  , localBuildTools
  , prebuiltUnits
  , localToolTargets
  , 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 Buck2Flags)
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.\n\n"
          ++ "A component that can't be built with buck2 is an error, unless "
          ++ "--keep-going is given: then it is skipped with a warning."
    , commandNotes = Nothing
    , commandDefaultFlags = defaultNixStyleFlags defaultBuck2Flags
    , commandOptions = nixStyleOptions buck2FlagOptions
    }

-- | The hooks of a package with a @Hooks@ build type are run by this program,
-- through a hooks executable that has to be built with the same release of
-- Cabal. So that a package can allow older releases (to be usable with older
-- versions of cabal) it is solved for with this one.
withHooksConstraint :: ProjectBaseContext -> ProjectBaseContext
withHooksConstraint ctx =
  ctx{projectConfig = projectConfig ctx <> mempty{projectConfigShared = mempty{projectConfigConstraints = [constraint]}}}
  where
    constraint =
      ( UserConstraint
          (UserAnySetupQualifier (mkPackageName "Cabal-hooks"))
          (PackagePropertyVersion (intersectVersionRanges (orLaterVersion release) (earlierVersion (alterVersion (\v -> take 1 v ++ [v !! 1 + 1]) release))))
      , ConstraintSourceUnknown
      )
    release = alterVersion (take 2) cabalVersion

-- | Implement @cabal buck2@
buck2Action :: NixStyleFlags Buck2Flags -> [String] -> GlobalFlags -> IO ()
buck2Action flags extraArgs globalFlags = do
  unless (null extraArgs) $
    die' verbosity ("'cabal buck2' doesn't take any extra arguments: " ++ unwords extraArgs)
  cache <- either (die' verbosity) return (cacheSetting (extraFlags flags))

  withContextAndSelectors verbosity RejectNoTargets Nothing depsFlags ["all"] globalFlags BuildCommand $
    \targetCtx ctx targetSelectors -> do
      baseCtx <- withHooksConstraint <$> 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 mode baseCtx targetSelectors

      ensureBuckconfigAndPackage verbosity projectRoot
      configureCache verbosity projectRoot cache

      localPkgs <- builtLocalPackages verbosity mode (distDirLayout baseCtx) (elaboratedPlanOriginal buildCtx)

      (externalBuildTools, resolvedDeps) <-
        generatePrebuilt
          verbosity
          projectRoot
          (cabalDirLayout baseCtx)
          (elaboratedShared buildCtx)
          (elaboratedPlanToExecute buildCtx)
          (prebuiltUnits mode (elaboratedPlanOriginal buildCtx))
          (localToolTargets projectRoot localPkgs)
          (localBuildTools projectRoot localPkgs)
          (wantedBuildTools localPkgs)

      -- 'generatePrebuilt' already found and parsed every real @.conf@
      -- file of the resolved dependency closure.
      componentLBIs <- configureComponents verbosity mode baseCtx buildCtx (PackageIndex.fromList resolvedDeps)

      writeAllPackages
        verbosity
        projectRoot
        componentLBIs
        externalBuildTools
        (projectTestOptions mode (elaboratedPlanOriginal buildCtx))
        localPkgs
        (fromFlagOrDefault False (installKeepGoing (installFlags flags)))

      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
    mode = dependencyMode (extraFlags flags)
    depsFlags = flags{installFlags = (installFlags flags){installOnlyDeps = toFlag True}}