diff --git a/Cabal-tests/tests/UnitTests/Distribution/Utils/Structured.hs b/Cabal-tests/tests/UnitTests/Distribution/Utils/Structured.hs index a6b482038cf..734555573d1 100644 --- a/Cabal-tests/tests/UnitTests/Distribution/Utils/Structured.hs +++ b/Cabal-tests/tests/UnitTests/Distribution/Utils/Structured.hs @@ -37,4 +37,4 @@ md5CheckGenericPackageDescription proxy = md5Check proxy md5CheckLocalBuildInfo :: Proxy LocalBuildInfo -> Assertion md5CheckLocalBuildInfo proxy = md5Check proxy - 0xd9542841ab7584d348aa142053fe934f + 0xa7235f257a140c5d1af756c4c81f4aed 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/Configure.hs b/Cabal/src/Distribution/Simple/Configure.hs index 53c3b35caa0..5ebbaa80825 100644 --- a/Cabal/src/Distribution/Simple/Configure.hs +++ b/Cabal/src/Distribution/Simple/Configure.hs @@ -907,6 +907,7 @@ buildOptionsFromConfigFlags verbosity cfg comp = do , exeCoverage = False , libCoverage = False , relocatable = fromFlagOrDefault False $ configRelocatable cfg + , relativeBuildTree = fromFlagOrDefault False $ configRelativeBuildTree cfg , programPrefix = flagToMaybe $ configProgPrefix cfg , programSuffix = flagToMaybe $ configProgSuffix cfg } 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..643212e09a7 100644 --- a/Cabal/src/Distribution/Simple/Register.hs +++ b/Cabal/src/Distribution/Simple/Register.hs @@ -151,6 +151,7 @@ generateOne verbHandles pkg lib lbi clbi regFlags = clbi inplace reloc + relBuildTree distPref (registrationPackageDB absPackageDBs) info verbosity (IPI.showInstalledPackageInfo installedPkgInfo) @@ -159,6 +160,7 @@ generateOne verbHandles pkg lib lbi clbi regFlags = common = registerCommonFlags regFlags inplace = fromFlag (regInPlace regFlags) reloc = relocatable lbi + relBuildTree = relativeBuildTree lbi -- FIXME: there's really no guarantee this will work. -- registering into a totally different db stack can -- fail if dependencies cannot be satisfied. @@ -270,17 +272,29 @@ generateRegistrationInfo -> ComponentLocalBuildInfo -> Bool -> Bool + -> Bool + -- ^ Relocatable build tree -> SymbolicPath Pkg (Dir Dist) -> PackageDB -> IO InstalledPackageInfo -generateRegistrationInfo verbosity pkg lib lbi clbi inplace reloc distPref packageDb = do +generateRegistrationInfo verbosity pkg lib lbi clbi inplace reloc relBuildTree distPref packageDb = do 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 relative build tree 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 relBuildTree && compilerFlavor (compiler lbi) == GHC + then Just <$> GHC.pkgRoot verbosity lbi packageDb + else return Nothing return ( inplaceInstalledPackageInfo inplaceDir @@ -290,6 +304,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 +636,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 +653,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/src/Distribution/Simple/Setup/Config.hs b/Cabal/src/Distribution/Simple/Setup/Config.hs index 839241eed0f..e1f349d4819 100644 --- a/Cabal/src/Distribution/Simple/Setup/Config.hs +++ b/Cabal/src/Distribution/Simple/Setup/Config.hs @@ -209,6 +209,8 @@ data ConfigFlags = ConfigFlags -- ^ Halt and show an error message indicating an error in flag assignment , configRelocatable :: Flag Bool -- ^ Enable relocatable package built + , configRelativeBuildTree :: Flag Bool + -- ^ Make the build tree relocatable , configDebugInfo :: Flag DebugInfoLevel -- ^ Emit debug info. , configDumpBuildInfo :: Flag DumpBuildInfo @@ -320,6 +322,7 @@ instance Eq ConfigFlags where && equal configExactConfiguration && equal configFlagError && equal configRelocatable + && equal configRelativeBuildTree && equal configDebugInfo && equal configDumpBuildInfo && equal configUseResponseFiles @@ -368,6 +371,7 @@ defaultConfigFlags progDb = , configExactConfiguration = Flag False , configFlagError = NoFlag , configRelocatable = Flag False + , configRelativeBuildTree = Flag False , configDebugInfo = Flag NoDebugInfo , configDumpBuildInfo = NoFlag , configUseResponseFiles = NoFlag @@ -838,6 +842,13 @@ configureOptions showOrParseArgs = configRelocatable (\v flags -> flags{configRelocatable = v}) (boolOpt [] []) + , option + "" + ["relative-build-tree"] + "recording project-local paths relative to the build tree root, so the build tree can be relocated. (GHC only)" + configRelativeBuildTree + (\v flags -> flags{configRelativeBuildTree = v}) + (boolOpt' ([], ["relative-build-tree"]) ([], ["no-relative-build-tree"])) , option "" ["response-files"] diff --git a/Cabal/src/Distribution/Types/LocalBuildConfig.hs b/Cabal/src/Distribution/Types/LocalBuildConfig.hs index 984cbc18161..99b4ebe087d 100644 --- a/Cabal/src/Distribution/Types/LocalBuildConfig.hs +++ b/Cabal/src/Distribution/Types/LocalBuildConfig.hs @@ -185,6 +185,8 @@ data BuildOptions = BuildOptions -- ^ Whether to enable library program coverage , relocatable :: Bool -- ^ Whether to build a relocatable package + , relativeBuildTree :: Bool + -- ^ Whether to make the build tree relocatable , programPrefix :: Maybe PathTemplate -- ^ Installed executable prefix , programSuffix :: Maybe PathTemplate @@ -224,6 +226,7 @@ buildOptionsConfigFlags (BuildOptions{..}) = , configCoverage = toFlag exeCoverage , configLibCoverage = mempty , configRelocatable = toFlag relocatable + , configRelativeBuildTree = toFlag relativeBuildTree , configOptimization = toFlag withOptimization , configSplitSections = toFlag splitSections , configSplitObjs = toFlag splitObjs diff --git a/Cabal/src/Distribution/Types/LocalBuildInfo.hs b/Cabal/src/Distribution/Types/LocalBuildInfo.hs index 7e91a5ebeb2..812120eccd2 100644 --- a/Cabal/src/Distribution/Types/LocalBuildInfo.hs +++ b/Cabal/src/Distribution/Types/LocalBuildInfo.hs @@ -43,6 +43,7 @@ module Distribution.Types.LocalBuildInfo , libCoverage , extraCoverageFor , relocatable + , relativeBuildTree , programPrefix , programSuffix , .. @@ -186,6 +187,7 @@ pattern LocalBuildInfo -> Bool -> [UnitId] -> Bool + -> Bool -> Maybe PathTemplate -> Maybe PathTemplate -> LocalBuildInfo @@ -227,6 +229,7 @@ pattern LocalBuildInfo , libCoverage , extraCoverageFor , relocatable + , relativeBuildTree , programPrefix , programSuffix } = @@ -281,6 +284,7 @@ pattern LocalBuildInfo , exeCoverage , libCoverage , relocatable + , relativeBuildTree , programPrefix , programSuffix } diff --git a/cabal-install/src/Distribution/Client/Config.hs b/cabal-install/src/Distribution/Client/Config.hs index c4c5a5dfba5..8ff597612b0 100644 --- a/cabal-install/src/Distribution/Client/Config.hs +++ b/cabal-install/src/Distribution/Client/Config.hs @@ -542,6 +542,7 @@ instance Semigroup SavedConfig where , configExactConfiguration = combine configExactConfiguration , configFlagError = combine configFlagError , configRelocatable = combine configRelocatable + , configRelativeBuildTree = combine configRelativeBuildTree , configUseResponseFiles = combine configUseResponseFiles , configDumpBuildInfo = combine configDumpBuildInfo , configAllowDependingOnPrivateLibs = diff --git a/cabal-install/src/Distribution/Client/ProjectBuilding/PackageFileMonitor.hs b/cabal-install/src/Distribution/Client/ProjectBuilding/PackageFileMonitor.hs index f7ddca60251..729c6d451cb 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,9 @@ packageFileMonitorKeyValues elab = , elabTestKeepTix = False , elabTestTestOptions = [] , elabBenchmarkOptions = [] + , -- Whether a relative build tree was requested only affects how paths + -- are recorded in generated metadata, not what is compiled, so null it here. + elabBuildOptions = (elabBuildOptions elab){LBC.relativeBuildTree = False} } -- The second part is the value used to guard the build step. So this is @@ -209,11 +262,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 +278,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/ProjectConfig/FieldGrammar.hs b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs index b8c858682aa..00a6b40a018 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/FieldGrammar.hs @@ -145,6 +145,7 @@ packageConfigFieldGrammar knownPrograms = do packageConfigBenchmarks <- optionalFieldDef "benchmarks" L.packageConfigBenchmarks mempty packageConfigCoverage <- packageConfigCoverageGrammar packageConfigRelocatable <- optionalFieldDef "relocatable" L.packageConfigRelocatable mempty + packageConfigRelativeBuildTree <- optionalFieldDef "relative-build-tree" L.packageConfigRelativeBuildTree mempty packageConfigDebugInfo <- optionalFieldDef "debug-info" L.packageConfigDebugInfo mempty packageConfigDumpBuildInfo <- optionalFieldDef "build-info" L.packageConfigDumpBuildInfo mempty packageConfigRunTests <- optionalFieldDef "run-tests" L.packageConfigRunTests mempty diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs index bf47efd8a2e..01d61de8fe0 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Legacy.hs @@ -777,6 +777,7 @@ convertLegacyPerPackageFlags , configDebugInfo = packageConfigDebugInfo , configDumpBuildInfo = packageConfigDumpBuildInfo , configRelocatable = packageConfigRelocatable + , configRelativeBuildTree = packageConfigRelativeBuildTree , configCoverageFor = _ } = configFlags packageConfigExtraLibDirs = fmap getSymbolicPath $ configExtraLibDirs configFlags @@ -1099,6 +1100,7 @@ convertToLegacyAllPackageConfig , configBenchmarks = mempty , configFlagError = mempty -- TODO: ??? , configRelocatable = mempty + , configRelativeBuildTree = mempty , configDebugInfo = mempty , configUseResponseFiles = mempty , configDumpBuildInfo = mempty @@ -1177,6 +1179,7 @@ convertToLegacyPerPackageConfig PackageConfig{..} = , configBenchmarks = packageConfigBenchmarks , configFlagError = mempty -- TODO: ??? , configRelocatable = packageConfigRelocatable + , configRelativeBuildTree = packageConfigRelativeBuildTree , configDebugInfo = packageConfigDebugInfo , configUseResponseFiles = mempty , configDumpBuildInfo = packageConfigDumpBuildInfo @@ -1563,6 +1566,7 @@ legacyPackageConfigFieldDescrs = , "coverage" , "library-coverage" , "relocatable" + , "relative-build-tree" -- not "extra-include-dirs", "extra-lib-dirs", "extra-framework-dirs" -- or "extra-prog-path". We use corrected ones above that parse -- as list fields. diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Lens.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Lens.hs index 69d4e5e3aed..dbdef35df82 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Lens.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Lens.hs @@ -440,6 +440,10 @@ packageConfigRelocatable :: Lens' PackageConfig (Flag Bool) packageConfigRelocatable f s = fmap (\x -> s{T.packageConfigRelocatable = x}) (f (T.packageConfigRelocatable s)) {-# INLINEABLE packageConfigRelocatable #-} +packageConfigRelativeBuildTree :: Lens' PackageConfig (Flag Bool) +packageConfigRelativeBuildTree f s = fmap (\x -> s{T.packageConfigRelativeBuildTree = x}) (f (T.packageConfigRelativeBuildTree s)) +{-# INLINEABLE packageConfigRelativeBuildTree #-} + packageConfigDebugInfo :: Lens' PackageConfig (Flag DebugInfoLevel) packageConfigDebugInfo f s = fmap (\x -> s{T.packageConfigDebugInfo = x}) (f (T.packageConfigDebugInfo s)) {-# INLINEABLE packageConfigDebugInfo #-} diff --git a/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs b/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs index 09d67a319e2..d7f5e14919f 100644 --- a/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs +++ b/cabal-install/src/Distribution/Client/ProjectConfig/Types.hs @@ -311,6 +311,7 @@ data PackageConfig = PackageConfig , packageConfigBenchmarks :: Flag Bool , packageConfigCoverage :: Flag Bool , packageConfigRelocatable :: Flag Bool + , packageConfigRelativeBuildTree :: Flag Bool , packageConfigDebugInfo :: Flag DebugInfoLevel , packageConfigDumpBuildInfo :: Flag DumpBuildInfo , packageConfigRunTests :: Flag Bool -- TODO: [required eventually] use this 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..0d6e2d69c2b 100644 --- a/cabal-install/src/Distribution/Client/ProjectPlanning.hs +++ b/cabal-install/src/Distribution/Client/ProjectPlanning.hs @@ -981,7 +981,15 @@ rebuildInstallPlan -> Rebuild () phaseMaintainPlanOutputs elaboratedPlan elaboratedShared = liftIO $ do debug verbosity "Updating plan.json" + let isRelBuildTree pp = case pp of + InstallPlan.Configured elab -> LBC.relativeBuildTree (elabBuildOptions elab) + InstallPlan.Installed elab -> LBC.relativeBuildTree (elabBuildOptions elab) + InstallPlan.PreExisting _ -> False + relocatable + | any isRelBuildTree (InstallPlan.toList elaboratedPlan) = Relocatable + | otherwise = NotRelocatable writePlanExternalRepresentation + relocatable distDirLayout elaboratedPlan elaboratedShared @@ -2389,6 +2397,7 @@ elaborateInstallPlan , stripExes = perPkgOptionFlag pkgid False packageConfigStripExes , withDebugInfo = perPkgOptionFlag pkgid NoDebugInfo packageConfigDebugInfo , relocatable = perPkgOptionFlag pkgid False packageConfigRelocatable + , relativeBuildTree = perPkgOptionFlag pkgid False packageConfigRelativeBuildTree , withProfLibDetail = elabProfExeDetail , withProfExeDetail = elabProfLibDetail , programPrefix = elabProgPrefix @@ -4129,6 +4138,7 @@ setupHsConfigureFlags , configCoverage , configLibCoverage , configRelocatable + , configRelativeBuildTree , configOptimization , configSplitSections , configSplitObjs 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-install/src/Distribution/Client/Setup.hs b/cabal-install/src/Distribution/Client/Setup.hs index 22ea26850d1..2a0dd8ae174 100644 --- a/cabal-install/src/Distribution/Client/Setup.hs +++ b/cabal-install/src/Distribution/Client/Setup.hs @@ -713,6 +713,8 @@ filterConfigureFlags' flags cabalLibVersion flags_latest { configBytecodeLib = NoFlag , configInstallDirs = (configInstallDirs flags){bytecodelibdir = NoFlag} + , -- Cabal < 3.17 doesn't know about '--relative-build-tree' + configRelativeBuildTree = NoFlag } flags_3_13_0 = diff --git a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs index b442ea2e5a9..38c3780f9cd 100644 --- a/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs +++ b/cabal-install/tests/UnitTests/Distribution/Client/ProjectConfig.hs @@ -736,6 +736,7 @@ instance Arbitrary PackageConfig where <*> arbitrary <*> arbitrary <*> arbitrary + <*> arbitrary <*> arbitraryFlag arbitraryShortToken <*> arbitrary <*> arbitrary @@ -804,6 +805,7 @@ instance Arbitrary PackageConfig where , packageConfigBenchmarks = x24 , packageConfigCoverage = x25 , packageConfigRelocatable = x26 + , packageConfigRelativeBuildTree = x60 , packageConfigDebugInfo = x27 , packageConfigDumpBuildInfo = x27_1 , packageConfigRunTests = x28 @@ -870,6 +872,7 @@ instance Arbitrary PackageConfig where , packageConfigBenchmarks = x24' , packageConfigCoverage = x25' , packageConfigRelocatable = x26' + , packageConfigRelativeBuildTree = x60' , packageConfigDebugInfo = x27' , packageConfigDumpBuildInfo = x27_1' , packageConfigRunTests = x28' @@ -908,7 +911,7 @@ instance Arbitrary PackageConfig where , (x15', x16', x53', x17', x18', x19') ) , ( (x20', x20_1', x21', x22', x23', x24') - , (x25', x26', x27', x27_1', x28', x29') + , (x25', x26', x60', x27', x27_1', x28', x29') , (x30', x31', x32', (x33', x33_1'), x34') , (x35', x36', x37', x38', x59', x39') , (x40', x41') @@ -934,7 +937,7 @@ instance Arbitrary PackageConfig where ) , ( (x20, x20_1, x21, x22, x23, x24) - , (x25, x26, x27, x27_1, x28, x29) + , (x25, x26, x60, x27, x27_1, x28, x29) , (x30, x31, x32, (x33, x33_1), x34) , (x35, x36, fmap NonEmpty x37, x38, x59, fmap NonEmpty x39) , (x40, x41) diff --git a/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/app/Main.hs b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/app/Main.hs new file mode 100644 index 00000000000..296ed0f5d84 --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/app/Main.hs @@ -0,0 +1,6 @@ +module Main where + +import MyLib (greeting) + +main :: IO () +main = putStrLn greeting diff --git a/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/cabal.project b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/cabal.project new file mode 100644 index 00000000000..e6fdbadb439 --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/cabal.project @@ -0,0 +1 @@ +packages: . diff --git a/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/data/hello.txt b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/data/hello.txt new file mode 100644 index 00000000000..ff457dc6f2c --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/data/hello.txt @@ -0,0 +1 @@ +hello data diff --git a/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/relocatable-reuse.cabal b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/relocatable-reuse.cabal new file mode 100644 index 00000000000..0bc79bf5aca --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/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/RelocatableBuildTreeReuse/relocate.test.hs b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/relocate.test.hs new file mode 100644 index 00000000000..e446c145c76 --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/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 @--relative-build-tree@ 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 "build-tree relocation is not verified on Windows" + 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 = ["--relative-build-tree", "--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/RelocatableBuildTreeReuse/src/MyLib.hs b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/src/MyLib.hs new file mode 100644 index 00000000000..bd241821b7b --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/src/MyLib.hs @@ -0,0 +1,4 @@ +module MyLib (greeting) where + +greeting :: String +greeting = "hello from relocatable lib" diff --git a/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/toggle.test.hs b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/toggle.test.hs new file mode 100644 index 00000000000..d890fa5e49e --- /dev/null +++ b/cabal-testsuite/PackageTests/RelocatableBuildTreeReuse/toggle.test.hs @@ -0,0 +1,14 @@ +import Test.Cabal.Prelude +import Data.Foldable (for_) + +-- Toggling @--relative-build-tree@ on or off must not cause a rebuild: it only +-- affects how paths are recorded, not what is compiled. +main = do + skipIfWindows "build-tree relocation is not verified on Windows" + cabalTest $ recordMode DoNotRecord $ do + cabal "build" ["--relative-build-tree"] + + for_ [["--relative-build-tree"], []] $ \flag -> do + r <- cabal' "build" flag + assertOutputContains "Up to date" r + assertOutputDoesNotContain "Compiling" r diff --git a/changelog.d/relative-build-tree.md b/changelog.d/relative-build-tree.md new file mode 100644 index 00000000000..ea15b352d4b --- /dev/null +++ b/changelog.d/relative-build-tree.md @@ -0,0 +1,23 @@ +--- +synopsis: Make build tree relocatable with `--relative-build-tree` +packages: [Cabal, cabal-install] +significance: significant +prs: 12145 +issues: 12137 +--- + +When using the flag `--relative-build-tree` (or `relative-build-tree: True` configuration in a project file), all project-local 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 --relative-build-tree +Up to date. +$ git worktree add ../new-feature +$ cp -R dist-newstyle ../new-feature +$ cd ../new-feature +$ cabal build --relative-build-tree +Up to date. +``` diff --git a/doc/cabal-project-description-file.rst b/doc/cabal-project-description-file.rst index 8f7639ee1b8..dcd7546ebff 100644 --- a/doc/cabal-project-description-file.rst +++ b/doc/cabal-project-description-file.rst @@ -1306,6 +1306,36 @@ Dynamic linking options The command line variant of this flag is ``--relocatable``. +.. cfg-field:: relative-build-tree: boolean + --relative-build-tree + :synopsis: Make the build tree relocatable. + :since: 3.18 + + :default: False + + Make the build tree relocatable. 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: + + - the inplace package registration files (``.conf``) use the + ``${pkgroot}`` variable rather than absolute directories, + - the filesystem paths recorded in ``plan.json`` 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 --relative-build-tree + $ git worktree add ../my-feature + $ cp -R dist-newstyle ../my-feature/ + $ cd ../my-feature + $ cabal build --relative-build-tree + Up to date + + The command line variant of this flag is ``--relative-build-tree`` (and + ``--no-relative-build-tree`` to disable). + Static linking options ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/nix-local-build.rst b/doc/nix-local-build.rst index 312778f8d47..c9d36dd7cb3 100644 --- a/doc/nix-local-build.rst +++ b/doc/nix-local-build.rst @@ -104,6 +104,31 @@ 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:`relative-build-tree` 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 --relative-build-tree + $ git worktree add ../my-feature + $ cp -R dist-newstyle ../my-feature/ + $ cd ../my-feature + $ cabal build --relative-build-tree + 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:: + + relative-build-tree: True + + .. _how reproducible: How can I have a reproducible set of versions for my dependencies? @@ -297,6 +322,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:`relative-build-tree` (that is, + ``--relative-build-tree``), 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)