From cbfd83724797a51509ef61ad9eaa8d81713ec57d Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Thu, 5 Mar 2026 17:00:32 +0900 Subject: [PATCH 1/3] Add -plugin-package-db for cross-compilation plugin support When cross-compiling (host != target), plugins are built for the host platform but GHC's UnitState contains target packages. Loading target libraries into GHC's host process fails. This adds a -plugin-package-db flag that points GHC at host-side package databases. When specified, a separate UnitState is built from these databases for plugin loading, while TH/bytecode continues to use the target UnitState via the external interpreter. Architecture: No -plugin-package-db: plugins use InternalInterp + existing target UnitState (unchanged) With -plugin-package-db: plugins use InternalInterp + host plugin UnitState (from host DBs) TH/GHCi uses ExternalInterp + target UnitState (unchanged) Changes: - DynFlags: add pluginPackageDBFlags field - Session: parse -plugin-package-db and -clear-plugin-package-dbs flags - Unit/State: add initPluginUnitConfig using hostPlatformArchOS - Driver/Env: add hscSetCurrentUnitState helper - Runtime/Loader: extend withPluginInterp to swap UnitState when plugin DBs are configured; replace checkExternalInterpreter with withPluginInterp (creates local InternalInterp for plugin loading alongside ExternalInterp for TH, see #14335) --- compiler/GHC/Driver/DynFlags.hs | 7 +++ compiler/GHC/Driver/Env.hs | 10 +++++ compiler/GHC/Driver/Session.hs | 12 +++++ compiler/GHC/Runtime/Loader.hs | 79 ++++++++++++++++++++++++++++----- compiler/GHC/Unit/State.hs | 36 +++++++++++++++ 5 files changed, 132 insertions(+), 12 deletions(-) diff --git a/compiler/GHC/Driver/DynFlags.hs b/compiler/GHC/Driver/DynFlags.hs index 4cadaac475de..dac054e16fcd 100644 --- a/compiler/GHC/Driver/DynFlags.hs +++ b/compiler/GHC/Driver/DynFlags.hs @@ -363,6 +363,11 @@ data DynFlags = DynFlags { pluginPackageFlags :: [PackageFlag], -- ^ The @-plugin-package-id@ flags from command line. -- In *reverse* order that they're specified on the command line. + pluginPackageDBFlags :: [PackageDBFlag], + -- ^ The @-plugin-package-db@ flags from command line. + -- When non-empty, a separate 'UnitState' is built from these + -- databases for plugin loading (host packages for cross-compilation). + -- In *reverse* order that they're specified on the command line. trustFlags :: [TrustFlag], -- ^ The @-trust@ and @-distrust@ flags. -- In *reverse* order that they're specified on the command line. @@ -687,6 +692,7 @@ defaultDynFlags mySettings = packageDBFlags = [], packageFlags = [], pluginPackageFlags = [], + pluginPackageDBFlags = [], ignorePackageFlags = [], trustFlags = [], packageEnv = Nothing, @@ -941,6 +947,7 @@ packageFlagsChanged idflags1 idflags0 = packageFlags idflags1 /= packageFlags idflags0 || ignorePackageFlags idflags1 /= ignorePackageFlags idflags0 || pluginPackageFlags idflags1 /= pluginPackageFlags idflags0 || + pluginPackageDBFlags idflags1 /= pluginPackageDBFlags idflags0 || trustFlags idflags1 /= trustFlags idflags0 || packageDBFlags idflags1 /= packageDBFlags idflags0 || packageGFlags idflags1 /= packageGFlags idflags0 diff --git a/compiler/GHC/Driver/Env.hs b/compiler/GHC/Driver/Env.hs index 1357d86b0da1..75e2be96d8e6 100644 --- a/compiler/GHC/Driver/Env.hs +++ b/compiler/GHC/Driver/Env.hs @@ -15,6 +15,7 @@ module GHC.Driver.Env , hsc_all_home_unit_ids , hscUpdateLoggerFlags , hscUpdateHUG + , hscSetCurrentUnitState , hscInsertHPT , hscSetActiveHomeUnit , hscSetActiveUnitId @@ -144,6 +145,15 @@ hscInsertHPT hmi hsc_env = UnitEnv.insertHpt hmi (hsc_unit_env hsc_env) hscUpdateHUG :: (HomeUnitGraph -> HomeUnitGraph) -> HscEnv -> HscEnv hscUpdateHUG f hsc_env = hsc_env { hsc_unit_env = updateHug f (hsc_unit_env hsc_env) } +-- | Replace the 'UnitState' in the current 'HomeUnitEnv'. +-- Used by plugin loading to swap in a host-side 'UnitState' built from +-- @-plugin-package-db@ databases during cross-compilation. +hscSetCurrentUnitState :: UnitState -> HscEnv -> HscEnv +hscSetCurrentUnitState us hsc_env = + let uid = ue_currentUnit (hsc_unit_env hsc_env) + upd hue = hue { HUG.homeUnitEnv_units = us } + in hscUpdateHUG (HUG.unitEnv_adjust upd uid) hsc_env + setModuleGraph :: ModuleGraph -> HscEnv -> HscEnv setModuleGraph mod_graph hsc_env = hsc_env { hsc_unit_env = (hsc_unit_env hsc_env) { ue_module_graph = mod_graph } } diff --git a/compiler/GHC/Driver/Session.hs b/compiler/GHC/Driver/Session.hs index 385a02e04375..224b6711fa11 100644 --- a/compiler/GHC/Driver/Session.hs +++ b/compiler/GHC/Driver/Session.hs @@ -2154,6 +2154,8 @@ package_flags_deps = [ (NoArg (setGeneralFlag Opt_HideAllPackages)) , make_ord_flag defFlag "hide-all-plugin-packages" (NoArg (setGeneralFlag Opt_HideAllPluginPackages)) + , make_ord_flag defFlag "plugin-package-db" (HasArg addPluginPkgDb) + , make_ord_flag defFlag "clear-plugin-package-dbs" (NoArg clearPluginPkgDbs) , make_ord_flag defFlag "package-env" (HasArg setPackageEnv) , make_ord_flag defFlag "ignore-package" (HasArg ignorePackage) , make_dep_flag defFlag "syslib" (HasArg exposePackage) "Use -package instead" @@ -3175,6 +3177,16 @@ exposePluginPackage p = exposePluginPackageId p = upd (\s -> s{ pluginPackageFlags = parsePackageFlag "-plugin-package-id" parseUnitArg p : pluginPackageFlags s }) + +addPluginPkgDb :: String -> DynP () +addPluginPkgDb path = + upd (\s -> s{ pluginPackageDBFlags = + PackageDB (PkgDbPath path) : pluginPackageDBFlags s }) + +clearPluginPkgDbs :: DynP () +clearPluginPkgDbs = + upd (\s -> s{ pluginPackageDBFlags = ClearPackageDBs : pluginPackageDBFlags s }) + hidePackage p = upd (\s -> s{ packageFlags = HidePackage p : packageFlags s }) ignorePackage p = diff --git a/compiler/GHC/Runtime/Loader.hs b/compiler/GHC/Runtime/Loader.hs index 6f998ad150f4..0c4e121849f9 100644 --- a/compiler/GHC/Runtime/Loader.hs +++ b/compiler/GHC/Runtime/Loader.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE CPP #-} + -- | Dynamically lookup up values from modules and loading them. -- -- NOTE: This module is only compiled when flag(interpreter) is enabled @@ -57,6 +59,7 @@ import GHC.Types.Name.Reader import GHC.Types.Unique.DFM import GHC.Unit.Finder ( findPluginModule, FindResult(..) ) +import GHC.Unit.State ( initPluginUnitConfig, mkUnitState ) import GHC.Driver.Config.Diagnostic ( initIfaceMessageOpts ) import GHC.Unit.Module ( Module, ModuleName, thisGhcUnit, GenModule(moduleUnit), IsBootInterface(NotBoot) ) import GHC.Unit.Module.ModIface @@ -69,7 +72,6 @@ import GHC.Utils.Error import GHC.Utils.Outputable import GHC.Utils.Exception -import Control.Monad ( unless ) import Data.Maybe ( mapMaybe ) import Unsafe.Coerce ( unsafeCoerce ) import GHC.Linker.Types @@ -156,8 +158,12 @@ initializePlugins hsc_env loadPlugins :: HscEnv -> IO ([LoadedPlugin], [Linkable], PkgsLoaded) loadPlugins hsc_env - = do { unless (null to_load) $ - checkExternalInterpreter hsc_env + = do { -- When -fexternal-interpreter is active, create a local Interp with + -- InternalInterp so plugins are loaded into GHC's own process. + -- TH/bytecode execution still goes through the external interpreter. + ; hsc_env' <- if null to_load then return hsc_env + else withPluginInterp hsc_env + ; let loadPlugin = loadPlugin' (mkVarOccFS (fsLit "plugin")) pluginTyConName hsc_env' ; plugins_with_deps <- mapM loadPlugin to_load ; let (plugins, ifaces, links, pkgs) = unzip4 plugins_with_deps ; return (zipWith attachOptions to_load (zip plugins ifaces), concat links, foldl' plusUDFM emptyUDFM pkgs) @@ -171,23 +177,72 @@ loadPlugins hsc_env where options = [ option | (opt_mod_nm, option) <- pluginModNameOpts dflags , opt_mod_nm == mod_nm ] - loadPlugin = loadPlugin' (mkVarOccFS (fsLit "plugin")) pluginTyConName hsc_env loadFrontendPlugin :: HscEnv -> ModuleName -> IO (FrontendPlugin, [Linkable], PkgsLoaded) loadFrontendPlugin hsc_env mod_name = do - checkExternalInterpreter hsc_env + hsc_env' <- withPluginInterp hsc_env (plugin, _iface, links, pkgs) <- loadPlugin' (mkVarOccFS (fsLit "frontendPlugin")) frontendPluginTyConName - hsc_env mod_name + hsc_env' mod_name return (plugin, links, pkgs) --- #14335 -checkExternalInterpreter :: HscEnv -> IO () -checkExternalInterpreter hsc_env = case interpInstance <$> hsc_interp hsc_env of - Just (ExternalInterp {}) - -> throwIO (InstallationError "Plugins require -fno-external-interpreter") - _ -> pure () +-- | When the external interpreter is active, provide an 'HscEnv' with a +-- local 'Interp' that uses 'InternalInterp' for loading plugins into GHC's +-- own process. Plugins are compiler extensions that need to run in the +-- compiler's address space — they cannot run in iserv because plugin values +-- (records of Haskell functions) must be directly callable from GHC. +-- +-- This decouples plugin loading from TH\/bytecode execution: plugins use the +-- in-process RTS linker, while TH and GHCi continue to use the external +-- interpreter (iserv) as configured. +-- +-- For cross-compilers without an internal interpreter, this is not possible +-- and we fall back to an error suggesting @-fplugin-library@ instead. +-- See #14335. +withPluginInterp :: HscEnv -> IO HscEnv +withPluginInterp hsc_env = case interpInstance <$> hsc_interp hsc_env of + Just (ExternalInterp {}) -> +#if defined(HAVE_INTERNAL_INTERPRETER) + do pluginInterp <- mkPluginInterp + let hsc_env1 = hsc_env { hsc_interp = Just pluginInterp } + -- When plugin-specific package DBs are configured (via + -- -plugin-package-db), build a separate UnitState from host + -- package DBs so that plugins resolve host-side libraries. + -- This is essential for cross-compilation where plugin packages + -- are built for the host, not the target. + case pluginPackageDBFlags (hsc_dflags hsc_env) of + [] -> return hsc_env1 -- no plugin DBs: use existing UnitState + _ -> do + let logger = hsc_logger hsc_env + dflags = hsc_dflags hsc_env + cfg = initPluginUnitConfig dflags + (pluginUS, _dbs) <- mkUnitState logger dflags cfg + return $ hscSetCurrentUnitState pluginUS hsc_env1 +#else + throwIO (InstallationError $ unlines + [ "Plugins require the internal interpreter, which is not available" + , "in this cross-compiler build." + , "Use -fplugin-library to load plugins from shared libraries instead." + ]) +#endif + _ -> return hsc_env + +#if defined(HAVE_INTERNAL_INTERPRETER) +-- | Create a local 'Interp' with 'InternalInterp' for loading plugins +-- into GHC's own process. The local interpreter has its own 'Loader' +-- state, independent of the external interpreter's, so plugin packages +-- are loaded into GHC's address space via the in-process RTS linker. +mkPluginInterp :: IO Interp +mkPluginInterp = do + loader <- uninitializedLoader + symbolCache <- mkInterpSymbolCache + return Interp + { interpInstance = InternalInterp + , interpLoader = loader + , interpSymbolCache = symbolCache + } +#endif loadPlugin' :: OccName -> Name -> HscEnv -> ModuleName -> IO (a, ModIface, [Linkable], PkgsLoaded) loadPlugin' occ_name plugin_name hsc_env mod_name diff --git a/compiler/GHC/Unit/State.hs b/compiler/GHC/Unit/State.hs index 07e0147113bf..66dfa1d3a69b 100644 --- a/compiler/GHC/Unit/State.hs +++ b/compiler/GHC/Unit/State.hs @@ -13,6 +13,8 @@ module GHC.Unit.State ( UnitErr (..), emptyUnitState, initUnits, + initPluginUnitConfig, + mkUnitState, readUnitDatabases, readUnitDatabase, getUnitDbRefs, @@ -79,6 +81,7 @@ import GHC.Prelude import GHC.Driver.DynFlags import GHC.Platform +import GHC.Platform.Host (hostPlatformArchOS) import GHC.Platform.Ways import GHC.Unit.Database @@ -415,6 +418,39 @@ initUnitConfig dflags cached_dbs home_units = offsetPackageDb (Just offset) (PackageDB (PkgDbPath p)) | OsPath.isRelative p = PackageDB (PkgDbPath (OsPath.unsafeEncodeUtf offset OsPath. p)) offsetPackageDb _ p = p +-- | Build a 'UnitConfig' for plugin packages using host-side package +-- databases. Used in cross-compilation scenarios where plugin packages +-- are built for the host platform, not the target. +-- +-- When @-plugin-package-db@ flags are specified, this config is used to +-- create a separate 'UnitState' so that plugins resolve from host package +-- DBs while TH\/bytecode uses the target 'UnitState'. +initPluginUnitConfig :: DynFlags -> UnitConfig +initPluginUnitConfig dflags = + UnitConfig + { unitConfigPlatformArchOS = hostPlatformArchOS + , unitConfigProgramName = programName dflags + , unitConfigWays = ways dflags + , unitConfigAllowVirtual = False + + , unitConfigGlobalDB = globalPackageDatabasePath dflags + , unitConfigGHCDir = topDir dflags + , unitConfigDBName = "package.conf.d" + + , unitConfigAutoLink = [] + , unitConfigDistrustAll = False + , unitConfigHideAll = gopt Opt_HideAllPluginPackages dflags + , unitConfigHideAllPlugins = gopt Opt_HideAllPluginPackages dflags + + , unitConfigDBCache = Nothing -- no cache, read from plugin DBs + , unitConfigFlagsDB = pluginPackageDBFlags dflags + , unitConfigFlagsExposed = pluginPackageFlags dflags + , unitConfigFlagsIgnored = [] + , unitConfigFlagsTrusted = [] + , unitConfigFlagsPlugins = pluginPackageFlags dflags + , unitConfigHomeUnits = Set.empty + } + -- | Map from 'ModuleName' to a set of module providers (i.e. a 'Module' and -- its 'ModuleOrigin'). From 1a91c1419e75c245c2879e3ae3ad61271cd5a34b Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Thu, 5 Mar 2026 18:27:43 +0900 Subject: [PATCH 2/3] Update T14335 test: plugins now work with -fexternal-interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T14335 previously tested that plugins + -fexternal-interpreter produced an error. With the new dual-interpreter architecture, plugins load via InternalInterp while TH uses the external interpreter, so this combination now succeeds. Change compile_fail → compile and remove the .stderr file. --- testsuite/tests/plugins/T14335.stderr | 1 - testsuite/tests/plugins/all.T | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 testsuite/tests/plugins/T14335.stderr diff --git a/testsuite/tests/plugins/T14335.stderr b/testsuite/tests/plugins/T14335.stderr deleted file mode 100644 index 709d918157c4..000000000000 --- a/testsuite/tests/plugins/T14335.stderr +++ /dev/null @@ -1 +0,0 @@ -ghc: Plugins require -fno-external-interpreter diff --git a/testsuite/tests/plugins/all.T b/testsuite/tests/plugins/all.T index 6dafb214fa1e..d8da916a31b2 100644 --- a/testsuite/tests/plugins/all.T +++ b/testsuite/tests/plugins/all.T @@ -150,7 +150,7 @@ test('T14335', [extra_files(['simple-plugin/', 'plugins01.hs']), pre_cmd('$MAKE -s --no-print-directory -C simple-plugin package.plugins01 TOP={top}')], - compile_fail, + compile, ['-package-db simple-plugin/pkg.plugins01/local.package.conf -fplugin Simple.Plugin \ -fexternal-interpreter -package simple-plugin ' + config.plugin_way_flags]) From c915c32c8c5e1ff47fed68db7b3fb8e9e1141003 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Thu, 5 Mar 2026 20:00:44 +0900 Subject: [PATCH 3/3] Add expected stderr for T14335 plugin test The plugin now runs successfully with -fexternal-interpreter, producing its normal output (plugin passes queried, options, pass run). Add the expected stderr to match. --- testsuite/tests/plugins/T14335.stderr | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 testsuite/tests/plugins/T14335.stderr diff --git a/testsuite/tests/plugins/T14335.stderr b/testsuite/tests/plugins/T14335.stderr new file mode 100644 index 000000000000..84e15cfa914e --- /dev/null +++ b/testsuite/tests/plugins/T14335.stderr @@ -0,0 +1,3 @@ +Simple Plugin Passes Queried +Got options: +Simple Plugin Pass Run