diff --git a/lib/package.gd b/lib/package.gd index b1d40c1d80..4f45541b2f 100644 --- a/lib/package.gd +++ b/lib/package.gd @@ -298,9 +298,30 @@ DeclareGlobalFunction( "LinearOrderByPartialWeakOrder" ); ##

## The arguments record and suggested are used ## for loading packages, as follows. +##

## The record record collects information about the needed and ## suggested packages of the package name, which allows one to ## compute an appropriate loading order of these packages. +## After the call, the value of the component LoadInfo of +## record is a record with the components +## +## +## name (the name of the package), +## +## +## comment (a string that is empty or describes why the package +## cannot be loaded, independent of the installed version), and +## +## +## versions (a list of records, one for each installed version of +## the package that has been checked; each such record has the components +## version, comment, and dependencies, +## where the latter is a list of records for each needed package that was +## checked, and each entry has the same format as the LoadInfo +## record itself. +## +## +##

## Finally, the value of suggested determines whether needed and ## suggested packages are considered (value true) or only needed ## packages (value false); diff --git a/lib/package.gi b/lib/package.gi index c852d79ec1..7e1084a434 100644 --- a/lib/package.gi +++ b/lib/package.gi @@ -663,12 +663,32 @@ InstallGlobalFunction( DisplayPackageLoadingLog, function( arg ) ## InstallGlobalFunction( PackageAvailabilityInfo, function( name, version, record, suggested, checkall ) - local InvalidStrongDependencies, Name, equal, comp, pair, currversion, - inforec, skip, msg, dep, record_local, wght, pos, needed, test, - name2, testpair; + local comp, loadinfo, InvalidStrongDependencies, Name, equal, pair, + currversion, inforec, skip, msg, dep, currloadinfo, GAPequal, + record_local, wght, pos, needed, test, name2, testpair; + + # Initialize the dependency info. + for comp in [ "AlreadyHandled", "Dependencies", "StrongDependencies", + "InstallationPaths", "Weights" ] do + if not IsBound( record.( comp ) ) then + record.( comp ):= []; + fi; + od; + if not IsBound( record.LoadInfo ) then + record.LoadInfo:= rec(); + fi; + + loadinfo:= record.LoadInfo; + if loadinfo = fail then + # happens if one fetches the global option but it is not set + loadinfo:= rec(); + fi; + loadinfo.name:= name; + loadinfo.versions:= []; + loadinfo.comment:= ""; InvalidStrongDependencies:= function( dependencies, weights, - strong_dependencies ) + strong_dependencies, loadinfo ) local result, order, pair, cycle; result:= false; @@ -686,6 +706,11 @@ InstallGlobalFunction( PackageAvailabilityInfo, "' but must be" ), "in the same load cycle, due to other dependencies" ], Name ); + Append( loadinfo.comment, + Concatenation( "package '", pair[1], + "' shall be loaded before package '", name, + "' but must be in the same load cycle, ", + "due to other dependencies, " ) ); result:= true; if not checkall then return result; @@ -700,43 +725,53 @@ InstallGlobalFunction( PackageAvailabilityInfo, Name:= name; name:= LowercaseString( name ); equal:= ""; - if 0 < Length( version ) and version[1] = '=' then + if StartsWith( version, "=" ) then equal:= "equal"; fi; if name = "gap" then # This case occurs if a package requires a particular GAP version. - return CompareVersionNumbers( GAPInfo.Version, version, equal ); + if CompareVersionNumbers( GAPInfo.Version, version, equal ) then + return true; + else + Append( loadinfo.comment, + Concatenation( "required GAP version ", version, + " is not compatible with the actual version" ) ); + return false; + fi; fi; - # 1. If the package `name' is already loaded then compare the version - # number of the loaded package with the required one. - # (Note that at most one version of a package can be available.) + # If the package `name' is already loaded then compare the version + # number of the loaded package with the required one. + # (Note that at most one version of a package can be available.) if IsBound( GAPInfo.PackagesLoaded.( name ) ) then - return CompareVersionNumbers( GAPInfo.PackagesLoaded.( name )[2], - version, equal ); + if CompareVersionNumbers( GAPInfo.PackagesLoaded.( name )[2], + version, equal ) then + return true; + else + Append( loadinfo.comment, + Concatenation( "package '", name, + "' is already loaded, required version ", version, + " is not compatible with the actual version" ) ); + return false; + fi; fi; - # 2. If the function was called from `AutoloadPackages' - # and if the package is listed among the packages to be excluded - # from autoload then exit. + # If the function was called from `AutoloadPackages' + # and if the package is listed among the packages to be excluded + # from autoload then exit. if IsBound( GAPInfo.ExcludeFromAutoload ) and name in GAPInfo.ExcludeFromAutoload then LogPackageLoadingMessage( PACKAGE_DEBUG, "package to be excluded from autoload, return 'false'", Name ); + Append( loadinfo.comment, + Concatenation( "package '", name, + "' shall be excluded from autoload, return 'false'" ) ); return false; fi; - # 3. Initialize the dependency info. - for comp in [ "AlreadyHandled", "Dependencies", "StrongDependencies", - "InstallationPaths", "Weights" ] do - if not IsBound( record.( comp ) ) then - record.( comp ):= []; - fi; - od; - - # 4. Deal with the case that `name' is among the packages - # from whose tests the current check for `name' arose. + # Deal with the case that `name' is among the packages + # from whose tests the current check for `name' arose. for pair in record.AlreadyHandled do if name = pair[1] then if CompareVersionNumbers( pair[2], version, equal ) then @@ -744,19 +779,24 @@ InstallGlobalFunction( PackageAvailabilityInfo, return fail; else # The version assumed on an outer level does not fit. + Append( loadinfo.comment, + Concatenation( "for package '", name, + "', version ", pair[2], + " is assumed on an outer level, ", + "but version ", version, " is required here" ) ); return false; fi; fi; od; - # 5. In recursive calls, regard the current package as handled, - # of course in the version in question. + # In recursive calls, regard the current package as handled, + # of course in the version in question. currversion:= [ name ]; Add( record.AlreadyHandled, currversion ); - # 6. Get the info records for the package `name', - # and take the first record that satisfies the conditions. - # (Note that they are ordered w.r.t. descending version numbers.) + # Get the info records for the package `name', + # and take the first record that satisfies the conditions. + # (Note that they are ordered w.r.t. descending version numbers.) for inforec in PackageInfo( name ) do Name:= inforec.PackageName; @@ -769,6 +809,11 @@ InstallGlobalFunction( PackageAvailabilityInfo, dep:= rec(); fi; + currloadinfo:= rec( version:= inforec.Version, + comment:= "", + dependencies:= [] ); + Add( loadinfo.versions, currloadinfo ); + # Test whether this package version fits. msg:= [ Concatenation( "PackageAvailabilityInfo for version ", inforec.Version ) ]; @@ -782,6 +827,8 @@ InstallGlobalFunction( PackageAvailabilityInfo, inforec.Version, " does not fit" ), Concatenation( "(required: ", version, ")" ) ], inforec.PackageName ); + Append( currloadinfo.comment, + Concatenation( "version ", version, "is required, " ) ); if not checkall then continue; fi; @@ -797,16 +844,23 @@ InstallGlobalFunction( PackageAvailabilityInfo, fi; # Test whether the required GAP version fits. - if IsBound( dep.GAP ) - and not CompareVersionNumbers( GAPInfo.Version, dep.GAP ) then - LogPackageLoadingMessage( PACKAGE_INFO, - Concatenation( "PackageAvailabilityInfo: required GAP version (", - dep.GAP, ") does not fit", - inforec.PackageName ) ); - if not checkall then - continue; + if IsBound( dep.GAP ) then + GAPequal:= ""; + if StartsWith( dep.GAP, "=" ) then + GAPequal:= "equal"; + fi; + if not CompareVersionNumbers( GAPInfo.Version, dep.GAP, GAPequal ) then + LogPackageLoadingMessage( PACKAGE_INFO, + Concatenation( "PackageAvailabilityInfo: required GAP version (", + dep.GAP, ") does not fit", + inforec.PackageName ) ); + Append( currloadinfo.comment, + Concatenation( "GAP version ", dep.GAP, " is required, " ) ); + if not checkall then + continue; + fi; + skip:= true; fi; - skip:= true; fi; # Test whether the availability test function fits. @@ -823,6 +877,9 @@ InstallGlobalFunction( PackageAvailabilityInfo, Concatenation( "PackageAvailabilityInfo: the AvailabilityTest", " function returned ", String( test ) ), inforec.PackageName ); + Append( currloadinfo.comment, + Concatenation( "the AvailabilityTest function returned ", + String( test ), ", " ) ); if not checkall then continue; fi; @@ -837,6 +894,9 @@ InstallGlobalFunction( PackageAvailabilityInfo, inforec.InstallationPath, "/init.g', please check the installation" ), inforec.PackageName ); + Append( currloadinfo.comment, + Concatenation( "cannot locate '", + inforec.InstallationPath, "/init.g', " ) ); if not checkall then continue; fi; @@ -892,8 +952,14 @@ InstallGlobalFunction( PackageAvailabilityInfo, inforec.PackageName ); for pair in needed do name2:= LowercaseString( pair[1] ); + Add( currloadinfo.dependencies, + rec( name:= name2, + versions:= [], + comment:= "" ) ); + record_local.LoadInfo:= Last( currloadinfo.dependencies ); testpair:= PackageAvailabilityInfo( name2, pair[2], record_local, suggested, checkall ); + if testpair = false then # This dependency is not satisfied. test:= false; @@ -922,7 +988,7 @@ InstallGlobalFunction( PackageAvailabilityInfo, fi; if InvalidStrongDependencies( record_local.Dependencies, - record_local.Weights, record_local.StrongDependencies ) then + record_local.Weights, record_local.StrongDependencies, currloadinfo ) then # This package version cannot be loaded due to conditions # imposed by `OtherPackagesLoadedInAdvance' components. # (Log messages are added inside the function.) @@ -950,7 +1016,8 @@ InstallGlobalFunction( PackageAvailabilityInfo, record.Weights:= record_local.Weights; if suggested and IsBound( dep.SuggestedOtherPackages ) then - # Collect info about suggested packages and their dependencies. + # Collect info about suggested packages and their dependencies, + # but without extending 'LoadInfo'. LogPackageLoadingMessage( PACKAGE_DEBUG, Concatenation( [ "PackageAvailabilityInfo: check suggested packages" ], List( dep.SuggestedOtherPackages, @@ -961,6 +1028,7 @@ InstallGlobalFunction( PackageAvailabilityInfo, # Do not change the information collected up to now # until we are sure that we will really use the suggested package. record_local:= StructuralCopy( record ); + Unbind( record_local.LoadInfo ); test:= PackageAvailabilityInfo( name2, pair[2], record_local, suggested, checkall ); if test <> true then @@ -968,7 +1036,8 @@ InstallGlobalFunction( PackageAvailabilityInfo, if IsString( test ) then if InvalidStrongDependencies( record_local.Dependencies, record_local.Weights, - record_local.StrongDependencies ) then + record_local.StrongDependencies, + rec( comment:= "" ) ) then test:= false; fi; fi; @@ -1532,10 +1601,16 @@ BindGlobal( "GetPackageNameForPrefix", function( prefix ) ## #F LoadPackage( [, ][, ] ) ## +## The global option LoadInfo (with value a mutable record) +## can be used to collect information about the dependencies +## which have been checked during a call of . +## After the call, the record will contain data as described for +## PackageAvailabilityInfo. +## InstallGlobalFunction( LoadPackage, function( arg ) local name, Name, version, banner, loadsuggested, msg, depinfo, path, pair, i, order, paths, cycle, secondrun, pkgname, pos, info, - filename, entry, r; + filename, entry, r, loadinfo; # Get the arguments. if Length( arg ) = 0 then @@ -1551,6 +1626,13 @@ InstallGlobalFunction( LoadPackage, function( arg ) name:= GetPackageNameForPrefix( name ); fi; + loadinfo:= ValueOption( "LoadInfo" ); + if loadinfo <> fail then + if not ( IsRecord( loadinfo ) and IsMutable( loadinfo ) ) then + Error( "option 'LoadInfo', if given, must be a mutable record" ); + fi; + fi; + # Return 'fail' if this package is not installed. if not IsBound( GAPInfo.PackagesInfo.( name ) ) then LogPackageLoadingMessage( PACKAGE_DEBUG, @@ -1558,6 +1640,10 @@ InstallGlobalFunction( LoadPackage, function( arg ) if InfoLevel(InfoPackageLoading) < 4 then Info(InfoWarning,1, name, " package is not available. Check that the name is correct"); Info(InfoWarning,1, "and it is present in one of the GAP root directories (see '??RootPaths')"); + if loadinfo <> fail then + loadinfo.name:= name; + loadinfo.comment:= "package is not listed in GAPInfo.PackagesInfo"; + fi; fi; return fail; fi; @@ -1607,7 +1693,7 @@ InstallGlobalFunction( LoadPackage, function( arg ) # Test whether the package is available, # and compute the dependency information. - depinfo:= rec(); + depinfo:= rec( LoadInfo:= loadinfo ); path:= PackageAvailabilityInfo( name, version, depinfo, loadsuggested, false ); if not IsString( path ) then diff --git a/tst/testinstall/package.tst b/tst/testinstall/package.tst index 2f1e95a388..8621b8e3f2 100644 --- a/tst/testinstall/package.tst +++ b/tst/testinstall/package.tst @@ -1,4 +1,4 @@ -#@local entry,equ,pair,sml,oldTermEncoding,pkginfo,info,tmp_dir,mockpkgpath,old_warning_level,p,n,filename,IsDateFormatValid +#@local entry,equ,pair,sml,oldTermEncoding,pkginfo,info,tmp_dir,mockpkgpath,old_warning_level,p,n,filename,IsDateFormatValid,loadinfo,eval_loadinfo gap> START_TEST("package.tst"); # CompareVersionNumbers( , [, \"equal\"] ) @@ -699,4 +699,207 @@ gap> SetPackagePath( "mockpkg", "/some/other/directory" ); Error, another version of package mockpkg is already loaded # -gap> STOP_TEST( "package.tst", 1); +# Test collecting information when calling LoadPackage, using the mock package +# ('eval_loadinfo' is an example how one can evaluate the returned info.) +# +gap> eval_loadinfo:= function( r, indent... ) +> local rr; +> if Length( indent ) = 0 then +> indent:= ""; +> else +> indent:= indent[1]; +> fi; +> Print( indent, "consider package ", r.name, "\n" ); +> indent:= Concatenation( indent, " " ); +> if IsBound( r.comment ) and r.comment <> "" then +> Print( indent, "comment: ", r.comment, "\n" ); +> fi; +> if IsBound( r.versions ) then +> for rr in r.versions do +> Print( indent, "consider version ", rr.version, ":\n" ); +> if IsBound( rr.comment ) and rr.comment <> "" then +> Print( indent, " comment: ", rr.comment, "\n" ); +> fi; +> if Length( rr.dependencies ) <> 0 then +> Print( indent, " dependencies:\n" ); +> Perform( rr.dependencies, +> x -> eval_loadinfo( x, Concatenation( indent, " " ) ) ); +> fi; +> od; +> fi; +> return ""; +> end;; + +# +gap> SetPackagePath("mockpkg", mockpkgpath); +gap> old_warning_level := InfoLevel( InfoWarning );; +gap> SetInfoLevel( InfoWarning, 0 ); + +# Try to load a different version of the package. +gap> loadinfo:= rec();; +gap> LoadPackage( "mockpkg", "=0.0" : LoadInfo:= loadinfo ); +fail +gap> loadinfo; +rec( + comment := "package 'mockpkg' is already loaded, required version =0.0 is no\ +t compatible with the actual version", name := "mockpkg", versions := [ ] ) +gap> eval_loadinfo( loadinfo );; +consider package mockpkg + comment: package 'mockpkg' is already loaded, required version =0.0 is not c\ +ompatible with the actual version + +# Force "unload" it (see above, we have done this already once). +gap> Unbind(GAPInfo.PackagesInfo.mockpkg); +gap> Unbind(GAPInfo.PackagesLoaded.mockpkg); +gap> for n in [ "mockpkg_GlobalFunction", "mockpkg_Operation", "mockpkg_Attribute", "Setmockpkg_Attribute", "Hasmockpkg_Attribute", "mockpkg_Property", "Setmockpkg_Property", "Hasmockpkg_Property", "mockpkg_ExtensionData" ] do +> if IsBoundGlobal(n) then +> MakeReadWriteGlobal(n); +> UnbindGlobal(n); +> fi; +> od; + +# Notify the package again +gap> SetPackagePath("mockpkg", mockpkgpath); + +# Force unavailability of the mock package, for various reasons. +# Try to load the package into a not admissible GAP version. +gap> GAPInfo.PackagesInfo.mockpkg:= ShallowCopy( GAPInfo.PackagesInfo.mockpkg );; +gap> info:= GAPInfo.PackagesInfo.mockpkg;; +gap> info[1]:= ShallowCopy( info[1] );; +gap> info:= info[1];; +gap> info.Dependencies:= ShallowCopy( info.Dependencies );; +gap> info.Dependencies.GAP:= "=0.0";; +gap> loadinfo:= rec();; +gap> LoadPackage( "mockpkg" : LoadInfo:= loadinfo ); +fail +gap> Unbind( info.Dependencies.GAP ); +gap> loadinfo; +rec( comment := "", name := "mockpkg", + versions := + [ rec( comment := "GAP version =0.0 is required, ", dependencies := [ ], + version := "0.1" ) ] ) +gap> eval_loadinfo( loadinfo );; +consider package mockpkg + consider version 0.1: + comment: GAP version =0.0 is required, + +# Try again to load the package into a not admissible GAP version. +gap> info.Dependencies.NeededOtherPackages:= [ [ "GAP", "=0.0" ] ];; +gap> info.AvailabilityTest:= ReturnTrue;; +gap> loadinfo:= rec();; +gap> LoadPackage( "mockpkg" : LoadInfo:= loadinfo ); +fail +gap> info.Dependencies.NeededOtherPackages:= [];; +gap> loadinfo; +rec( comment := "", name := "mockpkg", + versions := + [ + rec( comment := "", + dependencies := + [ + rec( + comment := "required GAP version =0.0 is not compatible with \ +the actual version", name := "gap", versions := [ ] ) ], version := "0.1" ) + ] ) +gap> eval_loadinfo( loadinfo );; +consider package mockpkg + consider version 0.1: + dependencies: + consider package gap + comment: required GAP version =0.0 is not compatible with the actual v\ +ersion + +# Try to load the package with a too restrictive availability test. +gap> info:= GAPInfo.PackagesInfo.mockpkg[1];; +gap> info.AvailabilityTest:= ReturnFalse;; +gap> loadinfo:= rec();; +gap> LoadPackage( "mockpkg" : LoadInfo:= loadinfo ); +fail +gap> info.AvailabilityTest:= ReturnTrue;; +gap> loadinfo; +rec( comment := "", name := "mockpkg", + versions := + [ rec( comment := "the AvailabilityTest function returned false, ", + dependencies := [ ], version := "0.1" ) ] ) +gap> eval_loadinfo( loadinfo );; +consider package mockpkg + consider version 0.1: + comment: the AvailabilityTest function returned false, + +# Try to load the package with not satisfied dependencies. +gap> GAPInfo.PackagesInfo.mockpkg2:= [ +> rec( PackageName := "mockpkg2", Version:= "0.0", +> AvailabilityTest:= ReturnTrue, +> InstallationPath:= GAPInfo.PackagesInfo.gapdoc[1].InstallationPath, +> Dependencies:= rec( OtherPackagesLoadedInAdvance:= [ [ "mockpkg", "" ] ] ) ) ];; +gap> info.Dependencies.OtherPackagesLoadedInAdvance:= [ [ "mockpkg2", "" ] ];; +gap> loadinfo:= rec();; +gap> LoadPackage( "mockpkg" : LoadInfo:= loadinfo ); +fail +gap> loadinfo; +rec( comment := "", name := "mockpkg", + versions := + [ + rec( + comment := "package 'mockpkg2' shall be loaded before package 'mockpk\ +g' but must be in the same load cycle, due to other dependencies, ", + dependencies := + [ + rec( comment := "", name := "mockpkg2", + versions := + [ + rec( comment := "", + dependencies := + [ + rec( comment := "", name := "mockpkg", + versions := [ ] ) ], version := "0.0" ) ] + ) ], version := "0.1" ) ] ) +gap> eval_loadinfo( loadinfo );; +consider package mockpkg + consider version 0.1: + comment: package 'mockpkg2' shall be loaded before package 'mockpkg' but m\ +ust be in the same load cycle, due to other dependencies, + dependencies: + consider package mockpkg2 + consider version 0.0: + dependencies: + consider package mockpkg +gap> Unbind( info.Dependencies.OtherPackagesLoadedInAdvance ); +gap> Unbind( GAPInfo.PackagesInfo.mockpkg2 ); +gap> info.Dependencies.NeededOtherPackages:= [ [ "gapdoc", "=0.0" ] ];; +gap> loadinfo:= rec();; +gap> LoadPackage( "mockpkg" : LoadInfo:= loadinfo ); +fail +gap> loadinfo; +rec( comment := "", name := "mockpkg", + versions := + [ + rec( comment := "", + dependencies := + [ + rec( + comment := "package 'gapdoc' is already loaded, required vers\ +ion =0.0 is not compatible with the actual version", name := "gapdoc", + versions := [ ] ) ], version := "0.1" ) ] ) +gap> eval_loadinfo( loadinfo );; +consider package mockpkg + consider version 0.1: + dependencies: + consider package gapdoc + comment: package 'gapdoc' is already loaded, required version =0.0 is \ +not compatible with the actual version + +# Try to load an unknown package. +gap> loadinfo:= rec();; +gap> LoadPackage( "unavailable_package" : LoadInfo:= loadinfo ); +fail +gap> loadinfo; +rec( comment := "package is not listed in GAPInfo.PackagesInfo", + name := "unavailable_package" ) +gap> eval_loadinfo( loadinfo );; +consider package unavailable_package + comment: package is not listed in GAPInfo.PackagesInfo +gap> SetInfoLevel( InfoWarning, old_warning_level ); + +# +gap> STOP_TEST( "package.tst" );