From 5de05c4fa51a793dd82492413e709238a560f17b Mon Sep 17 00:00:00 2001 From: ThomasBreuer Date: Wed, 13 Aug 2025 16:37:47 +0200 Subject: [PATCH 1/3] Start GAP with prescribed package versions In order to reproduce computations from a GAP session, it is desirable to start GAP with a given set of GAP packages whose exact version numbers are prescribed. The idea is as follows. - In a GAP session, use the new function `PackagesLoaded` for collecting the names and version numbers of the currently loaded GAP packages. - Write this description to a file. - Set the new user preference `PrescribedPackageVersions`, with value the name of that file. - Start a new GAP session. The user preference will modify the autoload process such that exactly the GAP packages listed in the file will be loaded, with exactly the listed versions. --- lib/package.gd | 31 ++++++++++++++++ lib/package.gi | 96 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 1 deletion(-) 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 &GAP; anew will load exactly the same packages. +## +## +## +DeclareGlobalName( "PackagesLoaded" ); diff --git a/lib/package.gi b/lib/package.gi index b6998c00f9..5077dff543 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 @@ -1987,6 +1999,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; @@ -2008,6 +2060,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; @@ -2084,6 +2139,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 +3643,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 ); From c6bd7cde7949b09e15aeb1a7a80f53af124594c4 Mon Sep 17 00:00:00 2001 From: ThomasBreuer Date: Fri, 21 Aug 2026 10:01:33 +0200 Subject: [PATCH 2/3] respect order in which packages were loaded When one wants to start a GAP session with the same package versions loaded as in the previous session, it can make a difference if these packages are loaded in a different order. The information about the order in which packages have been read in the current session is available via the log messages in `GAPInfo.PackageLoadingMessages`. This commit changes `PackagesLoaded` and the format of the value for the proposed user preference `PrescribedPackageVersions` such that the loading order is respected when the user preference is active. Note that the `OnlyNeeded` option is set (and logged) only per `LoadPackage` call, and sets of dependent packages get loaded together via one `LoadPackage` call. Thus we get the intended loading order by storing the packages for which `LoadPackage` was called, in the right order, and together with the `OnlyNeeded` value that was set in the reference GAP session. Thus we cannot simply set `OnleNeeded` to `true` when loading the packages. (In the list created by `PackagesLoaded()`, the indirectly loaded packages do not have entries concerning when they get loaded and which `OnlyNeeded` value is valid for them.) --- lib/package.gi | 137 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 24 deletions(-) diff --git a/lib/package.gi b/lib/package.gi index 5077dff543..aa885927c1 100644 --- a/lib/package.gi +++ b/lib/package.gi @@ -378,10 +378,12 @@ InstallGlobalFunction( InitializePackagesInfoRecords, function( arg ) 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 ); - GAPInfo.PrescribedPackageVersions:= List( exact, - pair -> [ pair[1], Concatenation( "=", pair[2] ) ] ); fi; # Sort the available info records by their version numbers. @@ -2001,18 +2003,34 @@ The level can be changed in a running session using \ # And a preference for prescribing explicit package versions. BindGlobal( "PrescribedPackageVersions", function( filename ) - local exact; + 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 -> 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], "\"", "" ) ] ); + 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; @@ -2033,14 +2051,16 @@ 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\"." +\"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" ) ); @@ -2057,12 +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 - neededPackages := GAPInfo.PrescribedPackageVersions; - PushOptions( rec( OnlyNeeded:= true ) ); + # 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; @@ -2076,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; @@ -2099,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\"", @@ -3659,23 +3708,63 @@ InstallGlobalFunction( ShowPackageVariables, function( arg ) ## ## ## 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. +## "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, -## and signal an error if this isnot possible. +## in exactly the same order as in the current session, +## and signal an error if this is not possible. ## ## ## BindGlobal( "PackagesLoaded", function() - local l; + 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; - l:= List( RecNames( GAPInfo.PackagesLoaded ), - x -> GAPInfo.PackagesLoaded.( x ) ); return JoinStringsWithSeparator( - Set( l, x -> Concatenation( x[3], " = \"", x[2], "\"" ) ), + Set( loaded, + x -> Concatenation( "\"", LowercaseString( x[1] ), + "\", \"", x[2], "\"", x[3] ) ), "\n" ); end ); From 71f9482b27cde0b11152bcbd7be9736925265c23 Mon Sep 17 00:00:00 2001 From: ThomasBreuer Date: Sat, 29 Aug 2026 00:07:29 +0200 Subject: [PATCH 3/3] fix typos --- lib/package.gd | 2 +- lib/package.gi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/package.gd b/lib/package.gd index 5217c7f743..ada128bc93 100644 --- a/lib/package.gd +++ b/lib/package.gd @@ -1390,7 +1390,7 @@ DeclareGlobalName( "PrescribedPackageVersions" ); ## 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, +## "PrescribedPackageVersions" to the name of this file. ## Then starting &GAP; anew will load exactly the same packages. ## ## diff --git a/lib/package.gi b/lib/package.gi index aa885927c1..c90ca66b6f 100644 --- a/lib/package.gi +++ b/lib/package.gi @@ -379,7 +379,7 @@ InstallGlobalFunction( InitializePackagesInfoRecords, function( arg ) UserPreference( "PrescribedPackageVersions" ) ); if exact <> fail then # Store the list of prescribed package version, - # including the informtion about ordering and 'OnlyNeeded' option. + # including the information about ordering and 'OnlyNeeded' option. GAPInfo.PrescribedPackageVersions:= exact; exact:= List( exact, x -> x{ [ 1, 2 ] } ); GAPInfo.PackagesInfo:= Filtered( GAPInfo.PackagesInfo,