cabal-buck2 0.2.0.1 → 0.3.0.0
raw patch · 9 files changed
+248/−53 lines, 9 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Distribution.Client.Buck2.Generate: [lpDir] :: LocalPackage -> FilePath
- Distribution.Client.Buck2.Spec: [depDir] :: SpecDep -> Maybe FilePath
- Distribution.Client.Buck2.Spec: [specPackageDir] :: BuildSpec -> FilePath
- Distribution.Client.Buck2.Spec: [toolDir] :: SpecBuildTool -> FilePath
+ Distribution.Client.Buck2.Prebuilt: RepoPaths :: String -> FilePath -> FilePath -> FilePath -> [(FilePath, FilePath)] -> RepoPaths
+ Distribution.Client.Buck2.Prebuilt: [rpExternal] :: RepoPaths -> [(FilePath, FilePath)]
+ Distribution.Client.Buck2.Prebuilt: [rpGhcVersion] :: RepoPaths -> String
+ Distribution.Client.Buck2.Prebuilt: [rpGlobalRootAbs] :: RepoPaths -> FilePath
+ Distribution.Client.Buck2.Prebuilt: [rpStoreDbRel] :: RepoPaths -> FilePath
+ Distribution.Client.Buck2.Prebuilt: [rpStoreRootAbs] :: RepoPaths -> FilePath
+ Distribution.Client.Buck2.Prebuilt: data RepoPaths
+ Distribution.Client.Buck2.Prebuilt: externalDirs :: RepoPaths -> [FilePath] -> [(FilePath, FilePath)]
+ Distribution.Client.Buck2.Prebuilt: toRepoRelative :: RepoPaths -> FilePath -> Maybe FilePath
+ Distribution.Client.Buck2.Spec: [toolPackage] :: SpecBuildTool -> String
- Distribution.Client.Buck2.Generate: LocalPackage :: FilePath -> PackageDescription -> LocalPackage
+ Distribution.Client.Buck2.Generate: LocalPackage :: PackageDescription -> LocalPackage
- Distribution.Client.Buck2.Generate: generatePackageTargets :: LocalPackageIndex -> FilePath -> FilePath -> Map (PackageName, ComponentName) LocalBuildInfo -> Set String -> Map (PackageName, ComponentName) [PathTemplate] -> Set FilePath -> Set (PackageName, LibraryName) -> Bool -> PackageDescription -> (PackageTargets, [String])
+ Distribution.Client.Buck2.Generate: generatePackageTargets :: LocalPackageIndex -> FilePath -> Map (PackageName, ComponentName) LocalBuildInfo -> Set String -> Map (PackageName, ComponentName) [PathTemplate] -> Set FilePath -> Set (PackageName, LibraryName) -> Bool -> PackageDescription -> (PackageTargets, [String])
- Distribution.Client.Buck2.Spec: BuildSpec :: String -> String -> FilePath -> [String] -> FilePath -> [FilePath] -> [SpecComponent] -> BuildSpec
+ Distribution.Client.Buck2.Spec: BuildSpec :: String -> String -> [String] -> FilePath -> [FilePath] -> [SpecComponent] -> BuildSpec
- Distribution.Client.Buck2.Spec: LocalTool :: String -> FilePath -> SpecBuildTool
+ Distribution.Client.Buck2.Spec: LocalTool :: String -> String -> SpecBuildTool
- Distribution.Client.Buck2.Spec: SpecDep :: String -> Maybe String -> Maybe FilePath -> SpecDep
+ Distribution.Client.Buck2.Spec: SpecDep :: String -> Maybe String -> SpecDep
Files
- README.md +7/−0
- cabal-buck2.cabal +14/−1
- changelog.md +17/−4
- src/Distribution/Client/Buck2/Generate.hs +10/−16
- src/Distribution/Client/Buck2/Prebuilt.hs +100/−10
- src/Distribution/Client/Buck2/Spec.hs +5/−7
- src/Distribution/Client/Buck2/Unpack.hs +0/−1
- src/Distribution/Client/Buck2/Write.hs +33/−5
- test/Main.hs +62/−9
README.md view
@@ -117,6 +117,13 @@ files that Cabal autogenerates, such as `cabal_macros.h` and `Paths_<pkg>.hs`. + * `cabal-buck2/packages.bzl` in the root of the project, which says+ where each of your packages is. Nothing in a package's+ `BUCK.cabal.bzl` says where it is or where the packages it depends+ on are, so moving a package, or building it as part of a bigger+ project, only changes this file. (All the packages have to be in+ the one Buck2 cell, the root.)+ * `third-party/haskell`: tells Buck2 about all the prebuilt package dependencies, either in the Cabal store or in GHC's package DB (unless you build the dependencies from source, see below). In here we also record the GHC version you're using, and the
cabal-buck2.cabal view
@@ -1,6 +1,19 @@ cabal-version: 3.8 name: cabal-buck2-version: 0.2.0.1+version: 0.3.0.0++-- To ship a new version:+-- - bump the version above+-- - update the changelog & README+-- - push the changes to the A.B branch of cabal-buck2+-- - create a branch A.B.C.D on haskell-buck2+-- - push an update to `version.bzl` for A.B.C.D on that branch+-- - cabal install locally, and check it on a local repo+-- - wait for CI on cabal-buck2+-- - cabal sdist, cabal upload, check the candidate+-- - git tag the version A.B.C.D+-- - cabal upload --publish+ synopsis: Build a Cabal project with buck2 (the `cabal buck2` external command) license: BSD-3-Clause build-type: Simple
changelog.md view
@@ -1,7 +1,22 @@-# 0.1.0.0+# 0.3.0.0 * For `cabal-install` version 3.18+ * The generated `BUCK.cabal.bzl` files no longer contain the+ directory of the package, or of the packages it depends on. The+ packages are is now in `cabal-buck2/packages.bzl` in the root of+ the project (which has an entry for each package that the project+ builds), so a package's `BUCK.cabal.bzl` is the same wherever the+ package is in the project, and whichever project it is part of.+ All of the packages have to be in the root Buck2 cell.+ * Support a GHC install whose package database isn't next to the+ libraries of its packages, such as a Nix `ghcWithPackages`. +# 0.2.0.1++ * For `cabal-install` version 3.18+ * Fix an issue with duplicate generated haskell_prebuilt_library()+ targets due to dependencies of build tools.+ # 0.2.0.0 * For `cabal-install` version 3.18@@ -28,8 +43,6 @@ * Fix name clash problem with prebuilt sublibraries * Fix broken `cabal buck2 -j` -# 0.2.0.1+# 0.1.0.0 * For `cabal-install` version 3.18- * Fix an issue with duplicate generated haskell_prebuilt_library()- targets due to dependencies of build tools.
src/Distribution/Client/Buck2/Generate.hs view
@@ -125,8 +125,7 @@ type LocalPackageIndex = Map PackageName LocalPackage data LocalPackage = LocalPackage- { lpDir :: FilePath- , lpDescription :: PackageDescription+ { lpDescription :: PackageDescription } -- | A generated file that lives in the package's @cabal-buck2\/autogen@@@ -186,9 +185,6 @@ :: LocalPackageIndex -> FilePath -- ^ The project root.- -> 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@@ -207,7 +203,7 @@ -- needs (its tests, say), which are skipped without a warning. -> PackageDescription -> (PackageTargets, [String])-generatePackageTargets localIndex projectRoot rootRelPkgDir componentLBIs externalBuildTools projectTestOptions sources skippedLibs inProject pkgDesc =+generatePackageTargets localIndex projectRoot componentLBIs externalBuildTools projectTestOptions sources skippedLibs inProject pkgDesc = (targets, concatMap ctWarnings results) where generate = generateComponent localIndex projectRoot componentLBIs externalBuildTools projectTestOptions sources skippedLibs inProject pkgDesc@@ -223,7 +219,7 @@ components = concatMap ctSpec results targets = PackageTargets- { ptSpec = packageSpec pkgDesc rootRelPkgDir (packageGhcOptions pkgDesc componentLBIs) components+ { ptSpec = packageSpec pkgDesc (packageGhcOptions pkgDesc componentLBIs) components , ptComponentCount = length components , ptAutogenFiles = dedupAutogenFiles (concatMap ctAutogen results) , ptNotes = concatMap ctNotes results@@ -319,7 +315,7 @@ 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) ->- let reexports = [SpecReexport (prettyShow (moduleReexportName r)) (prettyShow original) (specDep localIndex <$> from) | (r, Just (from, original)) <- reexportOrigins localIndex pkgDesc lbi clbi lib]+ let reexports = [SpecReexport (prettyShow (moduleReexportName r)) (prettyShow original) (specDep <$> from) | (r, Just (from, original)) <- reexportOrigins localIndex pkgDesc lbi clbi lib] -- A library with no modules (a compatibility package, say) has -- nothing to build, which a rule can't have. extra :: [(ModuleName.ModuleName, AutogenFile)]@@ -462,12 +458,11 @@ | otherwise = [] -- | A package's build spec.-packageSpec :: PackageDescription -> FilePath -> [String] -> [SpecComponent] -> BuildSpec-packageSpec pkgDesc rootRelPkgDir projectGhcOptions components =+packageSpec :: PackageDescription -> [String] -> [SpecComponent] -> BuildSpec+packageSpec pkgDesc projectGhcOptions components = BuildSpec { specPackageName = unPackageName (packageName pkgDesc) , specPackageVersion = prettyShow (packageVersion pkgDesc)- , specPackageDir = rootRelPkgDir , specGhcOptions = projectGhcOptions , specDataDir = getSymbolicPath (dataDir pkgDesc) , specDataFiles = map getSymbolicPath (dataFiles pkgDesc)@@ -491,7 +486,7 @@ scLanguage = Just (prettyShow (fromMaybe Haskell98 (defaultLanguage bi))) , scExtensions = map prettyShow (usedExtensions bi) , scExtraLibraries = extraLibs bi- , scDeps = map (specDep localIndex) (libraryDeps bi)+ , scDeps = map specDep (libraryDeps bi) , scReexports = [] , scBuildTools = mapMaybe buildToolSpec (ordNub [(pn, exe) | ExeDependency pn exe _ <- buildToolDepends bi]) , scCSources = map getSymbolicPath (cSources bi)@@ -511,7 +506,7 @@ -- 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 lp -> Just (LocalTool (nonLibraryTargetName "-exe" (lpDescription lp) exe) (lpDir lp))+ Just lp -> Just (LocalTool (nonLibraryTargetName "-exe" (lpDescription lp) exe) (unPackageName pn)) Nothing | n `Set.member` externalBuildTools -> Just (ExternalTool n) | otherwise -> Nothing@@ -593,14 +588,13 @@ ] -- | The library, or the package's main one, that a dependency names.-specDep :: LocalPackageIndex -> (PackageName, LibraryName) -> SpecDep-specDep localIndex (pn, ln) =+specDep :: (PackageName, LibraryName) -> SpecDep+specDep (pn, ln) = SpecDep { depPackage = unPackageName pn , depLibrary = case ln of LSubLibName n -> Just (unUnqualComponentName n) LMainLibName -> Nothing- , depDir = lpDir <$> Map.lookup pn localIndex } -- | Where a library's @reexported-modules@ come from, as Cabal resolves them: the
src/Distribution/Client/Buck2/Prebuilt.hs view
@@ -21,12 +21,16 @@ -- depend on. module Distribution.Client.Buck2.Prebuilt ( generatePrebuilt+ , RepoPaths (..)+ , externalDirs+ , toRepoRelative ) where import Distribution.Client.Compat.Prelude import Prelude () import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as BS8 import Data.Char (isHexDigit) import Data.List (stripPrefix) import qualified Data.Map as Map@@ -45,10 +49,12 @@ , isPathSeparator , joinPath , makeRelative+ , normalise , pathSeparator , splitDirectories , splitPath , takeDirectory+ , takeFileName , (<.>) , (</>) )@@ -80,6 +86,7 @@ import Distribution.Simple.Program.Db (lookupProgram) import Distribution.Simple.Program.Types (programPath) import Distribution.Simple.Utils (die', notice, ordNub, warn)+import Distribution.Utils.MD5 (md5, showMD5) import Distribution.Types.InstalledPackageInfo ( InstalledPackageInfo ( depends@@ -98,6 +105,7 @@ import Distribution.Client.Buck2.Fingerprint (ccFingerprint, ghcFingerprint) import Distribution.Client.Buck2.LocalPackages (ToolTarget (..), planElab)+import Distribution.Client.Buck2.Version (cabalBuck2Version) import Distribution.Client.Buck2.Starlark -- | Generate\/refresh @third-party\/haskell@ from the dependency closure@@ -172,6 +180,7 @@ , rpGlobalRootAbs = globalRootAbs , rpStoreRootAbs = storeRootAbs , rpStoreDbRel = makeRelative storeRootAbs storeDB+ , rpExternal = [] } createDirectoryIfMissing True targetDir@@ -211,8 +220,29 @@ -- An earlier version generated a filtered copy of the store's package db here. removeStaleStoreDB (targetDir </> "store-db") + -- Where GHC's package db isn't next to its libraries (a Nix @ghcWithPackages@,+ -- say, in which every package is in a store path of its own), they are+ -- reached through a symlink for each directory.+ let external = externalDirs paths (map (takeDirectory . snd) (concatMap libraryFiles packages))+ paths' = paths{rpExternal = external}+ removeStaleExternal (targetDir </> externalDirName)+ unless (null external) $ do+ notice verbosity $ "cabal buck2: linking " ++ show (length external) ++ " library director" ++ (if length external == 1 then "y" else "ies") ++ " outside GHC's libdir and the cabal store"+ createDirectoryIfMissing True (targetDir </> externalDirName)+ for_ external $ \(dir, link) -> ensureSymlink (targetDir </> link) dir+ for_ packages $ \p ->+ when (not (null (hsLibraries (rpInfo p))) && null (libraryFiles p)) $+ warn verbosity $+ "cabal buck2: found no library files for "+ ++ unUnitId (rpUnitId p)+ ++ " (hs-libraries: "+ ++ unwords (hsLibraries (rpInfo p))+ ++ ", in "+ ++ unwords (libraryDirs (rpInfo p) ++ libraryDynDirs (rpInfo p))+ ++ "): linking against it will fail"+ notice verbosity "cabal buck2: generating third-party/haskell/BUCK"- writeBuckFile targetDir paths packages buildToolPaths localTools+ writeBuckFile targetDir paths' packages buildToolPaths localTools fingerprint <- ghcFingerprint compiler globalDB ccPrint <- ccFingerprint compiler@@ -230,6 +260,10 @@ , rpStoreRootAbs :: FilePath , rpStoreDbRel :: FilePath -- ^ The store's package db, relative to 'rpStoreRootAbs'.+ , rpExternal :: [(FilePath, FilePath)]+ -- ^ Directories that are neither under GHC's libdir nor the cabal store,+ -- and the symlinks (relative to @third-party\/haskell@) to them, which+ -- they are reached through. See 'externalDirs'. } -- | Which of the two package dbs a unit id's @.conf@ lives in - decided@@ -260,10 +294,18 @@ , rpUnitId :: UnitId , rpDbKind :: PkgDbKind , rpStaticLibs :: [FilePath]+ -- ^ Absolute paths, like 'rpProfiledLibs' and 'rpSharedLibs'. , rpProfiledLibs :: [FilePath] , rpSharedLibs :: [(String, FilePath)] } +-- | Every library file found for the package, and what to call it.+libraryFiles :: ResolvedPackage -> [(String, FilePath)]+libraryFiles p =+ [("static", f) | f <- rpStaticLibs p]+ ++ [("profiled", f) | f <- rpProfiledLibs p]+ ++ [("shared", f) | (_, f) <- rpSharedLibs p]+ confPath :: FilePath -> FilePath -> UnitId -> FilePath confPath globalRootAbs storeDB uid = case classifyUnitId uid of GlobalDb -> globalRootAbs </> "package.conf.d" </> unUnitId uid <.> "conf"@@ -315,15 +357,18 @@ findLibs :: RepoPaths -> [FilePath] -> [String] -> IO [FilePath] findLibs paths dirs fnames = catMaybes <$> traverse (findOne paths dirs) fnames +-- | The absolute path of the file; it is made relative to @third-party\/haskell@+-- by 'toRepoRelative' when it is written, as the directories it is in+-- may need a symlink first (see 'externalDirs'). findOne :: RepoPaths -> [FilePath] -> String -> IO (Maybe FilePath)-findOne paths dirs fname = go dirs+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)+ then return (Just absPath) else go ds findSharedLibs :: RepoPaths -> InstalledPackageInfo -> IO [(String, FilePath)]@@ -445,11 +490,11 @@ , ("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))+ , ("static_libs", strList (rel (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])]+ ++ [("profiled_static_libs", strList profiled) | not (null profiled)]+ ++ [("pic_profiled_static_libs", strList profiled) | not (null profiled)]+ ++ [("shared_libs", VDict [(soname, str sopath) | (soname, abs') <- rpSharedLibs p, Just sopath <- [toRepoRelative paths abs']])] ++ [("cxx_header_dirs", strList headerDirs) | not (null headerDirs)] ++ [("exported_linker_flags", strList extraLinkerFlags) | not (null extraLinkerFlags)] ++ [("deps", strList depTargets) | not (null depTargets)]@@ -457,14 +502,16 @@ ) where info = rpInfo p+ rel = mapMaybe (toRepoRelative paths)+ profiled = rel (rpProfiledLibs 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+-- | Convert an absolute path under GHC's libdir, the cabal store or one of the+-- 'externalDirs' 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@@ -472,7 +519,46 @@ | otherwise = relTo (rpGlobalRootAbs paths) ("ghc-" ++ rpGhcVersion paths) path <|> relTo (rpStoreRootAbs paths) "cabal-store" path+ <|> listToMaybe (mapMaybe (\(dir, link) -> relTo dir link path) (rpExternal paths)) +-- | The directory of the symlinks to the 'externalDirs'.+externalDirName :: FilePath+externalDirName = "external"++-- | The directories with the library files of packages that are+-- neither under GHC's libdir nor in the cabal store, which buck2 can only get at+-- through a symlink in the project, with the name (relative to+-- @third-party\/haskell@) of the symlink to each. The headers of a package are+-- found through those too, when they are in them (as they are for a Nix+-- package); a directory of headers on its own, like @\/usr\/include@, gets+-- no symlink: it would be the system's headers in the project.+--+-- GHC's package db isn't always next to the libraries of its packages. In a+-- Nix @ghcWithPackages@, say, the library files of each package are in a store+-- path of their own (@\/nix\/store\/<hash>-aeson-...\/lib\/...@). A path in a+-- rule has to be relative and inside the project, so it can't be given as+-- it is.+--+-- A symlinked directory is part of the project as far as buck2's file watcher+-- is concerned, so no more is linked than needed. That is each directory with+-- library files, except one that is inside another such directory (the+-- directory of the static libraries of a package, which is in the+-- directory with its shared libraries, say). The symlink is named after the+-- directory and a hash of its path, which makes the name the same every time.+externalDirs :: RepoPaths -> [FilePath] -> [(FilePath, FilePath)]+externalDirs paths dirs = [(dir, externalDirName </> linkName dir) | dir <- sort maximal]+ where+ outside = ordNub [d | d <- map normalise dirs, isAbsolute d, isNothing (relTo (rpGlobalRootAbs paths) "" d <|> relTo (rpStoreRootAbs paths) "" d)]+ maximal = [d | d <- outside, not (any (`isProperAncestor` d) outside)]+ isProperAncestor a d = a /= d && makeRelative a d /= d+ linkName dir = takeFileName dir ++ "-" ++ take 12 (showMD5 (md5 (BS8.pack dir)))++-- | Remove the symlinks that an earlier run made.+removeStaleExternal :: FilePath -> IO ()+removeStaleExternal dir = do+ exists <- doesPathExist dir+ when exists $ removeDirectoryRecursive dir+ relTo :: FilePath -> FilePath -> FilePath -> Maybe FilePath relTo root repoRelPrefix path = let r = makeRelative root path@@ -545,6 +631,10 @@ [ "# @generated by `cabal buck2` - do not edit by hand." , "# Re-run `cabal buck2` to update." , ""+ , -- Checked by buck2/cabal.bzl before it loads anything else from+ -- here, so that tools.bzl written by another release of cabal-buck2+ -- is reported as that, not as a missing symbol.+ "CABAL_BUCK2_VERSION = " ++ show cabalBuck2Version , "GHC_VERSION = " ++ show ghcVersionStr , "GHC_FINGERPRINT = " ++ show fingerprint , "CC_FINGERPRINT = " ++ show ccPrint
src/Distribution/Client/Buck2/Spec.hs view
@@ -21,8 +21,6 @@ data BuildSpec = BuildSpec { specPackageName :: String , specPackageVersion :: String- , specPackageDir :: FilePath- -- ^ Relative to the buck2 cell root, @.@ at the root. , specGhcOptions :: [String] -- ^ Supplied by the project, not the @.cabal@ file. , specDataDir :: FilePath@@ -97,10 +95,10 @@ { 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. }+ -- A dependency doesn't say whether the package is built by this project or+ -- is a prebuilt one: that is whether it has an entry in @packages.bzl@ (see+ -- "Distribution.Client.Buck2.Packages"). -- | A module that a library re-exports (@reexported-modules@) from a library -- it depends on.@@ -114,7 +112,7 @@ -- | 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}+ = -- | Built by this project, as the given target of the given package.+ LocalTool {toolExe :: String, toolPackage :: String} | -- | An already-installed binary, exported from @third-party\/haskell@. ExternalTool {toolExe :: String}
src/Distribution/Client/Buck2/Unpack.hs view
@@ -23,7 +23,6 @@ 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
src/Distribution/Client/Buck2/Write.hs view
@@ -71,10 +71,11 @@ ["cabal buck2: some components can't be built with buck2:"] ++ map ((" " ++) . withoutPrefix) problems ++ ["Use --keep-going to generate everything else anyway."]+ writePackagesFile verbosity projectRoot pkgs traverse_ (writeOnePackage verbosity projectRoot) results where generate skipped pkg srcs =- generatePackageTargets localIndex projectRoot (rootRelativeDir projectRoot (bpDir pkg)) componentLBIs externalBuildTools projectTestOptions srcs skipped (bpInProject pkg) (bpDescription pkg)+ generatePackageTargets localIndex projectRoot componentLBIs externalBuildTools projectTestOptions srcs skipped (bpInProject pkg) (bpDescription pkg) librariesWithoutRule pkg (targets, _) = [ (packageName desc, libName lib)@@ -87,11 +88,39 @@ localIndex :: LocalPackageIndex localIndex = Map.fromList- [ (packageName pkgDesc, LocalPackage{lpDir = rootRelativeDir projectRoot (bpDir pkg), lpDescription = pkgDesc})+ [ (packageName pkgDesc, LocalPackage{lpDescription = pkgDesc}) | pkg <- pkgs , let pkgDesc = bpDescription pkg ] +-- | Writes @cabal-buck2\/packages.bzl@ in the project root, which says where+-- each package is, as its directory relative to the project root. @buck2\/cabal.bzl@+-- loads it to find the targets of the packages that a package depends on, and+-- the paths of its own directory. Nothing in a package's @BUCK.cabal.bzl@ is+-- relative to the project, so only this file changes when a package moves.+writePackagesFile :: Verbosity -> FilePath -> [BuiltPackage] -> IO ()+writePackagesFile verbosity projectRoot pkgs = do+ createDirectoryIfMissing True (takeDirectory path)+ writeFile path contents+ notice verbosity $ "cabal buck2: generated cabal-buck2" </> "packages.bzl" ++ " (" ++ show (length pkgs) ++ " package(s))"+ where+ path = projectRoot </> "cabal-buck2" </> "packages.bzl"+ contents =+ unlines+ [ "# @generated by `cabal buck2` - do not edit by hand."+ , "# Re-run `cabal buck2` after adding, removing or moving a package."+ , "#"+ , "# The directory of each package that this project builds, from the root of"+ , "# the project. See buck2/cabal.bzl."+ , ""+ ]+ ++ renderBinding "PACKAGES" (VDict (Map.toList packages))+ packages =+ Map.fromList+ [ (prettyShow (packageName (bpDescription pkg)), str (rootRelativeDir projectRoot (bpDir pkg)))+ | pkg <- pkgs+ ]+ withoutPrefix :: String -> String withoutPrefix = fromMaybe <*> stripPrefix "cabal buck2: " @@ -169,7 +198,7 @@ specValue spec = VDict $ [ ("cabal_buck2", str cabalBuck2Version)- , ("package", VDict [("name", str (specPackageName spec)), ("version", str (specPackageVersion spec)), ("dir", str (specPackageDir spec))])+ , ("package", VDict [("name", str (specPackageName spec)), ("version", str (specPackageVersion spec))]) ] ++ listField "ghc_options" (specGhcOptions spec) ++ [ ("data", VDict [("dir", str (specDataDir spec)), ("files", strList (specDataFiles spec))])@@ -214,13 +243,12 @@ VDict $ [("package", str (depPackage d))] ++ [("library", str lib) | Just lib <- [depLibrary d]]- ++ [("dir", str dir) | Just dir <- [depDir d]] reexportValue :: SpecReexport -> Value reexportValue r = VDict ([("module", str (reModule r)), ("original", str (reOriginal r))] ++ [("from", depValue d) | Just d <- [reFrom r]]) buildToolValue :: SpecBuildTool -> Value-buildToolValue (LocalTool exe dir) = VDict [("exe", str exe), ("dir", str dir)]+buildToolValue (LocalTool exe pkg) = VDict [("exe", str exe), ("package", str pkg)] buildToolValue (ExternalTool exe) = VDict [("exe", str exe), ("external", VBool True)] -- | A list-valued field, omitted if empty.
test/Main.hs view
@@ -5,7 +5,8 @@ import Control.Exception (SomeException, bracket, displayException, try) import Control.Monad (forM_, unless, when)-import Data.List (isInfixOf)+import Data.List (isInfixOf, nub)+import Data.Maybe (fromMaybe) import qualified Data.ByteString.Char8 as BS8 import qualified Data.Map as Map import System.Directory@@ -17,6 +18,7 @@ , findExecutable , getTemporaryDirectory , listDirectory+ , makeAbsolute , removeDirectoryRecursive ) import System.Environment (getEnvironment, lookupEnv)@@ -27,11 +29,19 @@ import Distribution.Client.Buck2.Cache (cacheBlock, spliceBlock) import Distribution.Client.Buck2.Fingerprint (ccFingerprintOf, fingerprintOf)+import Distribution.Client.Buck2.Prebuilt (RepoPaths (..), externalDirs, toRepoRelative) import Distribution.Client.Buck2.Version (cabalBuck2Version) main :: IO () main = do- exe <- maybe (fail "cabal-buck2 not found on PATH") return =<< findExecutable "cabal-buck2"+ -- The executable to test: $CABAL_BUCK2_EXE if it's set (as when this is run by+ -- buck2, which only puts a build-tool-depends executable on PATH for+ -- building, not for running tests - see BUCK), otherwise the one on PATH (as+ -- `cabal test` has it, from build-tool-depends).+ fromEnv <- lookupEnv "CABAL_BUCK2_EXE"+ exe <- case fromEnv of+ Just path -> makeAbsolute path -- the tests run in other directories+ Nothing -> maybe (fail "cabal-buck2 not found on PATH") return =<< findExecutable "cabal-buck2" results <- mapM (runTest exe) tests unitResults <- mapM runUnitTest unitTests let failures =@@ -42,7 +52,7 @@ exitFailure unitTests :: [(String, IO ())]-unitTests = [("fingerprint", fingerprint), ("cc-fingerprint", ccFingerprint), ("splice-block", spliceBlockTest)]+unitTests = [("fingerprint", fingerprint), ("cc-fingerprint", ccFingerprint), ("splice-block", spliceBlockTest), ("external-dirs", externalDirsTest)] runUnitTest :: (String, IO ()) -> IO Bool runUnitTest (name, test) = do@@ -272,10 +282,17 @@ 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).+ -- A dependency on another local package is just its name: where packages+ -- are is in cabal-buck2/packages.bzl, written once for the project, so that+ -- a package's own file doesn't change when the package moves. exe "'package': 'lib-pkg'" exeBzl- exe "'dir': 'lib-pkg'" exeBzl+ assertNotContains "exe-pkg/BUCK.cabal.bzl" "'dir'" exeBzl+ assertNotContains "lib-pkg/BUCK.cabal.bzl" "'dir'" libBzl+ packages <- readIn project ("cabal-buck2" </> "packages.bzl")+ let pk = assertContains "cabal-buck2/packages.bzl"+ pk "PACKAGES = {" packages+ pk "'exe-pkg': 'exe-pkg'," packages+ pk "'lib-pkg': 'lib-pkg'," packages -- C sources and include directories are recorded as written in the .cabal -- file; cabal.bzl turns them into a cxx_library().@@ -369,7 +386,11 @@ depBzl <- readIn project depBzlPath assertContains depBzlPath "'name': 'dep'" depBzl assertContains depBzlPath "'version': '1.0'" depBzl- assertContains depBzlPath "'dir': 'dist-newstyle/src/dep-1.0'" depBzl+ -- ... but not where it is (the 'dir' of its data files is something else).+ assertContains depBzlPath "'version': '1.0',\n }," depBzl+ -- Where it is, is in packages.bzl, with the packages of the project.+ packages <- readIn project ("cabal-buck2" </> "packages.bzl")+ assertContains "cabal-buck2/packages.bzl" "'dep': 'dist-newstyle/src/dep-1.0'," packages -- Its data files are recorded, for the tools that read them. assertContains depBzlPath "'data/*.txt'" depBzl -- hsc2hs is told what it is compiling for.@@ -381,7 +402,8 @@ appBzl <- readIn project ("app" </> "BUCK.cabal.bzl") -- A literate happy grammar is a module source. assertContains "app/BUCK.cabal.bzl" "'Parser': 'Parser.ly'" appBzl- assertContains "app/BUCK.cabal.bzl" "'dir': 'dist-newstyle/src/dep-1.0'" appBzl+ assertContains "app/BUCK.cabal.bzl" "'package': 'dep'," appBzl+ assertNotContains "app/BUCK.cabal.bzl" "'dir'" appBzl -- Without the flag the dependency is the store's again. _ <- buck2 project []@@ -401,7 +423,7 @@ reBzl <- readIn project ("re-lib" </> "BUCK.cabal.bzl") let re = assertContains "re-lib/BUCK.cabal.bzl" re "'reexports'" reBzl- re "'module': 'Orig.A',\n 'original': 'Orig.A',\n 'from': {\n 'package': 'base-lib',\n 'dir': 'base-lib'," reBzl+ re "'module': 'Orig.A',\n 'original': 'Orig.A',\n 'from': {\n 'package': 'base-lib',\n }," reBzl re "'module': 'New.B',\n 'original': 'Orig.B'," reBzl re "'module': 'Sub.M',\n 'original': 'Sub.M',\n 'from': {\n 'package': 're-lib',\n 'library': 'sub'," reBzl -- One of its own modules has no library it comes from, and no module of its own.@@ -544,3 +566,34 @@ 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++-- | Libraries outside GHC's libdir and the cabal store (as in a Nix+-- @ghcWithPackages@) are reached through a symlink for each directory.+externalDirsTest :: IO ()+externalDirsTest = do+ let paths = RepoPaths{rpGhcVersion = "9.6.7", rpGlobalRootAbs = "/ghc/lib", rpStoreRootAbs = "/home/u/.cabal/store/ghc-9.6.7", rpStoreDbRel = "package.db", rpExternal = []}+ aeson = "/nix/store/aaa-aeson-lib-aeson-2.2.5.1/lib/x86_64-linux-ghc-9.6.7"+ text = "/nix/store/bbb-text-lib-text-2.1.4/lib/x86_64-linux-ghc-9.6.7"+ dirs =+ [ aeson ++ "/aeson-2.2.5.1-X" -- the static libraries, inside the shared libraries' directory+ , aeson+ , text ++ "/text-2.1.4-Y"+ , text+ , "/ghc/lib/x86_64-linux-ghc-9.6.7/base-4.18.3.0" -- under GHC's libdir+ , "/home/u/.cabal/store/ghc-9.6.7/foo-1/lib" -- in the cabal store+ ]+ linked = externalDirs paths dirs+ check what expected actual =+ unless (expected == actual) $+ failure (what ++ ": expected " ++ show expected ++ ", got " ++ show actual)+ check "one link for each package, for neither GHC's libdir nor the store" [aeson, text] (map fst linked)+ check "links are the same on every run" linked (externalDirs paths (reverse dirs ++ dirs))+ let names = map snd linked+ check "links are in external/" True (all (\n -> take 9 n == "external/") names)+ check "the links are different" 2 (length (nub names))+ let paths' = paths{rpExternal = linked}+ aesonLink = fromMaybe (error "no link for aeson") (lookup aeson linked)+ check "a library in a linked directory" (Just (aesonLink ++ "/aeson-2.2.5.1-X/libHSaeson-2.2.5.1-X.a")) (toRepoRelative paths' (aeson ++ "/aeson-2.2.5.1-X/libHSaeson-2.2.5.1-X.a"))+ check "a library under GHC's libdir" (Just "ghc-9.6.7/x86_64-linux-ghc-9.6.7/base-4.18.3.0/libHSbase.a") (toRepoRelative paths' "/ghc/lib/x86_64-linux-ghc-9.6.7/base-4.18.3.0/libHSbase.a")+ check "a library in the store" (Just "cabal-store/foo-1/lib/libHSfoo.a") (toRepoRelative paths' "/home/u/.cabal/store/ghc-9.6.7/foo-1/lib/libHSfoo.a")+ check "something that isn't linked" Nothing (toRepoRelative paths' "/usr/include/stdio.h")