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 b6998c00f9..aa885927c1 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,19 @@ 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
+ # Store the list of prescribed package version,
+ # including the informtion about ordering and 'OnlyNeeded' option.
+ GAPInfo.PrescribedPackageVersions:= exact;
+ exact:= List( exact, x -> x{ [ 1, 2 ] } );
+ GAPInfo.PackagesInfo:= Filtered( GAPInfo.PackagesInfo,
+ r -> [ LowercaseString( r.PackageName ), r.Version ] in exact );
+ 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
@@ -1987,8 +2001,66 @@ 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, len, l;
+
+ if IsString( filename ) and filename <> "" then
+ filename:= UserHomeExpand( filename );
+ if IsReadableFile( filename ) then
+ # Check the contents; see 'PackagesLoaded' for the format.
+ exact:= List( SplitString( StringFile( filename ), "\n" ),
+ line -> SplitString( line, ",", " ," ) );
+ if ForAll( exact,
+ x -> Length( x ) in [ 2, 4 ] and
+ Length( x[2] ) > 2 and
+ x[2][1] = '\"' and Last( x[2] ) = '\"' and
+ ( Length( x ) = 2 or
+ ( ForAll( x[3], IsDigitChar ) and
+ x[4] in [ "true", "false" ] ) ) ) then
+ len:= Length( exact );
+ for l in exact do
+ l[1]:= ReplacedString( l[1], "\"", "" );
+ l[2]:= ReplacedString( l[2], "\"", "" );
+ if Length( l ) = 2 then
+ l[3]:= len;
+ else
+ l[3]:= Int( l[3] );
+ l[4]:= ( l[4] = "true" );
+ fi;
+ od;
+ SortBy( exact, x -> x[3] );
+ return exact;
+ 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\", \
+and the command line option -A."
+ ],
+ default:= "",
+ check:= filename -> PrescribedPackageVersions( filename ) <> fail,
+ ) );
+
InstallGlobalFunction( AutoloadPackages, function()
- local msg, pair, excludedpackages, name, record, neededPackages;
+ local msg, pair, excludedpackages, name, record, neededPackages,
+ prescribed;
SetInfoLevel( InfoPackageLoading,
UserPreference( "InfoPackageLoadingLevel" ) );
@@ -2005,9 +2077,16 @@ InstallGlobalFunction( AutoloadPackages, function()
GAPInfo.PackagesInfoInitialized:= false;
InitializePackagesInfoRecords();
- # If --bare is specified, load no packages
if GAPInfo.CommandLineOptions.bare then
+ # If '--bare' is specified, load no packages
neededPackages := [];
+ elif IsBound( GAPInfo.PrescribedPackageVersions ) then
+ # A list of packages with exact version numbers is prescribed,
+ # load these packages but no other packages.
+ # We assume that 'InitializePackagesInfoRecords' has removed all
+ # other package versions from the global data about available packages.
+ neededPackages := List( GAPInfo.PrescribedPackageVersions,
+ x -> x{ [ 1, 2 ] } );
else
neededPackages := GAPInfo.Dependencies.NeededOtherPackages;
fi;
@@ -2021,20 +2100,41 @@ InstallGlobalFunction( AutoloadPackages, function()
List( neededPackages,
pair -> Concatenation( pair[1], " (", pair[2], ")" ) ) ),
"GAP" );
- if GAPInfo.CommandLineOptions.A then
+ # If the '-A' option is given, load the needed packages
+ # but not their suggested packages.
+ # Note that 'PrescribedPackageVersions' overrides the '-A' option.
+ if GAPInfo.CommandLineOptions.A and
+ not IsBound( GAPInfo.PrescribedPackageVersions ) then
PushOptions( rec( OnlyNeeded:= true ) );
fi;
for pair in neededPackages do
+ # Set 'OnlyNeeded' for this package
+ # iff 'PrescribedPackageVersions' forces it.
+ if IsBound( GAPInfo.PrescribedPackageVersions ) then
+ prescribed:= First( GAPInfo.PrescribedPackageVersions,
+ l -> LowercaseString( pair[1] ) = l[1] );
+ if Length( prescribed ) = 4 then
+ PushOptions( rec( OnlyNeeded:= prescribed[4] ) );
+ fi;
+ fi;
+
+ # Load the package.
if LoadPackage( pair[1], pair[2], false ) <> true then
LogPackageLoadingMessage( PACKAGE_ERROR, Concatenation(
"needed package ", pair[1], " cannot be loaded" ), "GAP" );
Error( "failed to load needed package `", pair[1],
"' (version ", pair[2], ")" );
fi;
+
+ if IsBound( GAPInfo.PrescribedPackageVersions ) and
+ Length( prescribed ) = 4 then
+ PopOptions();
+ fi;
od;
LogPackageLoadingMessage( PACKAGE_DEBUG, "needed packages loaded",
"GAP" );
- if GAPInfo.CommandLineOptions.A then
+ if GAPInfo.CommandLineOptions.A and
+ not IsBound( GAPInfo.PrescribedPackageVersions ) then
PopOptions();
fi;
fi;
@@ -2044,6 +2144,10 @@ InstallGlobalFunction( AutoloadPackages, function()
LogPackageLoadingMessage( PACKAGE_DEBUG,
"omitting packages suggested via \"PackagesToLoad\" (-A option)",
"GAP" );
+ elif IsBound( GAPInfo.PrescribedPackageVersions ) then
+ LogPackageLoadingMessage( PACKAGE_DEBUG,
+ "omitting packages suggested via \"PackagesToLoad\" (PrescribedPackageVersions)",
+ "GAP" );
elif ValueOption( "OnlyNeeded" ) = true then
LogPackageLoadingMessage( PACKAGE_DEBUG,
[ "omitting packages suggested via \"PackagesToLoad\"",
@@ -2084,6 +2188,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,
@@ -3585,3 +3692,79 @@ 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" or "name", "version", pos, flag,
+## where name is the lowercase name of a loaded &GAP; package,
+## version is its version number,
+## pos, if given, indicates that this package has been loaded
+## in the pos-th call of ,
+## and flag is false or true, meaning that this package
+## and its indirectly loaded dependencies were loaded including suggested
+## packages or without suggested packages, respectively (the value of the
+## option OnlyNeeded during the call).
+##
+## 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,
+## in exactly the same order as in the current session,
+## and signal an error if this is not possible.
+##
+##
+##
+BindGlobal( "PackagesLoaded", function()
+ local loaded, loadmsg, i, msg, nextmsg, old, l, pos;
+
+ # the names of the currently loaded packages, and their version numbers
+ loaded:= List( RecNames( GAPInfo.PackagesLoaded ),
+ x -> GAPInfo.PackagesLoaded.( x ){ [ 3, 2 ] } );
+
+ # the packages for which 'LoadPackage' has been called in the current
+ # session, in the right order;
+ # note that there is only one explicit 'LoadPackage' call for each set
+ # of packages which depend on each other
+ loadmsg:= [];
+ for i in [ 1 .. Length( GAPInfo.PackageLoadingMessages ) - 1 ] do
+ msg:= GAPInfo.PackageLoadingMessages[i];
+ nextmsg:= GAPInfo.PackageLoadingMessages[i+1];
+ if StartsWith( msg[3][1], "entering LoadPackage" ) and
+ not StartsWith( nextmsg[3][1],
+ "return from LoadPackage, package was already loaded" ) then
+ Add( loadmsg, msg );
+ fi;
+ od;
+ old:= List( loadmsg,
+ x -> [ x[1], PositionSublist( x[3][1],
+ "(omitting suggested packages)" ) <> fail ] );
+
+ # store in which order the packages will have to be loaded.
+ for l in loaded do
+ pos:= PositionProperty( old, x -> l[1] = x[1] );
+ if pos <> fail then
+ l[3]:= Concatenation( ", ", String( pos ), ", ",
+ String( old[ pos ][2] ) );
+ else
+ l[3]:= "";
+ fi;
+ od;
+
+ return JoinStringsWithSeparator(
+ Set( loaded,
+ x -> Concatenation( "\"", LowercaseString( x[1] ),
+ "\", \"", x[2], "\"", x[3] ) ),
+ "\n" );
+ end );