packages feed

cabal-buck2-0.1.0.0: src/Distribution/Client/Buck2/Write.hs

-- | 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()"
    ]