diff --git a/lib/package.gd b/lib/package.gd
index fc2cb2ac9a..5217c7f743 100644
--- a/lib/package.gd
+++ b/lib/package.gd
@@ -1365,3 +1365,34 @@ DeclareGlobalFunction( "Cite" );
DeclareGlobalFunction( "ShowPackageVariables" );
DeclareGlobalFunction( "PackageVariablesInfo" );
+
+
+# a utility function
+DeclareGlobalName( "PrescribedPackageVersions" );
+
+
+#############################################################################
+##
+#F PackagesLoaded()
+##
+##
+##
+##
+##
+## a string that describes the names of all currently loaded ⪆ packages
+## and their version numbers.
+##
+##
+##
+## The result consists of \n separated lines of the form
+## name = "version",
+## where name is the name of a loaded ⪆ package
+## and version is its version.
+##
+## One can print this string to a file and set the user preference
+## "PrescribedPackageVersions" to the name of this file,
+## Then starting ⪆ anew will load exactly the same packages.
+##
+##
+##
+DeclareGlobalName( "PackagesLoaded" );
diff --git a/lib/package.gi b/lib/package.gi
index 8916dc312e..aeff59e543 100644
--- a/lib/package.gi
+++ b/lib/package.gi
@@ -286,7 +286,8 @@ end );
## In earlier versions, this function had an argument; now we ignore it.
##
InstallGlobalFunction( InitializePackagesInfoRecords, function( arg )
- local pkgdirs, pkgdir, pkgdirstrs, ignore, name, file, files, record, r;
+ local pkgdirs, pkgdir, pkgdirstrs, ignore, name, file, files, record, r,
+ exact;
if IsBound( GAPInfo.PackagesInfoInitialized ) and
GAPInfo.PackagesInfoInitialized = true then
@@ -372,6 +373,17 @@ InstallGlobalFunction( InitializePackagesInfoRecords, function( arg )
fi;
od;
+ # If exact version numbers are prescribed then ignore all package
+ # versions that do not fit to the list.
+ exact:= PrescribedPackageVersions(
+ UserPreference( "PrescribedPackageVersions" ) );
+ if exact <> fail then
+ GAPInfo.PackagesInfo:= Filtered( GAPInfo.PackagesInfo,
+ r -> [ LowercaseString( r.PackageName ), r.Version ] in exact );
+ GAPInfo.PrescribedPackageVersions:= List( exact,
+ pair -> [ pair[1], Concatenation( "=", pair[2] ) ] );
+ fi;
+
# Sort the available info records by their version numbers.
# (Sort stably in order to make sure that an instance from the first
# possible root path gets chosen if the same version of a package
@@ -1993,6 +2005,46 @@ The level can be changed in a running session using \
multi:= false,
) );
+# And a preference for prescribing explicit package versions.
+BindGlobal( "PrescribedPackageVersions", function( filename )
+ local exact;
+
+ if IsString( filename ) and filename <> "" then
+ filename:= UserHomeExpand( filename );
+ if IsReadableFile( filename ) then
+ exact:= List( SplitString( StringFile( filename ), "\n" ),
+ line -> Filtered( SplitString( line, "=", " " ), x -> x <> "" ) );
+ if ForAll( exact, x -> Length( x ) = 2 and
+ Length( x[2] ) > 2 and
+ x[2][1] = '\"' and Last( x[2] ) = '\"' ) then
+ return List( exact, pair -> [ LowercaseString( pair[1] ),
+ ReplacedString( pair[2], "\"", "" ) ] );
+ fi;
+ fi;
+ fi;
+ return fail;
+end );
+
+DeclareUserPreference( rec(
+ name:= "PrescribedPackageVersions",
+ description:= [
+ "If the value is a nonempty string then it is assumed to be the name \
+of a file that consists of lines of the form name = \"version\", \
+where name is the (case insensitive) name of a package \
+and version is the exact version of the package name \
+that shall be loaded. \
+In this case, &GAP;'s automatic package loading mechanism will try to load \
+exactly these package versions, all other available packages (in particular \
+suggested packages that are not listed in the file) will be ignored, \
+and an error will occur if not all of the given packages can be loaded \
+in the prescribed versions. \
+This preference overrides the preferences \"PackagesToLoad\", \
+\"ExcludeFromAutoload\", \"PackagesToIgnore\"."
+ ],
+ default:= "",
+ check:= filename -> PrescribedPackageVersions( filename ) <> fail,
+ ) );
+
InstallGlobalFunction( AutoloadPackages, function()
local msg, pair, excludedpackages, name, record, neededPackages;
@@ -2014,6 +2066,9 @@ InstallGlobalFunction( AutoloadPackages, function()
# If --bare is specified, load no packages
if GAPInfo.CommandLineOptions.bare then
neededPackages := [];
+ elif IsBound( GAPInfo.PrescribedPackageVersions ) then
+ neededPackages := GAPInfo.PrescribedPackageVersions;
+ PushOptions( rec( OnlyNeeded:= true ) );
else
neededPackages := GAPInfo.Dependencies.NeededOtherPackages;
fi;
@@ -2090,6 +2145,9 @@ InstallGlobalFunction( AutoloadPackages, function()
LogPackageLoadingMessage( PACKAGE_DEBUG,
"suggested packages loaded", "GAP" );
fi;
+ if IsBound( GAPInfo.PrescribedPackageVersions ) then
+ PopOptions();
+ fi;
# Load the documentation for not yet loaded packages.
LogPackageLoadingMessage( PACKAGE_DEBUG,
@@ -3550,3 +3608,39 @@ InstallGlobalFunction( ShowPackageVariables, function( arg )
Print( result );
fi;
end );
+
+
+#############################################################################
+##
+#F PackagesLoaded()
+##
+##
+##
+##
+##
+## a string that describes the names of all currently loaded &GAP; packages
+## and their version numbers.
+##
+##
+##
+## The result consists of \n separated lines of the form
+## name = "version",
+## where name is the name of a loaded &GAP; package
+## and version is its version.
+##
+## One can print this string to a file and set the user preference
+## "PrescribedPackageVersions" to the name of this file,
+## Then starting &GAP; anew will try to load exactly the same packages,
+## and signal an error if this isnot possible.
+##
+##
+##
+BindGlobal( "PackagesLoaded", function()
+ local l;
+
+ l:= List( RecNames( GAPInfo.PackagesLoaded ),
+ x -> GAPInfo.PackagesLoaded.( x ) );
+ return JoinStringsWithSeparator(
+ Set( l, x -> Concatenation( x[3], " = \"", x[2], "\"" ) ),
+ "\n" );
+ end );