From 348aa1eb19600f4131f0f278efb7f0d7bb916607 Mon Sep 17 00:00:00 2001 From: ThomasBreuer Date: Wed, 13 Aug 2025 16:37:47 +0200 Subject: [PATCH] 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 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 );