diff --git a/Cabal/src/Distribution/Simple/Build.hs b/Cabal/src/Distribution/Simple/Build.hs index 1ceb75a4a54..1d59392ef8a 100644 --- a/Cabal/src/Distribution/Simple/Build.hs +++ b/Cabal/src/Distribution/Simple/Build.hs @@ -573,6 +573,7 @@ buildComponent lib' lbi clbi + Nothing debug verbosity $ "Registering inplace:\n" ++ IPI.showInstalledPackageInfo installedPkgInfo registerPackage verbosity @@ -860,7 +861,7 @@ testSuiteLibV09AsLibAndExe , testSuites = [] , subLibraries = [lib] } - ipi = inplaceInstalledPackageInfo inplaceDir distPref pkg (mkAbiHash "") lib lbi libClbi + ipi = inplaceInstalledPackageInfo inplaceDir distPref pkg (mkAbiHash "") lib lbi libClbi Nothing testLibDep = Dependency pkgName' diff --git a/Cabal/src/Distribution/Simple/Haddock.hs b/Cabal/src/Distribution/Simple/Haddock.hs index af4b4fd8990..470078194c9 100644 --- a/Cabal/src/Distribution/Simple/Haddock.hs +++ b/Cabal/src/Distribution/Simple/Haddock.hs @@ -437,6 +437,7 @@ haddock_setupHooks lib lbi' clbi + Nothing debug verbosity $ "Registering inplace:\n" diff --git a/Cabal/src/Distribution/Simple/Register.hs b/Cabal/src/Distribution/Simple/Register.hs index 7b35a5d0583..6ae72e149d6 100644 --- a/Cabal/src/Distribution/Simple/Register.hs +++ b/Cabal/src/Distribution/Simple/Register.hs @@ -277,10 +277,20 @@ generateRegistrationInfo verbosity pkg lib lbi clbi inplace reloc distPref packa inplaceDir <- absoluteWorkingDirLBI lbi installedPkgInfo <- if inplace - then -- NB: With an inplace installation, the user may run './Setup - -- build' to update the library files, without reregistering. - -- In this case, it is critical that the ABI hash not flip. - + then do + -- NB: With an inplace installation, the user may run './Setup + -- build' to update the library files, without reregistering. + -- In this case, it is critical that the ABI hash not flip. + + -- When a relocatable build is requested, emit ${pkgroot}-relative + -- paths (relative to the directory containing the package db) so that + -- the dist tree can be copied/relocated without invalidating the + -- registration. Only GHC understands the ${pkgroot} substitution, so + -- for other compilers we fall back to absolute paths. + maybePkgRoot <- + if reloc && compilerFlavor (compiler lbi) == GHC + then Just <$> GHC.pkgRoot verbosity lbi packageDb + else return Nothing return ( inplaceInstalledPackageInfo inplaceDir @@ -290,6 +300,7 @@ generateRegistrationInfo verbosity pkg lib lbi clbi inplace reloc distPref packa lib lbi clbi + maybePkgRoot ) else do abi_hash <- abiHash verbosity pkg distPref lbi lib clbi @@ -621,8 +632,13 @@ inplaceInstalledPackageInfo -> Library -> LocalBuildInfo -> ComponentLocalBuildInfo + -> Maybe (SymbolicPath CWD (Dir Pkg)) + -- ^ If @Just pkgroot@, emit paths relative to @pkgroot@ using the + -- @${pkgroot}@ substitution variable (for relocatable inplace builds). + -- @pkgroot@ is the directory containing the package database (see + -- 'Distribution.Simple.GHC.pkgRoot'). If @Nothing@, emit absolute paths. -> InstalledPackageInfo -inplaceInstalledPackageInfo inplaceDir distPref pkg abi_hash lib lbi clbi = +inplaceInstalledPackageInfo inplaceDir distPref pkg abi_hash lib lbi clbi mb_pkgroot = generalInstalledPackageInfo adjustRelativeIncludeDirs pkg @@ -633,25 +649,32 @@ inplaceInstalledPackageInfo inplaceDir distPref pkg abi_hash lib lbi clbi = installDirs where i = interpretSymbolicPathAbsolute inplaceDir -- See Note [Symbolic paths] in Distribution.Utils.Path + -- Make an absolute path relative to ${pkgroot} when a relocatable build + -- was requested; otherwise leave it absolute. + relocate :: FilePath -> FilePath + relocate = case mb_pkgroot of + Nothing -> id + Just pkgroot -> ("${pkgroot}" ) . shortRelativePath (getSymbolicPath pkgroot) adjustRelativeIncludeDirs = concatMap $ \d -> - [ i $ makeRelativePathEx d -- local include-dir - , i $ libTargetDir makeRelativePathEx d -- autogen include-dir + [ relocate $ i $ makeRelativePathEx d -- local include-dir + , relocate $ i $ libTargetDir makeRelativePathEx d -- autogen include-dir ] libTargetDir = componentBuildDir lbi clbi installDirs = - (absoluteComponentInstallDirs pkg lbi (componentUnitId clbi) NoCopyDest) - { libdir = i libTargetDir - , dynlibdir = i libTargetDir - , bytecodelibdir = i libTargetDir - , datadir = - let rawDataDir = dataDir pkg - in if null $ getSymbolicPath rawDataDir - then i sameDirectory - else i rawDataDir - , docdir = i inplaceDocdir - , htmldir = inplaceHtmldir - , haddockdir = inplaceHtmldir - } + fmap relocate $ + (absoluteComponentInstallDirs pkg lbi (componentUnitId clbi) NoCopyDest) + { libdir = i libTargetDir + , dynlibdir = i libTargetDir + , bytecodelibdir = i libTargetDir + , datadir = + let rawDataDir = dataDir pkg + in if null $ getSymbolicPath rawDataDir + then i sameDirectory + else i rawDataDir + , docdir = i inplaceDocdir + , htmldir = inplaceHtmldir + , haddockdir = inplaceHtmldir + } inplaceDocdir = distPref makeRelativePathEx "doc" inplaceHtmldir = i $ diff --git a/cabal-install/src/Distribution/Client/ProjectBuilding/PackageFileMonitor.hs b/cabal-install/src/Distribution/Client/ProjectBuilding/PackageFileMonitor.hs index f7ddca60251..b91704cdfa3 100644 --- a/cabal-install/src/Distribution/Client/ProjectBuilding/PackageFileMonitor.hs +++ b/cabal-install/src/Distribution/Client/ProjectBuilding/PackageFileMonitor.hs @@ -20,13 +20,18 @@ import Distribution.Client.Types hiding ) import Distribution.InstalledPackageInfo (InstalledPackageInfo) +import Distribution.Simple.Compiler (PackageDBX (..)) import Distribution.Simple.LocalBuildInfo ( ComponentName (..) ) -import Distribution.Simple.Utils (removeFileForcibly) +import Distribution.Simple.Utils (removeFileForcibly, shortRelativePath) +import qualified Distribution.Types.LocalBuildConfig as LBC +import qualified Data.Map as Map import qualified Data.Set as Set +import System.FilePath (isAbsolute, normalise) + ----------------------------- -- Package change detection -- @@ -60,13 +65,17 @@ newPackageFileMonitor -> PackageFileMonitor newPackageFileMonitor shared - DistDirLayout{distPackageCacheFile} + DistDirLayout{distPackageCacheFile, distDirectory} dparams = PackageFileMonitor { pkgFileMonitorConfig = FileMonitor { fileMonitorCacheFile = distPackageCacheFile dparams "config" - , fileMonitorKeyValid = (==) `on` normaliseConfiguredPackage shared + , -- #12137: The config monitor key is compared after relativising + -- project-local paths, so that relocating the whole tree + -- does not spuriously invalidate it. + fileMonitorKeyValid = + (==) `on` (relativiseElabConfigPaths distDirectory . normaliseConfiguredPackage shared) , fileMonitorCheckIfOnlyValueChanged = False } , pkgFileMonitorBuild = @@ -80,6 +89,47 @@ newPackageFileMonitor newFileMonitor (distPackageCacheFile dparams "registration") } +-- | Rewrite the absolute paths in an 'ElaboratedConfiguredPackage' that live +-- under the build tree root so that they become relative to it. This makes the +-- package-configuration file monitor /key/ location-independent. +relativiseElabConfigPaths :: FilePath -> ElaboratedConfiguredPackage -> ElaboratedConfiguredPackage +relativiseElabConfigPaths buildDir elab = + elab + { elabPkgSourceLocation = relLocation (elabPkgSourceLocation elab) + , elabPackageDbs = map (fmap relDB) (elabPackageDbs elab) + , elabSetupPackageDBStack = map relDB (elabSetupPackageDBStack elab) + , elabBuildPackageDBStack = map relDB (elabBuildPackageDBStack elab) + , elabRegisterPackageDBStack = map relDB (elabRegisterPackageDBStack elab) + , elabInplaceSetupPackageDBStack = map relDB (elabInplaceSetupPackageDBStack elab) + , elabInplaceBuildPackageDBStack = map relDB (elabInplaceBuildPackageDBStack elab) + , elabInplaceRegisterPackageDBStack = map relDB (elabInplaceRegisterPackageDBStack elab) + , elabInstallDirs = fmap rel (elabInstallDirs elab) + , elabExtraLibDirs = map rel (elabExtraLibDirs elab) + , elabExtraLibDirsStatic = map rel (elabExtraLibDirsStatic elab) + , elabExtraFrameworkDirs = map rel (elabExtraFrameworkDirs elab) + , elabExtraIncludeDirs = map rel (elabExtraIncludeDirs elab) + , elabProgramPaths = Map.map rel (elabProgramPaths elab) + , elabProgramPathExtra = map rel (elabProgramPathExtra elab) + , elabHaddockCss = fmap rel (elabHaddockCss elab) + , elabHaddockHscolourCss = fmap rel (elabHaddockHscolourCss elab) + , elabHaddockOutputDir = fmap rel (elabHaddockOutputDir elab) + , elabTestWrapper = fmap rel (elabTestWrapper elab) + } + where + rel :: FilePath -> FilePath + rel p + | isAbsolute p = shortRelativePath (normalise buildDir) (normalise p) + | otherwise = p + + relDB :: PackageDBX FilePath -> PackageDBX FilePath + relDB (SpecificPackageDB fp) = SpecificPackageDB (rel fp) + relDB db = db + + relLocation :: PackageLocation (Maybe FilePath) -> PackageLocation (Maybe FilePath) + relLocation (LocalUnpackedPackage fp) = LocalUnpackedPackage (rel fp) + relLocation (LocalTarballPackage fp) = LocalTarballPackage (rel fp) + relLocation loc = loc + -- | Helper function for 'checkPackageFileMonitorChanged', -- 'updatePackageConfigFileMonitor' and 'updatePackageBuildFileMonitor'. -- @@ -119,6 +169,7 @@ packageFileMonitorKeyValues elab = , elabTestKeepTix = False , elabTestTestOptions = [] , elabBenchmarkOptions = [] + , elabBuildOptions = (elabBuildOptions elab){LBC.relocatable = False} } -- The second part is the value used to guard the build step. So this is @@ -209,11 +260,14 @@ checkPackageFileMonitorChanged changedToMaybe (MonitorUnchanged x _) = Just x updatePackageConfigFileMonitor - :: PackageFileMonitor + :: FilePath + -- ^ The build directory + -> PackageFileMonitor -> FilePath -> ElaboratedConfiguredPackage -> IO () updatePackageConfigFileMonitor + buildDir PackageFileMonitor{pkgFileMonitorConfig} srcdir pkg = @@ -222,7 +276,7 @@ updatePackageConfigFileMonitor srcdir Nothing [] - pkgconfig + (relativiseElabConfigPaths buildDir pkgconfig) () where (pkgconfig, _buildComponents) = packageFileMonitorKeyValues pkg diff --git a/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs b/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs index 10cba75bb16..a6920fc2654 100644 --- a/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs +++ b/cabal-install/src/Distribution/Client/ProjectBuilding/UnpackedPackage.hs @@ -576,7 +576,11 @@ buildInplaceUnpackedPackage whenReconfigure $ do mbLBI <- runConfigure invalidatePackageRegFileMonitor packageFileMonitor - updatePackageConfigFileMonitor packageFileMonitor (getSymbolicPath srcdir) pkg + updatePackageConfigFileMonitor + distDirectory + packageFileMonitor + (getSymbolicPath srcdir) + pkg return mbLBI PBBuildPhase{runBuild} -> whenRebuild $ withFileMonitor runBuild diff --git a/cabal-install/src/Distribution/Client/ProjectPlanOutput.hs b/cabal-install/src/Distribution/Client/ProjectPlanOutput.hs index 5008de76471..4afae231cf7 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanOutput.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanOutput.hs @@ -85,23 +85,29 @@ import Distribution.Simple.Program.GHC (packageDbArgsDb) -- -- This is for the benefit of debugging and external tools like editors. writePlanExternalRepresentation - :: DistDirLayout + :: Relocatable + -- ^ When 'Relocatable', emit project-local filesystem paths (@dist-dir@, + -- @build-info@, inplace @bin-file@, local @pkg-src@ @path@) relative to the + -- project root instead of as absolute paths, so that the whole tree + -- (sources + dist-dir) can be relocated without invalidating @plan.json@. + -> DistDirLayout -> ElaboratedInstallPlan -> ElaboratedSharedConfig -> IO () writePlanExternalRepresentation + relocatable distDirLayout elaboratedInstallPlan elaboratedSharedConfig = writeFileAtomic (distProjectCacheFile distDirLayout "plan.json") $ BB.toLazyByteString . J.encodeToBuilder - $ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig + $ encodePlanAsJson relocatable distDirLayout elaboratedInstallPlan elaboratedSharedConfig -- | Renders a subset of the elaborated install plan in a semi-stable JSON -- format. -encodePlanAsJson :: DistDirLayout -> ElaboratedInstallPlan -> ElaboratedSharedConfig -> J.Value -encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = +encodePlanAsJson :: Relocatable -> DistDirLayout -> ElaboratedInstallPlan -> ElaboratedSharedConfig -> J.Value +encodePlanAsJson relocatable distDirLayout elaboratedInstallPlan elaboratedSharedConfig = -- TODO: [nice to have] include all of the sharedPackageConfig and all of -- the parts of the elaboratedInstallPlan J.object @@ -116,9 +122,19 @@ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = , "install-plan" J..= installPlanToJ elaboratedInstallPlan ] where + relative :: Bool + relative = relocatable == Relocatable + plat :: Platform plat@(Platform arch os) = pkgConfigPlatform elaboratedSharedConfig + -- For relocatable plans, express project-local paths relative to the build + -- directory (where plan.json itself lives), so the whole tree can be moved. + mkPath :: FilePath -> FilePath + mkPath p + | relative, isAbsolute p = shortRelativePath (normalise (distDirectory distDirLayout)) (normalise p) + | otherwise = p + installPlanToJ :: ElaboratedInstallPlan -> [J.Value] installPlanToJ = map planPackageToJ . InstallPlan.toList @@ -181,7 +197,7 @@ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = ] ++ ( case elabBuildStyle elab of BuildInplaceOnly{} -> - ["dist-dir" J..= J.String dist_dir] ++ [buildInfoFileLocation] + ["dist-dir" J..= J.String (mkPath dist_dir)] ++ [buildInfoFileLocation] BuildAndInstall -> -- TODO: install dirs? [] @@ -223,7 +239,7 @@ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = | elabSetupScriptCliVersion elab < mkVersion [3, 7, 0, 0] = "build-info" J..= J.Null | otherwise = - "build-info" J..= J.String (getSymbolicPath $ buildInfoPref $ makeSymbolicPath dist_dir) + "build-info" J..= J.String (mkPath $ getSymbolicPath $ buildInfoPref $ makeSymbolicPath dist_dir) packageLocationToJ :: PackageLocation (Maybe FilePath) -> J.Value packageLocationToJ pkgloc = @@ -231,12 +247,12 @@ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = LocalUnpackedPackage local -> J.object [ "type" J..= J.String "local" - , "path" J..= J.String local + , "path" J..= J.String (mkPath local) ] LocalTarballPackage local -> J.object [ "type" J..= J.String "local-tar" - , "path" J..= J.String local + , "path" J..= J.String (mkPath local) ] RemoteTarballPackage uri _ -> J.object @@ -310,7 +326,7 @@ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = where bin = if isInplaceBuildStyle (elabBuildStyle elab) - then dist_dir "build" prettyShow s prettyShow s <.> exeExtension plat + then mkPath $ dist_dir "build" prettyShow s prettyShow s <.> exeExtension plat else InstallDirs.bindir (elabInstallDirs elab) prettyShow s <.> exeExtension plat flib_file' :: Pretty a => a -> [J.Pair] @@ -319,7 +335,7 @@ encodePlanAsJson distDirLayout elaboratedInstallPlan elaboratedSharedConfig = where bin = if isInplaceBuildStyle (elabBuildStyle elab) - then dist_dir "build" prettyShow s ("lib" ++ prettyShow s) <.> dllExtension plat + then mkPath $ dist_dir "build" prettyShow s ("lib" ++ prettyShow s) <.> dllExtension plat else InstallDirs.bindir (elabInstallDirs elab) ("lib" ++ prettyShow s) <.> dllExtension plat comp2str :: ComponentDeps.Component -> String diff --git a/cabal-install/src/Distribution/Client/ProjectPlanning.hs b/cabal-install/src/Distribution/Client/ProjectPlanning.hs index 0bc07c791a0..b636b9243f5 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanning.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanning.hs @@ -981,7 +981,16 @@ rebuildInstallPlan -> Rebuild () phaseMaintainPlanOutputs elaboratedPlan elaboratedShared = liftIO $ do debug verbosity "Updating plan.json" + -- #12137: Emit project-relative paths in plan.json for relocatable builds + let isReloc pp = case pp of + InstallPlan.Configured elab -> LBC.relocatable (elabBuildOptions elab) + InstallPlan.Installed elab -> LBC.relocatable (elabBuildOptions elab) + InstallPlan.PreExisting _ -> False + relocatable + | any isReloc (InstallPlan.toList elaboratedPlan) = Relocatable + | otherwise = NotRelocatable writePlanExternalRepresentation + relocatable distDirLayout elaboratedPlan elaboratedShared diff --git a/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs b/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs index 89fc0e54bdf..899282310e7 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanning/Types.hs @@ -36,6 +36,7 @@ module Distribution.Client.ProjectPlanning.Types , BuildStyle (..) , MemoryOrDisk (..) , isInplaceBuildStyle + , Relocatable (..) , CabalFileText , NotPerComponentReason (..) , NotPerComponentBuildType (..) @@ -829,6 +830,19 @@ isInplaceBuildStyle :: BuildStyle -> Bool isInplaceBuildStyle (BuildInplaceOnly{}) = True isInplaceBuildStyle BuildAndInstall = False +-- | Whether generated build metadata should use project-relative paths, +-- so that the whole build tree can be relocated to a +-- different directory, or absolute paths (the traditional default). +data Relocatable + = -- | Default behavior + NotRelocatable + | -- | All build artifacts use relative paths + Relocatable + deriving (Eq, Ord, Show, Generic) + +instance Binary Relocatable +instance Structured Relocatable + instance Binary MemoryOrDisk instance Structured MemoryOrDisk diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/app/Main.hs b/cabal-testsuite/PackageTests/RelocatableReuse/app/Main.hs new file mode 100644 index 00000000000..296ed0f5d84 --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import MyLib (greeting) + +main :: IO () +main = putStrLn greeting diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/cabal.project b/cabal-testsuite/PackageTests/RelocatableReuse/cabal.project new file mode 100644 index 00000000000..e6fdbadb439 --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/cabal.project @@ -0,0 +1 @@ +packages: . diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/data/hello.txt b/cabal-testsuite/PackageTests/RelocatableReuse/data/hello.txt new file mode 100644 index 00000000000..ff457dc6f2c --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/data/hello.txt @@ -0,0 +1 @@ +hello data diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/relocatable-reuse.cabal b/cabal-testsuite/PackageTests/RelocatableReuse/relocatable-reuse.cabal new file mode 100644 index 00000000000..0bc79bf5aca --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/relocatable-reuse.cabal @@ -0,0 +1,17 @@ +cabal-version: 2.4 +name: relocatable-reuse +version: 0.1.0.0 +build-type: Simple +data-files: data/hello.txt + +library + exposed-modules: MyLib + hs-source-dirs: src + build-depends: base + default-language: Haskell2010 + +executable rr-exe + main-is: Main.hs + hs-source-dirs: app + build-depends: base, relocatable-reuse + default-language: Haskell2010 diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/relocate.test.hs b/cabal-testsuite/PackageTests/RelocatableReuse/relocate.test.hs new file mode 100644 index 00000000000..408e33640cb --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/relocate.test.hs @@ -0,0 +1,50 @@ +import Test.Cabal.Prelude +import Test.Cabal.Monad (testSourceCopyDir, testVerbosity) + +import Control.Monad.Trans.Reader (withReaderT) +import Distribution.Simple.Utils (copyDirectoryRecursive) +import System.Directory (copyFile, createDirectoryIfMissing) + +-- Copying the whole project (sources + dist tree) to a different absolute +-- location and rebuilding with @--enable-relocatable@ must reuse the existing +-- build artifacts rather than recompiling. +-- +-- The dist tree is kept INSIDE the project (a relative @--builddir@) so that +-- relocating the whole tree preserves the relationship between the sources and +-- the build directory — which is what a real relocation looks like (the +-- default @dist-newstyle@ lives inside the project). The config file monitor +-- keys off paths relative to the build dir, so this relationship being +-- preserved is what makes the relocated build reuse the artifacts. +main = do + skipIfWindows "does not support relocatable builds" + cabalTest $ recordMode DoNotRecord $ do + env <- getTestEnv + let v = testVerbosity env + root = testCurrentDir env + origSrc = testSourceCopyDir env "orig" + relocSrc = testSourceCopyDir env "relocated" + -- A relative --builddir places the dist tree inside the project dir. + relocBuild = ["--enable-relocatable", "--builddir=dist-reloc"] + copyPkg from to = liftIO $ do + createDirectoryIfMissing True to + copyFile (from "relocatable-reuse.cabal") (to "relocatable-reuse.cabal") + copyFile (from "cabal.project") (to "cabal.project") + copyDirectoryRecursive v (from "src") (to "src") + copyDirectoryRecursive v (from "app") (to "app") + copyDirectoryRecursive v (from "data") (to "data") + + -- Build the package in the 'orig' subdirectory; dist-reloc is created + -- inside it. + copyPkg root origSrc + withReaderT (\e -> e{testRelativeCurrentDir = "orig"}) $ + cabal "build" relocBuild + + -- Relocate: copy the whole tree (sources + the inner dist-reloc) to a + -- different absolute location. + liftIO $ copyDirectoryRecursive v origSrc relocSrc + + -- Building in the relocated location must find everything up to date. + withReaderT (\e -> e{testRelativeCurrentDir = "relocated"}) $ do + r <- cabal' "build" relocBuild + assertOutputContains "Up to date" r + assertOutputDoesNotContain "Compiling" r diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/src/MyLib.hs b/cabal-testsuite/PackageTests/RelocatableReuse/src/MyLib.hs new file mode 100644 index 00000000000..bd241821b7b --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/src/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (greeting) where + +greeting :: String +greeting = "hello from relocatable lib" diff --git a/cabal-testsuite/PackageTests/RelocatableReuse/toggle.test.hs b/cabal-testsuite/PackageTests/RelocatableReuse/toggle.test.hs new file mode 100644 index 00000000000..a40ee04546e --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableReuse/toggle.test.hs @@ -0,0 +1,13 @@ +import Test.Cabal.Prelude +import Data.Foldable (for_) + +-- Toggling @--enable-relocatable@ on or off must not cause a rebuild +main = do + skipIfWindows "does not support relocatable builds" + cabalTest $ recordMode DoNotRecord $ do + cabal "build" ["--enable-relocatable"] + + for_ [["--enable-relocatable"], []] $ \flag -> do + r <- cabal' "build" flag + assertOutputContains "Up to date" r + assertOutputDoesNotContain "Compiling" r diff --git a/changelog.d/relocatable-build-tree.md b/changelog.d/relocatable-build-tree.md new file mode 100644 index 00000000000..6b5eb3750aa --- /dev/null +++ b/changelog.d/relocatable-build-tree.md @@ -0,0 +1,23 @@ +--- +synopsis: Make build tree relocatable with `--enable-relocatable` +packages: [Cabal, cabal-install] +significance: significant +prs: 12145 +issues: 12137 +--- + +When using the flag `--enable-relocatable` (or `relocatable: True` configuration in a project file), all paths in the build tree (e.g. `dist-newstyle`) are relativized to the build tree root. + +In practice, this means that the build tree can be moved to another codebase, and re-used. This is useful when creating worktrees with `git`, to minimize the amount of re-builds of local packages. This is particularly useful for users leveraging agentic workflows. + +For example: + +```txt +$ cabal build --enable-relocatable +Up to date. +$ git worktree add ../new-feature +$ cp -R dist-newstyle ../new-feature +$ cd ../new-feature +$ cabal build --enable-relocatable +Up to date. +``` diff --git a/doc/cabal-project-description-file.rst b/doc/cabal-project-description-file.rst index 8f7639ee1b8..7b54fa033d7 100644 --- a/doc/cabal-project-description-file.rst +++ b/doc/cabal-project-description-file.rst @@ -1294,17 +1294,37 @@ Dynamic linking options The command line variant of this flag is ``--enable-library-bytecode`` and ``--disable-library-bytecode``. -.. cfg-field:: relocatable: - --relocatable - :synopsis: Build relocatable package. - :since: 1.22 +.. cfg-field:: relocatable: boolean + --enable-relocatable + --disable-relocatable + :synopsis: Make the build tree relocatable. + :since: 3.18 :default: False - :strike:`Build a package which is relocatable.` (TODO: It is not - clear what this actually does, or if it works at all.) - - The command line variant of this flag is ``--relocatable``. + Build the package so that its build tree can be relocated. The + absolute paths that Cabal would otherwise bake into the build tree are + made relative to the build tree root (e.g. ``dist-newstyle``) instead. + This also changes ``plan.json``: the filesystem paths recorded therein + are relative to the build tree root. + + In practice this means the whole build tree can be copied elsewhere + (for example into a ``git`` worktree) next to the same sources and + reused without rebuilding the local packages:: + + $ cabal build --enable-relocatable + $ git worktree add ../my-feature + $ cp -R dist-newstyle ../my-feature/ + $ cd ../my-feature + $ cabal build --enable-relocatable + Up to date + + This feature relies on ``RPATH`` support and is currently only + available with GHC on Linux, macOS and FreeBSD; it is not supported on + Windows. + + The command line variant of this flag is ``--enable-relocatable`` and + ``--disable-relocatable``. Static linking options ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/nix-local-build.rst b/doc/nix-local-build.rst index 312778f8d47..1e66c75373b 100644 --- a/doc/nix-local-build.rst +++ b/doc/nix-local-build.rst @@ -104,6 +104,33 @@ for each package using :cfg-field:`profiling-detail`:: Alternately, you can call ``cabal build --enable-profiling`` to temporarily build with profiling. +How can I reuse a build tree in another directory (e.g. a ``git`` worktree)? +---------------------------------------------------------------------------- + +By default the build tree (``dist-newstyle``) records absolute paths, so +copying it to a different location and building there rebuilds the local +packages from scratch. Building with :cfg-field:`relocatable` makes the +build tree relocatable — all of the project-local paths it records are +made relative to the build tree root — so it can be moved next to the same +sources and reused:: + + $ cabal build --enable-relocatable + $ git worktree add ../my-feature + $ cp -R dist-newstyle ../my-feature/ + $ cd ../my-feature + $ cabal build --enable-relocatable + Up to date + +This is handy for keeping several worktrees of the same project warm +without recompiling shared local packages in each one. If your development +workflow is heavily worktree-based, we recommend adding the following to +your project file:: + + relocatable: True + +This feature relies on ``RPATH`` support and is only available with GHC on Linux, macOS and +FreeBSD (not Windows); see :cfg-field:`relocatable` for details. + .. _how reproducible: How can I have a reproducible set of versions for my dependencies? @@ -297,6 +324,14 @@ this folder (the most important two are first): Haskell data structure as well as an example tool showing possible applications. + The filesystem paths in ``plan.json`` (such as ``dist-dir``, + ``bin-file`` and the local ``pkg-src`` path) are absolute by default. + When the plan is built with :cfg-field:`relocatable` (that is, + ``--enable-relocatable``), the project-local paths are instead written + relative to the build tree root, so the plan stays valid if the build + tree is moved. Paths outside the build tree (such as the global store) + remain absolute in either case. + .. todo:: Document JSON schema (including version history of schema)