Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  package.gi   Sprache: unbekannt

 
Spracherkennung für: .gi vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

#############################################################################
##
##  This file is part of GAP, a system for computational discrete algebra.
##  This file's authors include Frank Celler, Alexander Hulpke.
##
##  Copyright of GAP belongs to its developers, whose names are too numerous
##  to list here. Please refer to the COPYRIGHT file for details.
##
##  SPDX-License-Identifier: GPL-2.0-or-later
##
##  This file contains support for ⪆ packages.
##


# recode string to GAPInfo.TermEncoding, assuming input is UTF-8 or latin1
# (if useful this may become documented for general use)
BindGlobal( "RecodeForCurrentTerminal", function( str )
    local fun, u;
    if IsBoundGlobal( "Unicode" ) and IsBoundGlobal( "Encode" ) then
      # The GAPDoc package is completely loaded.
      fun:= ValueGlobal( "Unicode" );
      u:= fun( str, "UTF-8" );
      if u = fail then
        u:= fun( str, "ISO-8859-1");
      fi;
      if GAPInfo.TermEncoding <> "UTF-8" then
        fun:= ValueGlobal( "SimplifiedUnicodeString" );
        u:= fun( u, GAPInfo.TermEncoding );
      fi;
      fun:= ValueGlobal( "Encode" );
      u:= fun( u, GAPInfo.TermEncoding );
      return u;
    else
      # GAPDoc is not yet available, do nothing in this case.
      return str;
    fi;
  end );

#############################################################################
##
#F  CompareVersionNumbers( <supplied>, <required>[, "equal"] )
##
InstallGlobalFunction( CompareVersionNumbers, function( arg )
    local s, r, inequal, i, j, a, b;

    s:= arg[1];
    r:= arg[2];
    inequal:= not ( Length( arg ) = 3 and arg[3] = "equal" );

    # Deal with the case of a `dev' version.
    if   2 < Length( s )
       and s{ [ Length( s ) - 2 .. Length( s ) ] } = "dev" then
      return inequal or ( Length(r)>2 and r{[Length(r)-2..Length(r)]}="dev" );
    elif 2 < Length( r )
       and r{ [ Length( r ) - 2 .. Length( r ) ] } = "dev" then
      return false;
    fi;

    while 0 < Length( s ) or 0 < Length( r ) do

      # Remove leading non-digit characters.
      i:= 1;
      while i <= Length( s ) and not IsDigitChar( s[i] ) do
        i:= i+1;
      od;
      s:= s{ [ i .. Length( s ) ] };
      j:= 1;
      while j <= Length( r ) and not IsDigitChar( r[j] ) do
        j:= j+1;
      od;
      r:= r{ [ j .. Length( r ) ] };

      # If one of the two strings is empty then we are done.
      if   Length( s ) = 0 then
        return Length( r ) = 0;
      elif Length( r ) = 0 then
        return inequal;
      fi;

      # Compare the next portion of digit characters.
      i:= 1;
      while i <= Length( s ) and IsDigitChar( s[i] ) do
        i:= i+1;
      od;
      a:= Int( s{ [ 1 .. i-1 ] } );
      j:= 1;
      while j <= Length( r ) and IsDigitChar( r[j] ) do
        j:= j+1;
      od;
      b:= Int( r{ [ 1 .. j-1 ] } );
      if   a < b then
        return false;
      elif b < a then
        return inequal;
      fi;
      s:= s{ [ i .. Length( s ) ] };
      r:= r{ [ j .. Length( r ) ] };

    od;

    # The two remaining strings are empty.
    return true;
end );


#############################################################################
##
#F  PackageInfo( <pkgname> )
##
InstallGlobalFunction( PackageInfo, function( pkgname )
    pkgname:= LowercaseString( pkgname );
    if not IsBound( GAPInfo.PackagesInfo.( pkgname ) ) then
      return [];
    else
      return GAPInfo.PackagesInfo.( pkgname );
    fi;
    end );


#############################################################################
##
#F  RECORDS_FILE( <name> )
##
InstallGlobalFunction( RECORDS_FILE, function( name )
    local str, rows, recs, pos, r;

    str:= StringFile( name );
    if str = fail then
      return [];
    fi;
    rows:= SplitString( str, "", "\n" );
    recs:= [];
    for r in rows do
      # remove comments starting with `#'
      pos:= Position( r, '#' );
      if pos <> fail then
        r:= r{ [ 1 .. pos-1 ] };
      fi;
      Append( recs, SplitString( r, "", " \n\t\r" ) );
    od;
    return List( recs, LowercaseString );
    end );


#############################################################################
##
#F  SetPackageInfo( <record> )
##
InstallGlobalFunction( SetPackageInfo, function( record )
    local rnam, info;
    if IsHPCGAP then
        info := rec();
        for rnam in REC_NAMES(record) do
          info.(rnam) := Immutable(record.(rnam));
        od;
        record := info;
    fi;
    GAPInfo.PackageInfoCurrent:= record;
    end );


#############################################################################
##
#F  FindPackageInfosInSubdirectories( pkgdir, name )
##
##  Finds all PackageInfos in subdirectories of directory name in
##  directory pkgdir, return a list of their paths.
##
BindGlobal( "FindPackageInfosInSubdirectories", function( pkgdir, name )
    local pkgpath, file, files, subdir;
    pkgpath:= Filename( [ pkgdir ], name );
    # This can be 'fail' if 'name' is a void link.
    if pkgpath = fail then
      return [];
    fi;

    if not IsDirectoryPath( pkgpath ) then
      return [];
    fi;
    if name in [ ".", ".." ] then
      return [];
    fi;

    file:= Filename( [ pkgdir ],
                      Concatenation( name, "/PackageInfo.g" ) );
    if file = fail then
      files := [];
      # Perhaps some subdirectories contain `PackageInfo.g' files.
      for subdir in Set( DirectoryContents( pkgpath ) ) do
        if not subdir in [ ".", ".." ] then
          pkgpath:= Filename( [ pkgdir ],
                              Concatenation( name, "/", subdir ) );
          if pkgpath <> fail and IsDirectoryPath( pkgpath )
                              and not subdir in [ ".", ".." ] then
            file:= Filename( [ pkgdir ],
                Concatenation( name, "/", subdir, "/PackageInfo.g" ) );
            if file <> fail then
              Add( files,
                    [ file, Concatenation( name, "/", subdir ) ] );
            fi;
          fi;
        fi;
      od;
    else
      files:= [ [ file, name ] ];
    fi;
    return files;
end );


#############################################################################
##
#F  AddPackageInfo( files )
##
BindGlobal( "AddPackageInfos", function( files, pkgdir, ignore )
    local file, record, pkgname, date, dd, mm;
    for file in files do
      # Read the `PackageInfo.g' file.
      Unbind( GAPInfo.PackageInfoCurrent );
      Read( file[1] );
      if IsBound( GAPInfo.PackageInfoCurrent ) then
        record:= GAPInfo.PackageInfoCurrent;
        Unbind( GAPInfo.PackageInfoCurrent );
        pkgname:= LowercaseString( record.PackageName );
        NormalizeWhitespace( pkgname );

        # Check whether GAP wants to reset loadability.
        if     IsBound( GAPInfo.PackagesRestrictions.( pkgname ) )
            and GAPInfo.PackagesRestrictions.( pkgname ).OnInitialization(
                    record ) = false then
          Add( GAPInfo.PackagesInfoRefuseLoad, record );
        elif pkgname in ignore then
          LogPackageLoadingMessage( PACKAGE_DEBUG,
              Concatenation( "ignore package ", record.PackageName,
              " (user preference PackagesToIgnore)" ), "GAP" );
        else
          record.InstallationPath:= Filename( [ pkgdir ], file[2] );
          # normalize to include trailing "/"
          record.InstallationPath:= Filename( [ Directory( record.InstallationPath ) ], "" );
          if not IsBound( record.PackageDoc ) then
            record.PackageDoc:= [];
          elif IsRecord( record.PackageDoc ) then
            record.PackageDoc:= [ record.PackageDoc ];
          fi;

          # Normalize the format of 'Date', i.e. if it is the format yyyy-mm-dd
          # then we change it to dd/mm/yyyy. When other tools have adapted to
          # the yyyy-mm-dd format we can normalize to that format and at some
          # point in the future get rid of this code.
          if Length(record.Date) = 10 and record.Date{[5,8]} = "--" then
            date := List( SplitString( record.Date, "-" ), Int);
            date := Permuted(date, (1,3)); # date = [dd,mm,yyyy]
            # generate the day and month strings
            # if the day has only one digit we have to add a 0
            if date[1] < 10 then
              dd := Concatenation("0", String(date[1]));
            else
              dd := String(date[1]);
            fi;
            # if the month has only one digit we have to add a 0
            if date[2] < 10 then
              mm := Concatenation("0", String(date[2]));
            else
              mm := String(date[2]);
            fi;
            record.Date := Concatenation(dd, "/", mm, "/", String(date[3]));
          fi;

          if IsHPCGAP then
            # FIXME: we make the package info record immutable, to
            # allow access from multiple threads; but that in turn
            # can break packages, which rely on their package info
            # record being readable (see issue #2568)
            MakeImmutable(record);
          fi;
          Add( GAPInfo.PackagesInfo, record );
        fi;
      fi;
    od;
end );

#############################################################################
##
#F  InitializePackagesInfoRecords()
##
##  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;

    if IsBound( GAPInfo.PackagesInfoInitialized ) and
       GAPInfo.PackagesInfoInitialized = true then
      # This function has already been executed in this session.
      return;
    fi;

    GAPInfo.LoadPackageLevel:= 0;
    GAPInfo.PackagesInfo:= [];
    GAPInfo.PackagesInfoRefuseLoad:= [];

    LogPackageLoadingMessage( PACKAGE_DEBUG,
        "entering InitializePackagesInfoRecords", "GAP" );

    # the first time this is called, add the cmd line args to the list
    if IsEmpty(GAPInfo.PackageDirectories) then
        for pkgdirstrs in GAPInfo.CommandLineOptions.packagedirs do
            pkgdirs:= List( SplitString( pkgdirstrs, ";" ), Directory );
            for pkgdir in pkgdirs do
                if not pkgdir in GAPInfo.PackageDirectories then
                    Add( GAPInfo.PackageDirectories, pkgdir );
                fi;
            od;
        od;
    fi;
    # add any new pkg directories to the list
    pkgdirs:= DirectoriesLibrary( "pkg" );
    if pkgdirs <> fail then
        pkgdirs:= Filtered( pkgdirs, dir -> not dir in GAPInfo.PackageDirectories );
        if not IsEmpty(pkgdirs) then
            Append( GAPInfo.PackageDirectories, pkgdirs );
        fi;
    fi;

    if IsEmpty(GAPInfo.PackageDirectories) then
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          "exit InitializePackagesInfoRecords (no pkg directories found)",
          "GAP" );
      GAPInfo.PackagesInfo:= AtomicRecord();
      return;
    fi;

    if IsBound( GAPInfo.ExcludeFromAutoload ) then
      # The function was called from `AutoloadPackages'.
      # Collect the `NOAUTO' information in a global list,
      # which will be used in `AutoloadPackages' for both packages to be
      # loaded according to the user preference "PackagesToLoad"
      # and suggested packages of needed packages.
      # The component `GAPInfo.ExcludeFromAutoload' will be unbound after the
      # call of `AutoloadPackages'.
      GAPInfo.ExcludeFromAutoload:= Set(
          UserPreference( "ExcludeFromAutoload" ), LowercaseString );
    fi;

    # Do not store information about packages in "PackagesToIgnore".
    ignore:= List( UserPreference( "PackagesToIgnore" ), LowercaseString );

    # Loop over the package directories,
    # remove the packages listed in `NOAUTO' files from GAP's suggested
    # packages, and unite the information for the directories.
    for pkgdir in GAPInfo.PackageDirectories do

      if IsBound( GAPInfo.ExcludeFromAutoload ) then
        UniteSet( GAPInfo.ExcludeFromAutoload,
                  List( RECORDS_FILE( Filename( pkgdir, "NOAUTO" ) ),
                        LowercaseString ) );
      fi;

      # pkgdir may be a package instead of a package directory
      file:= Filename( [ pkgdir ], "PackageInfo.g" );
      if file <> fail then
        AddPackageInfos( [ [ file, "" ] ], pkgdir, ignore );
      else
        # Loop over subdirectories of this package directory.
        for name in Set( DirectoryContents( pkgdir ) ) do

            ## Get all package dirs
            files := FindPackageInfosInSubdirectories( pkgdir, name );

            AddPackageInfos( files, pkgdir, ignore );

        od;
      fi;
    od;

    # 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
    # is available in several root paths.
    # Note that 'CompareVersionNumbers' returns 'true'
    # if the two arguments are equal.)
    StableSortParallel( List( GAPInfo.PackagesInfo, r -> r.Version ),
                  GAPInfo.PackagesInfo,
                  { a, b } -> not CompareVersionNumbers( a, b, "equal" ) and CompareVersionNumbers( a, b ) );

    # Turn the lists into records.
    record:= rec();
    for r in GAPInfo.PackagesInfo do
      name:= LowercaseString( r.PackageName );
      if IsBound( record.( name ) ) then
        record.( name ) := Concatenation( record.( name ), [ r ] );
      else
        record.( name ):= [ r ];
      fi;
      if IsHPCGAP then
        # FIXME: we make the package info record immutable, to
        # allow access from multiple threads; but that in turn
        # can break packages, which rely on their package info
        # record being readable (see issue #2568)
        MakeImmutable( record.( name ) );
      fi;
    od;
    GAPInfo.PackagesInfo:= AtomicRecord(record);

    GAPInfo.PackagesInfoInitialized:= true;
    LogPackageLoadingMessage( PACKAGE_DEBUG,
        "return from InitializePackagesInfoRecords", "GAP" );
end );


#############################################################################
##
#F  LinearOrderByPartialWeakOrder( <pairs>, <weights> )
##
##  The algorithm works with a directed graph
##  whose vertices are subsets of the <M>c_i</M>
##  and whose edges represent the given partial order.
##  We start with one vertex for each <M>x_i</M> and each <M>y_i</M>
##  from the input list, and draw an edge from <M>x_i</M> to <M>y_i</M>.
##  Furthermore,
##  we need a queue <M>Q</M> of the smallest vertices found up to now,
##  and a stack <M>S</M> of the largest vertices found up to now;
##  both <M>Q</M> and <M>S</M> are empty at the beginning.
##  Now we add the vertices without predecessors to <M>Q</M> and remove the
##  edges from these vertices until no more such vertex is found.
##  Then we put the vertices without successors on <M>S</M> and remove the
##  edges to these vertices until no more such vertex is found.
##  If edges are left then each of them leads eventually into a cycle in the
##  graph; we find a cycle and amalgamate it into a new vertex.
##  Now we repeat the process until all edges have disappeared.
##  Finally, the concatenation of <M>Q</M> and <M>S</M> gives us the sets
##  <M>c_i</M>.
##
InstallGlobalFunction( LinearOrderByPartialWeakOrder,
    function( pairs, weights )
    local Q, S, Qw, Sw, F, pair, vx, vy, v, pos, candidates, minwght,
          smallest, s, maxwght, largest, p, cycle, next, new;

    # Initialize the queue and the stack.
    Q:= [];
    S:= [];
    Qw:= [];
    Sw:= [];

    # Create a list of vertices according to the input.
    F:= [];
    for pair in Set( pairs ) do
      if pair[1] <> pair[2] then
        vx:= First( F, r -> r.keys[1] = pair[1] );
        if vx = fail then
          vx:= rec( keys:= [ pair[1] ], succ:= [], pred:= [] );
          Add( F, vx );
        fi;
        vy:= First( F, r -> r.keys[1] = pair[2] );
        if vy = fail then
          vy:= rec( keys:= [ pair[2] ], succ:= [], pred:= [] );
          Add( F, vy );
        fi;
        Add( vx.succ, vy );
        Add( vy.pred, vx );
      fi;
    od;

    # Assign the weights.
    weights:= SortedList( weights );
    for v in F do
      pos:= PositionSorted( weights, v.keys );
      if pos <= Length( weights ) and weights[ pos ][1] = v.keys[1] then
        v.wght:= weights[ pos ][2];
      else
        v.wght:= 0;
      fi;
    od;

    # While F contains a vertex, ...
    while not IsEmpty( F ) do

      # ... find the vertices in F without predecessors and add them to Q,
      # remove the edges from these vertices,
      # and remove these vertices from F.
      candidates:= Filtered( F, v -> IsEmpty( v.pred ) );
      if not IsEmpty( candidates ) then
        minwght:= infinity;    # larger than all admissible weights
        for v in candidates do
          if v.wght < minwght then
            minwght:= v.wght;
            smallest:= [ v ];
          elif v.wght = minwght then
            Add( smallest, v );
          fi;
        od;
        for v in smallest do
          Add( Q, v.keys );
          Add( Qw, v.wght );
          for s in v.succ do
            s.pred:= Filtered( s.pred, x -> not IsIdenticalObj( v, x ) );
            if IsEmpty( s.pred )
               and ForAll( smallest, x -> not IsIdenticalObj( s, x ) ) then
              Add( smallest, s );
            fi;
          od;
          pos:= PositionProperty( F, x -> IsIdenticalObj( v, x ) );
          Unbind( F[ pos ] );
          F:= Compacted( F );
        od;
      fi;

      # Then find the vertices in F without successors and put them on S,
      # remove the edges to these vertices,
      # and remove these vertices from F.
      candidates:= Filtered( F, v -> IsEmpty( v.succ ) );
      if not IsEmpty( candidates ) then
        maxwght:= -1;    # smaller than all admissible weights
        for v in candidates do
          if v.wght > maxwght then
            maxwght:= v.wght;
            largest:= [ v ];
          elif v.wght = maxwght then
            Add( largest, v );
          fi;
        od;
        for v in largest do
          Add( S, v.keys );
          Add( Sw, v.wght );
          for p in v.pred do
            p.succ:= Filtered( p.succ, x -> not IsIdenticalObj( v, x ) );
            if IsEmpty( p.succ )
               and ForAll( largest, x -> not IsIdenticalObj( p, x ) ) then
              Add( largest, p );
            fi;
          od;
          pos:= PositionProperty( F, x -> IsIdenticalObj( v, x ) );
          Unbind( F[ pos ] );
          F:= Compacted( F );
        od;
      fi;

      if not IsEmpty( F ) then
        # Find a cycle in F.
        # (Note that now any vertex has a successor,
        # so we may start anywhere, and eventually get into a cycle.)
        cycle:= [];
        next:= F[1];
        repeat
          Add( cycle, next );
          next:= next.succ[1];
          pos:= PositionProperty( cycle, x -> IsIdenticalObj( x, next ) );
        until pos <> fail;
        cycle:= cycle{ [ pos .. Length( cycle ) ] };

        # Replace the set of vertices in the cycle by a new vertex,
        # replace all edges from/to a vertex outside the cycle
        # to/from a vertex in the cycle by edges to/from the new vertex.
        new:= rec( keys:= [], succ:= [], pred:= [],
                   wght:= Maximum( List( cycle, v -> v.wght ) ) );
        for v in cycle do
          UniteSet( new.keys, v.keys );
          for s in v.succ do
            if ForAll( cycle, w -> not IsIdenticalObj( s, w ) ) then
              if ForAll( new.succ, w -> not IsIdenticalObj( s, w ) ) then
                Add( new.succ, s );
              fi;
              pos:= PositionProperty( s.pred, w -> IsIdenticalObj( v, w ) );
              if ForAll( s.pred, w -> not IsIdenticalObj( new, w ) ) then
                s.pred[ pos ]:= new;
              else
                Unbind( s.pred[ pos ] );
                s.pred:= Compacted( s.pred );
              fi;
            fi;
          od;
          for p in v.pred do
            if ForAll( cycle, w -> not IsIdenticalObj( p, w ) ) then
              if ForAll( new.pred, w -> not IsIdenticalObj( p, w ) ) then
                Add( new.pred, p );
              fi;
              pos:= PositionProperty( p.succ, w -> IsIdenticalObj( v, w ) );
              if ForAll( p.succ, w -> not IsIdenticalObj( new, w ) ) then
                p.succ[ pos ]:= new;
              else
                Unbind( p.succ[ pos ] );
                p.succ:= Compacted( p.succ );
              fi;
            fi;
          od;
          pos:= PositionProperty( F, x -> IsIdenticalObj( v, x ) );
          Unbind( F[ pos ] );
          F:= Compacted( F );
        od;
        Add( F, new );
      fi;

    od;

    # Now the whole input is distributed to Q and S.
    return rec( cycles:= Concatenation( Q, Reversed( S ) ),
                weights:= Concatenation( Qw, Reversed( Sw ) ) );
    end );


#############################################################################
##
#I  InfoPackageLoading
##
##  (We cannot do this in `package.gd'.)
##
DeclareInfoClass( "InfoPackageLoading" );


#############################################################################
##
#F  LogPackageLoadingMessage( <severity>, <message>[, <name>] )
##
if not IsBound( TextAttr ) then
  TextAttr:= "dummy";
fi;
#T needed? (decl. of GAPDoc is loaded before)

InstallGlobalFunction( LogPackageLoadingMessage, function( severity, message, currpkg... )
    local i;

    if Length( currpkg ) = 1 then
      currpkg:= currpkg[1];
    elif Length( currpkg ) > 1 then
      Error("usage: LogPackageLoadingMessage( <severity>, <message>[, <name>] )");
    elif IsBound( GAPInfo.PackageCurrent ) then
      # This happens inside availability tests.
      currpkg:= GAPInfo.PackageCurrent.PackageName;
    else
      currpkg:= "(unknown package)";
    fi;
    if IsString( message ) then
      message:= [ message ];
    fi;
    if severity <= PACKAGE_WARNING
       and UserPreference("UseColorsInTerminal") = true
       and IsBound( TextAttr )
       and IsRecord( TextAttr ) then
      if severity = PACKAGE_ERROR then
        message:= List( message,
            msg -> Concatenation( TextAttr.1, msg, TextAttr.reset ) );
      else
        message:= List( message,
            msg -> Concatenation( TextAttr.4, msg, TextAttr.reset ) );
      fi;
    fi;
    Add( GAPInfo.PackageLoadingMessages, [ currpkg, severity, message ] );
    Info( InfoPackageLoading, severity, currpkg, ": ", message[1] );
    for i in [ 2 .. Length( message ) ] do
      Info( InfoPackageLoading, severity, List( currpkg, x -> ' ' ),
            "  ", message[i] );
    od;
    end );

if not IsReadOnlyGlobal( "TextAttr" ) then
  Unbind( TextAttr );
fi;


#############################################################################
##
#F  DisplayPackageLoadingLog( [<severity>] )
##
InstallGlobalFunction( DisplayPackageLoadingLog, function( arg )
    local severity, entry, message, i;

    if Length( arg ) = 0 then
      severity:= PACKAGE_WARNING;
    else
      severity:= arg[1];
    fi;

    for entry in GAPInfo.PackageLoadingMessages do
      if severity >= entry[2] then
        message:= entry[3];
        Info( InfoPackageLoading, 1, entry[1], ": ", message[1] );
        for i in [ 2 .. Length( message ) ] do
          Info( InfoPackageLoading, 1, List( entry[1], x -> ' ' ),
                "  ", message[i] );
        od;
      fi;
    od;
    end );


#############################################################################
##
#F  PackageAvailabilityInfo( <name>, <version>, <record>, <suggested>,
#F      <checkall> )
##
InstallGlobalFunction( PackageAvailabilityInfo,
    function( name, version, record, suggested, checkall )
    local comp, loadinfo, 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",
                  "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:= "";

    Name:= name;
    name:= LowercaseString( name );
    equal:= "";
    if StartsWith( version, "=" ) then
      equal:= "equal";
    fi;

    if name = "gap" then
      # This case occurs if a package requires a particular GAP version.
      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;

    # 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
      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;

    # 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;

    # 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
          # The availability of the package will be decided on an outer level.
          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;

    # In recursive calls, regard the current package as handled,
    # of course in the version in question.
    currversion:= [ name ];
    Add( record.AlreadyHandled, currversion );

    # 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;
      skip:= false;
      currversion[2]:= inforec.Version;

      if IsBound( inforec.Dependencies ) then
        dep:= inforec.Dependencies;
      else
        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 ) ];
      if version <> "" then
        if not CompareVersionNumbers( inforec.Version, version, equal ) then
          # The severity of the log message must be less than `PACKAGE_INFO',
          # since we do not want to see the message when looking for reasons
          # why the current package cannot be loaded.
          LogPackageLoadingMessage( PACKAGE_DEBUG,
              [ Concatenation( "PackageAvailabilityInfo: version ",
                    inforec.Version, " does not fit" ),
                Concatenation( "(required: ", version, ")" ) ],
              inforec.PackageName );
          Append( currloadinfo.comment,
                  Concatenation( "version ", version, "is required, " ) );
          if not checkall then
            continue;
          fi;
          skip:= true;
        else
          Add( msg, Concatenation( "(required: ", version, ")" ) );
          LogPackageLoadingMessage( PACKAGE_INFO, msg,
              inforec.PackageName );
        fi;
      else
        LogPackageLoadingMessage( PACKAGE_INFO, msg,
            inforec.PackageName );
      fi;

      # Test whether the required GAP version fits.
      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;
      fi;

      # Test whether the availability test function fits.
      GAPInfo.PackageCurrent:= inforec;
      test:= (not IsBound(inforec.AvailabilityTest)) or (inforec.AvailabilityTest() = true);
      Unbind( GAPInfo.PackageCurrent );
      if test = true then
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            Concatenation( "PackageAvailabilityInfo: the AvailabilityTest",
                " function returned 'true'" ),
            inforec.PackageName );
      else
        LogPackageLoadingMessage( PACKAGE_INFO,
            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;
        skip:= true;
      fi;

      # Locate the `init.g' file of the package.
      if Filename( [ Directory( inforec.InstallationPath ) ], "init.g" )
           = fail  then
        LogPackageLoadingMessage( PACKAGE_WARNING,
            Concatenation( "PackageAvailabilityInfo: cannot locate `",
              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;
        skip:= true;
      fi;

      record_local:= StructuralCopy( record );

      wght:= 1;
      pos:= PositionProperty( record_local.Weights, pair -> pair[1] = name );
      if pos = fail then
        Add( record_local.Weights, [ name, wght ] );
      else
        record_local.Weights[ pos ][2]:= wght;
      fi;

      # Check the dependencies of this package version.
      needed:= [];
      if IsBound( dep.NeededOtherPackages ) then
        Append( needed, dep.NeededOtherPackages );
      fi;
      test:= true;
      if IsEmpty( needed ) then
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "PackageAvailabilityInfo: no needed packages",
            inforec.PackageName );
      else
        LogPackageLoadingMessage( PACKAGE_DEBUG, Concatenation(
            [ "PackageAvailabilityInfo: check needed packages" ],
            List( needed,
                  pair -> Concatenation( pair[1], " (", pair[2], ")" ) ) ),
            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;
            LogPackageLoadingMessage( PACKAGE_INFO,
                Concatenation( "PackageAvailabilityInfo: dependency '",
                    name2, "' is not satisfied" ), inforec.PackageName );
            if not checkall then
              # Skip the check of other dependencies.
              break;
            fi;
          elif testpair <> true then
            # The package `name2' is available but not yet loaded.
            Add( record_local.Dependencies, [ name2, name ] );
          fi;
        od;
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "PackageAvailabilityInfo: check of needed packages done",
            inforec.PackageName );
      fi;
      if test = false then
        # At least one package needed by this version is not available,
        if not checkall then
          continue;
        fi;
        skip:= true;
      fi;

      # All checks for this version have been performed.
      # Go to the next installed version if some check failed.
      if skip then
        continue;
      fi;

      # The version given by `inforec' will be taken.
      # Copy the information back to the argument record.
      record.InstallationPaths:= record_local.InstallationPaths;
      Add( record.InstallationPaths,
           [ name, [ inforec.InstallationPath, inforec.Version,
                     inforec.PackageName, false ] ] );
      record.Dependencies:= record_local.Dependencies;
      record.AlreadyHandled:= record_local.AlreadyHandled;
      record.Weights:= record_local.Weights;

      if suggested and IsBound( dep.SuggestedOtherPackages ) then
        # Collect info about suggested packages and their dependencies,
        # but without extending 'LoadInfo'.
        LogPackageLoadingMessage( PACKAGE_DEBUG, Concatenation(
            [ "PackageAvailabilityInfo: check suggested packages" ],
            List( dep.SuggestedOtherPackages,
                  pair -> Concatenation( pair[1], " (", pair[2], ")" ) ) ),
            inforec.PackageName );
        for pair in dep.SuggestedOtherPackages do
          name2:= LowercaseString( pair[1] );
          # 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
            Add( record_local.Dependencies, [ name2, name ] );
            if test <> false then
              record.InstallationPaths:= record_local.InstallationPaths;
              record.Dependencies:= record_local.Dependencies;
              record.AlreadyHandled:= record_local.AlreadyHandled;
              record.Weights:= record_local.Weights;
            fi;
          fi;
        od;
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "PackageAvailabilityInfo: check of suggested packages done",
            inforec.PackageName );
      fi;

      # Print a warning if the package should better be upgraded.
      if IsBound( GAPInfo.PackagesRestrictions.( name ) ) then
        GAPInfo.PackagesRestrictions.( name ).OnLoad( inforec );
      fi;
#T component name OnLoad:
#T shouldn't this be done only if the package is actually loaded?

      LogPackageLoadingMessage( PACKAGE_INFO,
          Concatenation( "PackageAvailabilityInfo: version ",
                         inforec.Version, " is available" ),
          inforec.PackageName );

      return inforec.InstallationPath;

    od;

    # No info record satisfies the requirements.
    if not IsBound( GAPInfo.PackagesInfo.( name ) ) then
      inforec:= First( GAPInfo.PackagesInfoRefuseLoad,
                       r -> LowercaseString( r.PackageName ) = name );
      if inforec <> fail then
        # Some versions are installed but all were refused.
        GAPInfo.PackagesRestrictions.( name ).OnLoad( inforec );
      fi;
    fi;

    LogPackageLoadingMessage( PACKAGE_INFO,
        Concatenation( "PackageAvailabilityInfo: ",
            "no installed version fits" ), Name );

    return false;
end );


#############################################################################
##
#F  TestPackageAvailability( <name>[, <version>][, <checkall>] )
##
InstallGlobalFunction( TestPackageAvailability, function( arg )
    local name, version, checkall, result;

    # Get the arguments.
    name:= LowercaseString( arg[1] );
    version:= "";
    checkall:= false;
    if Length( arg ) = 2 then
      if IsString( arg[2] ) then
        version:= arg[2];
      elif arg[2] = true then
        checkall:= true;
      fi;
    elif Length( arg ) = 3 then
      if IsString( arg[2] ) then
        version:= arg[2];
      fi;
      if arg[3] = true then
        checkall:= true;
      fi;
    fi;

    # Ignore suggested packages.
    result:= PackageAvailabilityInfo( name, version, rec(), false,
                                      checkall );

    if result = false then
      return fail;
    else
      return result;
    fi;
    end );


#############################################################################
##
#F  IsPackageLoaded( <name>[, <version>] )
##
InstallGlobalFunction( IsPackageLoaded, function( name, version... )
    local result;

    if Length(version) > 0 then
        version := version[1];
    fi;
    result := IsPackageMarkedForLoading( name, version );
    if result then
        # check if the package actually completed loading
        result := GAPInfo.PackagesLoaded.( LowercaseString( name ) )[4];
    fi;
    return result;
    end );


#############################################################################
##
#F  IsPackageMarkedForLoading( <name>, <version> )
##
InstallGlobalFunction( IsPackageMarkedForLoading, function( name, version )
    local equal;

    equal:= "";
    if 0 < Length( version ) and version[1] = '=' then
      equal:= "equal";
    fi;
    name:= LowercaseString( name );
    return IsBound( GAPInfo.PackagesLoaded.( name ) )
           and CompareVersionNumbers( GAPInfo.PackagesLoaded.( name )[2],
                   version, equal );
    end );


#############################################################################
##
#F  DefaultPackageBannerString( <inforec> )
##

DeclareUserPreference( rec(
  name:= "ShortBanners",
  description:= [
    "If this option is set to <K>true</K>, package banners printed during \
loading will only show the name, version and description of a package."
    ],
  default:= false,
  values:= [ true, false ],
  multi:= false,
  ) );

InstallGlobalFunction( DefaultPackageBannerString,
    function( inforec, useShortBanner... )
    local len, sep, i, str, authors, maintainers, contributors, printPersons;

    if Length( useShortBanner ) = 0 then
      useShortBanner := false;
    elif Length( useShortBanner ) = 1 then
      useShortBanner := useShortBanner[1];
    else
      Error( "DefaultPackageBannerString must be called with at most two arguments" );
    fi;

    # Start with a row of `-' signs.
    len:= SizeScreen()[1] - 3;
    if GAPInfo.TermEncoding = "UTF-8" then
      # The unicode character we use takes up 3 bytes in UTF-8 encoding,
      # hence we must adjust the length accordingly.
      sep:= "─";
      i:= 1;
      while 2 * i <= len do
        Append( sep, sep );
        i:= 2 * i;
      od;
      Append( sep, sep{ [ 1 .. 3 * ( len - i ) ] } );
    else
      sep:= ListWithIdenticalEntries( len, '-' );
    fi;
    Add( sep, '\n' );

    str:= "";

    # Add package name and version number.
    if IsBound( inforec.PackageName ) and IsBound( inforec.Version ) then
      Append( str, Concatenation(
              "Loading ", inforec.PackageName, " ", inforec.Version ) );
    fi;

    # Add the long title.
    if IsBound( inforec.PackageDoc ) and IsBound( inforec.PackageDoc[1] ) and
       IsBound( inforec.PackageDoc[1].LongTitle ) and
       not IsEmpty( inforec.PackageDoc[1].LongTitle ) then
      Append( str, Concatenation(
              " (", inforec.PackageDoc[1].LongTitle, ")" ) );
    fi;
    Add( str, '\n' );

    if not useShortBanner then
        # Add info about the authors and/or maintainers
        printPersons := function( role, persons )
          local fill, person;
          fill:= ListWithIdenticalEntries( Length(role), ' ' );
          Append( str, role );
          for i in [ 1 .. Length( persons ) ] do
            person:= persons[i];
            if IsBound( person.FirstNames ) then
              Append( str, person.FirstNames );
              Append( str, " " );
            fi;
            Append( str, person.LastName );
            if   IsBound( person.WWWHome ) then
              Append( str, Concatenation( " (", person.WWWHome, ")" ) );
            elif IsBound( person.Email ) then
              Append( str, Concatenation( " (", person.Email, ")" ) );
            fi;
            if   i = Length( persons ) then
              Append( str, ".\n" );
            elif i = Length( persons )-1 then
              if i = 1 then
                Append( str, " and\n" );
              else
                Append( str, ", and\n" );
              fi;
              Append( str, fill );
            else
              Append( str, ",\n" );
              Append( str, fill );
            fi;
          od;
        end;
        if IsBound( inforec.Persons ) then
          authors:= Filtered( inforec.Persons, x -> x.IsAuthor );
          if not IsEmpty( authors ) then
            printPersons( "by ", authors );
          fi;
          contributors:= Filtered( inforec.Persons, x -> not x.IsAuthor and not x.IsMaintainer );
          if not IsEmpty( contributors ) then
            Append( str, "with contributions by:\n");
            printPersons( "   ", contributors );
          fi;
          maintainers:= Filtered( inforec.Persons, x -> x.IsMaintainer );
          if not IsEmpty( maintainers ) and authors <> maintainers then
            Append( str, "maintained by:\n");
            printPersons( "   ", maintainers );
          fi;
        fi;

        # Add info about the home page of the package.
        if IsBound( inforec.PackageWWWHome ) then
          Append( str, "Homepage: " );
          Append( str, inforec.PackageWWWHome );
          Append( str, "\n" );
        fi;

        # Add info about the issue tracker of the package.
        if IsBound( inforec.IssueTrackerURL ) then
          Append( str, "Report issues at " );
          Append( str, inforec.IssueTrackerURL );
          Append( str, "\n" );
        fi;

        str := Concatenation(sep, str, sep);
    fi;

    # temporary hack, in some package names with umlauts are in HTML encoding
    str := RecodeForCurrentTerminal(str);
    str:= ReplacedString( str, "ä", RecodeForCurrentTerminal("ä") );
    str:= ReplacedString( str, "ö", RecodeForCurrentTerminal("ö") );
    str:= ReplacedString( str, "ü", RecodeForCurrentTerminal("ü") );

    return str;
    end );


#############################################################################
##
#F  DirectoriesPackagePrograms( <name> )
##
InstallGlobalFunction( DirectoriesPackagePrograms, function( name )
    local info, installationpath;

    # We are not allowed to call
    # `InstalledPackageVersion', `TestPackageAvailability' etc.
    name:= LowercaseString( name );
    info:= PackageInfo( name );
    if IsBound( GAPInfo.PackagesLoaded.( name ) ) then
      # The package is already loaded.
      installationpath:= GAPInfo.PackagesLoaded.( name )[1];
    elif IsBound( GAPInfo.PackageCurrent ) and
         LowercaseString( GAPInfo.PackageCurrent.PackageName ) = name then
      # The package in question is currently going to be loaded.
      installationpath:= GAPInfo.PackageCurrent.InstallationPath;
    elif 0 < Length( info ) then
      # Take the installed package with the highest version
      # that has been found first in the root paths.
      installationpath:= info[1].InstallationPath;
    else
      # This package is not known.
      return [];
    fi;
    return [ Directory( Concatenation( installationpath, "/bin/",
                            GAPInfo.Architecture, "/" ) ) ];
end );


#############################################################################
##
#F  DirectoriesPackageLibrary( <name>[, <path>] )
##
InstallGlobalFunction( DirectoriesPackageLibrary, function( arg )
    local name, path, info, installationpath, tmp;

    if IsEmpty(arg) or 2 < Length(arg) then
        Error( "usage: DirectoriesPackageLibrary( <name>[, <path>] )" );
    elif not ForAll(arg, IsString) then
        Error( "string argument(s) expected" );
    fi;

    name:= LowercaseString( arg[1] );
    if '\\' in name or ':' in name  then
        Error( "<name> must not contain '\\' or ':'" );
    elif 1 = Length(arg)  then
        path := "lib";
    else
        path := arg[2];
    fi;

    # We are not allowed to call
    # `InstalledPackageVersion', `TestPackageAvailability' etc.
    info:= PackageInfo( name );
    if IsBound( GAPInfo.PackagesLoaded.( name ) ) then
      # The package is already loaded.
      installationpath:= GAPInfo.PackagesLoaded.( name )[1];
    elif IsBound( GAPInfo.PackageCurrent ) and
         LowercaseString( GAPInfo.PackageCurrent.PackageName ) = name then
      # The package in question is currently going to be loaded.
      installationpath:= GAPInfo.PackageCurrent.InstallationPath;
    elif 0 < Length( info ) then
      # Take the installed package with the highest version
      # that has been found first in the root paths.
      installationpath:= info[1].InstallationPath;
    else
      # This package is not known.
      return [];
    fi;
    tmp:= Filename( Directory( installationpath ), path );
    if IsDirectoryPath( tmp ) = true then
      return [ Directory( tmp ) ];
    fi;
    return [];
end );


#############################################################################
##
#F  ReadPackage( [<name>, ]<file> )
#F  RereadPackage( [<name>, ]<file> )
##
BindGlobal( "_ReadPackage", function( arg, error )
    local pos, relpath, pkgname, namespace, filename;

    # Note that we cannot use `ReadAndCheckFunc' because this calls
    # `READ_GAP_ROOT', but here we have to read the file in one of those
    # directories where the package version resides that has been loaded
    # or (at least currently) would be loaded.
    if   Length( arg ) = 1 then
      # Guess the package name.
      pos:= Position( arg[1], '/' );
      if pos = fail then
        ErrorNoReturn(arg[1], " is not a filename in the form 'package/filepath'");
      fi;
      pkgname:= arg[1]{ [ 1 .. pos-1 ] };
      relpath:= arg[1]{ [ pos+1 .. Length( arg[1] ) ] };
    elif Length( arg ) = 2 then
      pkgname:= arg[1];
      relpath:= arg[2];
    else
      Error( "expected 1 or 2 arguments" );
    fi;
    pkgname:= LowercaseString( pkgname );
    namespace := GAPInfo.PackagesInfo.(pkgname)[1].PackageName;

    # Note that `DirectoriesPackageLibrary' finds the file relative to the
    # installation path of the info record chosen in `LoadPackage'.
    filename:= Filename( DirectoriesPackageLibrary( pkgname, "" ), relpath );
    if filename <> fail and IsReadableFile( filename ) then
      ENTER_NAMESPACE(namespace);
      Read( filename );
      LEAVE_NAMESPACE();
      return true;
    elif error then
      Info(InfoWarning, 1, "ReadPackage could not read <", pkgname, ">/", relpath, "\n");
    fi;
    return false;
    end );

InstallGlobalFunction( ReadPackage, function( arg )
    return _ReadPackage( arg, true );
    end );

InstallGlobalFunction( RereadPackage, function( arg )
    local res;

    MakeReadWriteGlobal( "REREADING" );
    REREADING:= true;
    MakeReadOnlyGlobal( "REREADING" );
    res:= _ReadPackage( arg, false );
    MakeReadWriteGlobal( "REREADING" );
    REREADING:= false;
    MakeReadOnlyGlobal( "REREADING" );
    return res;
    end );


#############################################################################
##
#F  LoadPackageDocumentation( <info> )
##
##  In versions before 4.5, a second argument was required.
##  For the sake of backwards compatibility, we do not forbid a second
##  argument, but we ignore it.
##  (In later versions, we may forbid the second argument.)
##
InstallGlobalFunction( LoadPackageDocumentation, function( arg )
    local info, short, pkgdoc, long, sixfile;

    info:= arg[1];

    # Load all books for the package.
    for pkgdoc in info.PackageDoc do
      # Fetch the names.
      if IsBound( pkgdoc.LongTitle ) then
        long:= pkgdoc.LongTitle;
      else
        long:= Concatenation( "GAP Package `", info.PackageName, "'" );
      fi;
      short:= pkgdoc.BookName;
      if not IsBound( GAPInfo.PackagesLoaded.( LowercaseString(
                          info.PackageName ) ) ) then
        short:= Concatenation( short, " (not loaded)" );
      fi;

      # Check that the `manual.six' file is available.
      sixfile:= Filename( [ Directory( info.InstallationPath ) ],
                          pkgdoc.SixFile );
      if sixfile = fail then
        LogPackageLoadingMessage( PACKAGE_INFO,
            Concatenation( [ "book `", pkgdoc.BookName,
                "': no manual index file `",
                pkgdoc.SixFile, "', ignored" ] ),
            info.PackageName );
      else
        # Finally notify the book via its directory.
#T Here we assume that this is the directory that contains also `manual.six'!
        HELP_ADD_BOOK( short, long,
            Directory( sixfile{ [ 1 .. Length( sixfile )-10 ] } ) );
      fi;
    od;
    end );

#############################################################################
##
#F  LoadPackage_ReadImplementationParts( <secondrun>, <banner> )
##
BindGlobal( "LoadPackage_ReadImplementationParts",
    function( secondrun, banner )
    local pair, info, bannerstring, pkgname, namespace;

    for pair in secondrun do
      namespace := pair[1].PackageName;
      pkgname := LowercaseString( namespace );
      if pair[2] <> fail then
        GAPInfo.PackageCurrent:= pair[1];
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "start reading file 'read.g'",
            namespace );
        ENTER_NAMESPACE(namespace);
        Read( pair[2] );
        LEAVE_NAMESPACE();
        Unbind( GAPInfo.PackageCurrent );
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "finish reading file 'read.g'",
            namespace );
      fi;
      # mark the package as completely loaded
      GAPInfo.PackagesLoaded.(pkgname)[4] := true;
      MakeImmutable( GAPInfo.PackagesLoaded.(pkgname) );
    od;

    # Show the banners.
    if banner then
      for pair in secondrun do
        info:= pair[1];

        # If the component `BannerString' is bound in `info' then we print
        # this string, otherwise we print the default banner string.
        if UserPreference( "ShortBanners" ) then
          bannerstring:= DefaultPackageBannerString( info, true );
        elif IsBound( info.BannerFunction ) then
          bannerstring:= RecodeForCurrentTerminal(info.BannerFunction(info));
        elif IsBound( info.BannerString ) then
          bannerstring:= RecodeForCurrentTerminal(info.BannerString);
        else
          bannerstring:= DefaultPackageBannerString( info );
        fi;

        # Suppress output formatting to avoid troubles with umlauts,
        # accents etc. in the banner.
        PrintWithoutFormatting( bannerstring );
      od;
    fi;
    end );


#############################################################################
##
#F  GetPackageNameForPrefix( <prefix> ) . . . . . . . .  show list of matches
#F                                                   or single match directly
##
##  Compute all names of installed packages that match the prefix <prefix>.
##  In case of a unique match return this match,
##  otherwise print an info message about the matches and return <prefix>.
##
##  This function is called by `LoadPackage'.
##
BindGlobal( "GetPackageNameForPrefix", function( prefix )
    local len, lowernames, name, allnames, indent, pos, sep;

    len:= Length( prefix );
    lowernames:= [];
    for name in Set( RecNames( GAPInfo.PackagesInfo ) ) do
      if Length( prefix ) <= Length( name ) and
         name{ [ 1 .. len ] } = prefix then
        Add( lowernames, name );
      fi;
    od;
    if IsEmpty( lowernames ) then
      # No package name matches.
      return prefix;
    fi;
    allnames:= List( lowernames,
                     nam -> GAPInfo.PackagesInfo.( nam )[1].PackageName );
    if Length( allnames ) = 1 then
      # There is one exact match.
      LogPackageLoadingMessage( PACKAGE_DEBUG, Concatenation(
          [ "replace prefix '", prefix, "' by the unique completion '",
            allnames[1], "'" ] ), allnames[1] );
      return lowernames[1];
    fi;

    # Several package names match.
    if 0 < InfoLevel( InfoPackageLoading ) then
      Print( "#I  Call 'LoadPackage' with one of the following strings:\n" );
      len:= SizeScreen()[1] - 6;
      indent:= "#I  ";
      Print( indent );
      pos:= Length( indent );
      sep:= "";
      for name in allnames do
        Print( sep );
        pos:= pos + Length( sep );
        if len < pos + Length( name ) then
          Print( "\n", indent );
          pos:= Length( indent );
        fi;
        Print( "\"", name, "\"" );
        pos:= pos + Length( name ) + 2;
        sep:= ", ";
      od;
      Print( ".\n" );
    fi;
    return prefix;
    end );


#############################################################################
##
#F  LoadPackage( <name>[, <version>][, <banner>] )
##
##  The global option <C>LoadInfo</C> (with value a mutable record)
##  can be used to collect information about the dependencies
##  which have been checked during a call of <Ref Func="LoadPackage"/>.
##  After the call, the record will contain data as described for
##  <C>PackageAvailabilityInfo</C>.
##
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, loadinfo;

    # Get the arguments.
    if Length( arg ) = 0 then
      name:= "";
    else
      name:= arg[1];
      if not IsString( name ) then
        Error( "<name> must be a string" );
      fi;
      name:= LowercaseString( name );
    fi;
    if not IsBound( GAPInfo.PackagesInfo.( name ) ) then
      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,
          "no package with this name is installed, return 'fail'", name );
      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;

    # The package is available, fetch the name for messages.
    Name:= GAPInfo.PackagesInfo.( name )[1].PackageName;
    version:= "";
    banner:= not GAPInfo.CommandLineOptions.q and
             not GAPInfo.CommandLineOptions.b;
    if 1 < Length( arg ) then
      if IsString( arg[2] ) then
        version:= arg[2];
        if 2 < Length( arg ) then
          banner:= banner and not ( arg[3] = false );
        fi;
      else
        banner:= banner and not ( arg[2] = false );
      fi;
    fi;
    loadsuggested:= ( ValueOption( "OnlyNeeded" ) <> true );

    # Print a warning if `LoadPackage' is called inside a
    # `LoadPackage' call.
    if not IsBound( GAPInfo.LoadPackageLevel ) then
      GAPInfo.LoadPackageLevel:= 0;
    fi;
    GAPInfo.LoadPackageLevel:= GAPInfo.LoadPackageLevel + 1;
    if GAPInfo.LoadPackageLevel <> 1 then
      if IsBound( GAPInfo.PackageCurrent ) then
        msg:= GAPInfo.PackageCurrent.PackageName;
      else
        msg:= "?";
      fi;
      LogPackageLoadingMessage( PACKAGE_WARNING,
          [ Concatenation( "Do not call `LoadPackage( \"", name,
                "\", ... )' in the package file" ),
            Concatenation( INPUT_FILENAME(), "," ),
            "use `IsPackageMarkedForLoading' instead" ], msg );
    fi;

    # Start logging.
    msg:= "entering LoadPackage ";
    if not loadsuggested then
      Append( msg, " (omitting suggested packages)" );
    fi;
    LogPackageLoadingMessage( PACKAGE_DEBUG, msg, Name );

    # Test whether the package is available,
    # and compute the dependency information.
    depinfo:= rec( LoadInfo:= loadinfo );
    path:= PackageAvailabilityInfo( name, version, depinfo, loadsuggested,
                                    false );
    if not IsString( path ) then
      if path = false then
        path:= fail;
      fi;
      # The result is either `true' (the package is already loaded)
      # or `fail' (the package cannot be loaded).
      if path = true then
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "return from LoadPackage, package was already loaded", Name );
      else
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "return from LoadPackage, package is not available", Name );
        if banner then
          if InfoLevel(InfoPackageLoading) < 4 then
            Info(InfoWarning,1, Name, " package is not available. To see further details, enter");
            Info(InfoWarning,1, "SetInfoLevel(InfoPackageLoading,4); and try to load the package again.");
          fi;
        fi;
      fi;
      GAPInfo.LoadPackageLevel:= GAPInfo.LoadPackageLevel - 1;
      return path;
    fi;

    # Suspend reordering of methods following InstallTrueMethod
    # because it would slow things down too much
    SuspendMethodReordering();

    # Compute the order in which the packages are loaded.
    # For each set of packages with cyclic dependencies,
    # we will first read all `init.g' files
    # and afterwards all `read.g' files.
    if IsEmpty( depinfo.Dependencies ) then
      order:= rec( cycles:= [ [ name ] ],
                   weights:= [ depinfo.Weights[1][2] ] );
    else
      order:= LinearOrderByPartialWeakOrder( depinfo.Dependencies,
                                             depinfo.Weights );
    fi;
    # paths:= TransposedMatMutable( depinfo.InstallationPaths );
    # (TransposedMatMutable is not yet available here ...)
    paths:= [ [], [] ];
    for pair in depinfo.InstallationPaths do
      Add( paths[1], pair[1] );
      Add( paths[2], pair[2] );
    od;
    SortParallel( paths[1], paths[2] );

    secondrun:= [];
    for i in [ 1 .. Length( order.cycles ) ] do
      cycle:= order.cycles[i];

      # First mark all packages in the current cycle as loaded,
      # in order to avoid that an occasional call of `LoadPackage'
      # inside the package code causes the files to be read more than once.
      for pkgname in cycle do
        pos:= PositionSorted( paths[1], pkgname );
        # the following entry is made immutable in LoadPackage_ReadImplementationParts
        GAPInfo.PackagesLoaded.( pkgname ):= paths[2][ pos ];
      od;

      if loadsuggested then
        msg:= "start loading needed/suggested/self packages";
      else
        msg:= "start loading needed/self packages";
      fi;
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          Concatenation( [ msg ], cycle ),
          Name );

      for pkgname in cycle do
        pos:= PositionSorted( paths[1], pkgname );
        info:= First( PackageInfo( pkgname ),
                      r -> r.InstallationPath = paths[2][ pos ][1] );

        if not ValidatePackageInfo(info) then
           Print("#E Validation of package ", pkgname, " from ", info.InstallationPath, " failed\n");
        fi;

        # Notify the documentation (for the available version).
        LoadPackageDocumentation( info );

        # Notify extensions provided by the package.
        if IsBound( info.Extensions ) then
          for entry in info.Extensions do
            LogPackageLoadingMessage( PACKAGE_DEBUG,
                Concatenation( "notify extension ", entry.filename ),
                pkgname );
            r:= ShallowCopy( entry );
            r.providedby:= pkgname;
            Add( GAPInfo.PackageExtensionsPending, Immutable( r ) );
          od;
        fi;

        # Read the `init.g' files.
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "start reading file 'init.g'",
            info.PackageName );
        GAPInfo.PackageCurrent:= info;
        ReadPackage( pkgname, "init.g" );
        Unbind( GAPInfo.PackageCurrent );
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            "finish reading file 'init.g'",
            info.PackageName );

        filename:= Filename( [ Directory( info.InstallationPath ) ],
                             "read.g" );
        Add( secondrun, [ info, filename ] );
      od;

      # Read the `read.g' files collected up to now.
      # Afterwards show the banners.
      # (We have delayed this until now because it uses functionality
      # from the package GAPDoc.)
      # Note that no banners are printed during autoloading.
      LoadPackage_ReadImplementationParts( secondrun, banner );
      secondrun:= [];

    od;

    # Load those package extensions whose condition is satisfied.
    for i in [ 1 .. Length( GAPInfo.PackageExtensionsPending ) ] do
      entry:= GAPInfo.PackageExtensionsPending[i];
      if ForAll( entry.needed, l -> IsPackageLoaded( l[1], l[2] ) ) then
        ReadPackage( entry.providedby, entry.filename );
        Add( GAPInfo.PackageExtensionsLoaded, entry );
        Unbind( GAPInfo.PackageExtensionsPending[i] );
        LogPackageLoadingMessage( PACKAGE_DEBUG,
            Concatenation( "load extension ", entry.filename ),
            entry.providedby );
      fi;
    od;
    GAPInfo.PackageExtensionsPending:= Compacted( GAPInfo.PackageExtensionsPending );

    LogPackageLoadingMessage( PACKAGE_DEBUG, "return from LoadPackage",
        Name );
    GAPInfo.LoadPackageLevel:= GAPInfo.LoadPackageLevel - 1;

    ResumeMethodReordering();
    return true;
    end );


#############################################################################
##
#F  LoadAllPackages()
##
InstallGlobalFunction( LoadAllPackages, function()
    SuspendMethodReordering();
    if ValueOption( "reversed" ) = true then
        List( Reversed( RecNames( GAPInfo.PackagesInfo ) ), LoadPackage );
    else
        List( RecNames( GAPInfo.PackagesInfo ), LoadPackage );
    fi;
    ResumeMethodReordering();
    end );


#############################################################################
##
#F  SetPackagePath( <pkgname>, <pkgpath> )
##
InstallGlobalFunction( SetPackagePath, function( pkgname, pkgpath )
    local pkgdir, file, record;

    InitializePackagesInfoRecords();
    pkgname:= LowercaseString( pkgname );
    NormalizeWhitespace( pkgname );
    if IsBound( GAPInfo.PackagesLoaded.( pkgname ) ) then
      # compare using `Directory` to expand "~" and add trailing "/"
      if Directory( GAPInfo.PackagesLoaded.( pkgname )[1] ) = Directory( pkgpath ) then
        return;
      fi;
      Error( "another version of package ", pkgname, " is already loaded" );
    fi;

    pkgdir:= Directory( pkgpath );
    file:= Filename( [ pkgdir ], "PackageInfo.g" );
    if file = fail then
      return;
    fi;
    Unbind( GAPInfo.PackageInfoCurrent );
    Read( file );
    record:= GAPInfo.PackageInfoCurrent;
    Unbind( GAPInfo.PackageInfoCurrent );
    if pkgname <> NormalizedWhitespace( LowercaseString(
                      record.PackageName ) ) then
      Error( "found package ", record.PackageName, " not ", pkgname,
             " in ", pkgpath );
    fi;
    if IsBound( GAPInfo.PackagesRestrictions.( pkgname ) )
       and GAPInfo.PackagesRestrictions.( pkgname ).OnInitialization(
               record ) = false  then
      Add( GAPInfo.PackagesInfoRefuseLoad, record );
    else
      record.InstallationPath:= Filename( [ pkgdir ], "" );
      if not IsBound( record.PackageDoc ) then
        record.PackageDoc:= [];
      elif IsRecord( record.PackageDoc ) then
        record.PackageDoc:= [ record.PackageDoc ];
      fi;
    fi;
    GAPInfo.PackagesInfo.( pkgname ):= [ record ];
    end );


#############################################################################
##
#F  ExtendRootDirectories( <paths> )
##
InstallGlobalFunction( ExtendRootDirectories, function( rootpaths )
    local i;

    rootpaths:= Filtered( rootpaths, path -> not path in GAPInfo.RootPaths );
    if not IsEmpty( rootpaths ) then
      # 'DirectoriesLibrary' concatenates root paths with directory names.
      for i in [ 1 .. Length( rootpaths ) ] do
        if not EndsWith( rootpaths[i], "/" ) then
          rootpaths[i]:= Concatenation( rootpaths[i], "/" );
        fi;
      od;
      # Append the new root paths.
      GAPInfo.RootPaths:= Immutable( Concatenation( GAPInfo.RootPaths,
          rootpaths ) );
      # Clear the cache.
      GAPInfo.DirectoriesLibrary:= AtomicRecord( rec() );
      # Reread the package information.
      if IsBound( GAPInfo.PackagesInfoInitialized ) and
         GAPInfo.PackagesInfoInitialized = true then
        GAPInfo.PackagesInfoInitialized:= false;
        InitializePackagesInfoRecords();
      fi;
    fi;
    end );


#############################################################################
##
#F  ExtendPackageDirectories( <paths_or_dirs> )
##
InstallGlobalFunction( ExtendPackageDirectories, function( paths_or_dirs )
    local p, changed;
    changed:= false;
    for p in paths_or_dirs do
      if IsString( p ) then
        p:= Directory( p );
      elif not IsDirectory( p ) then
        Error("input must be a list of path strings or directory objects");
      fi;
      if not p in GAPInfo.PackageDirectories then
        Add( GAPInfo.PackageDirectories, p );
        changed:= true;
      fi;
    od;
    if changed then
      # Reread the package information.
      if IsBound( GAPInfo.PackagesInfoInitialized ) and
         GAPInfo.PackagesInfoInitialized = true then
        GAPInfo.PackagesInfoInitialized:= false;
        InitializePackagesInfoRecords();
      fi;
    fi;
    end );


#############################################################################
##
#F  InstalledPackageVersion( <name> )
##
InstallGlobalFunction( InstalledPackageVersion, function( name )
    local avail, info;

    avail:= TestPackageAvailability( name, "" );
    if   avail = fail then
      return fail;
    elif avail = true then
      return GAPInfo.PackagesLoaded.( LowercaseString( name ) )[2];
    fi;
    info:= First( PackageInfo( name ), r -> r.InstallationPath = avail );
    return info.Version;
    end );


#############################################################################
##
#F  AutoloadPackages()
##

# The packages to load during startup can be specified via a user preference.
DeclareUserPreference( rec(
  name:= "PackagesToLoad",
  description:= [
    "A list of names of packages which should be loaded during startup. \
For backwards compatibility, the default lists most of packages \
that were autoloaded in &GAP; 4.4 (add or remove packages as you like)."
    ],
  default:= [ "autpgrp", "alnuth", "crisp", "ctbllib", "factint", "fga",
              "irredsol", "laguna", "PackageManager", "polenta", "polycyclic",
              "resclasses", "sophus", "tomlib" ],
  values:= function() return RecNames( GAPInfo.PackagesInfo ); end,
  multi:= true,
  ) );

# And a preference for avoiding some packages.
DeclareUserPreference( rec(
  name:= "ExcludeFromAutoload",
  description:= [
    "These packages are not loaded at &GAP; startup. This doesn't work for \
packages which are needed by the &GAP; library, or which are already loaded \
in a workspace."
    ],
  default:= [],
  values:= function() return RecNames( GAPInfo.PackagesInfo ); end,
  multi:= true,
  ) );

# And a preference for ignoring some packages completely during the session.
DeclareUserPreference( rec(
  name:= "PackagesToIgnore",
  description:= [
    "These packages are not regarded as available. This doesn't work for \
packages which are needed by the &GAP; library, or which are already loaded \
in a workspace."
    ],
  default:= [],
  values:= function() return RecNames( GAPInfo.PackagesInfo ); end,
  multi:= true,
  ) );

# And a preference for setting the info level of package loading.
DeclareUserPreference( rec(
  name:= "InfoPackageLoadingLevel",
  description:= [
    "Info messages concerning package loading up to this level are printed.  \
The level can be changed in a running session using \
<Ref Oper=\"SetInfoLevel\"/>."
    ],
  default:= PACKAGE_ERROR,
  values:= [ PACKAGE_ERROR, PACKAGE_WARNING, PACKAGE_INFO, PACKAGE_DEBUG ],
  multi:= false,
  ) );

InstallGlobalFunction( AutoloadPackages, function()
    local msg, pair, excludedpackages, name, record, neededPackages;

    SetInfoLevel( InfoPackageLoading,
        UserPreference( "InfoPackageLoadingLevel" ) );

    if GAPInfo.CommandLineOptions.L = "" then
      msg:= "entering AutoloadPackages (no workspace)";
    else
      msg:= Concatenation( "entering AutoloadPackages (workspace ",
                           GAPInfo.CommandLineOptions.L, ")" ) ;
    fi;
    LogPackageLoadingMessage( PACKAGE_DEBUG, msg, "GAP" );

    GAPInfo.ExcludeFromAutoload:= [];
    GAPInfo.PackagesInfoInitialized:= false;
    InitializePackagesInfoRecords();

    # If --bare is specified, load no packages
    if GAPInfo.CommandLineOptions.bare then
      neededPackages := [];
    else
      neededPackages := GAPInfo.Dependencies.NeededOtherPackages;
    fi;

    # Load the needed other packages (suppressing banners)
    # that are not yet loaded.
    if ForAny( neededPackages,
               p -> not IsBound( GAPInfo.PackagesLoaded.( p[1] ) ) ) then
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          Concatenation( [ "trying to load needed packages" ],
              List( neededPackages,
                  pair -> Concatenation( pair[1], " (", pair[2], ")" ) ) ),
          "GAP" );
      if GAPInfo.CommandLineOptions.A then
        PushOptions( rec( OnlyNeeded:= true ) );
      fi;
      for pair in neededPackages do
        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;
      od;
      LogPackageLoadingMessage( PACKAGE_DEBUG, "needed packages loaded",
          "GAP" );
      if GAPInfo.CommandLineOptions.A then
        PopOptions();
      fi;
    fi;

    # Load suggested packages of GAP (suppressing banners).
    if   GAPInfo.CommandLineOptions.A then
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          "omitting packages suggested via \"PackagesToLoad\" (-A option)",
          "GAP" );
    elif ValueOption( "OnlyNeeded" ) = true then
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          [ "omitting packages suggested via \"PackagesToLoad\"",
            " ('OnlyNeeded' option)" ],
          "GAP" );
    elif ForAny( List( UserPreference( "PackagesToLoad" ), LowercaseString ),
                 p -> not IsBound( GAPInfo.PackagesLoaded.( p ) ) ) then

      # Try to load the suggested other packages (suppressing banners),
      # issue a warning for each such package where this is not possible.
      excludedpackages:= List( UserPreference( "ExcludeFromAutoload" ),
                               LowercaseString );
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          Concatenation( [ "trying to load suggested packages" ],
              UserPreference( "PackagesToLoad" ) ),
          "GAP" );
      for name in UserPreference( "PackagesToLoad" ) do
#T admit pair [ name, version ] in user preferences!
        if LowercaseString( name ) in excludedpackages then
          LogPackageLoadingMessage( PACKAGE_DEBUG,
              Concatenation( "excluded from autoloading: ", name ),
              "GAP" );
        elif not IsBound( GAPInfo.PackagesLoaded.( LowercaseString( name ) ) ) then
          LogPackageLoadingMessage( PACKAGE_DEBUG,
              Concatenation( "considering for autoloading: ", name ),
              "GAP" );
          if LoadPackage( name, false ) <> true then
            LogPackageLoadingMessage( PACKAGE_DEBUG,
                 Concatenation( "suggested package ", name,
                     " cannot be loaded" ), "GAP" );
          else
            LogPackageLoadingMessage( PACKAGE_DEBUG,
                Concatenation( name, " loaded" ), "GAP" );
#T possible to get the right case of the name?
          fi;
        fi;
      od;
      LogPackageLoadingMessage( PACKAGE_DEBUG,
          "suggested packages loaded", "GAP" );
    fi;

    # Load the documentation for not yet loaded packages.
    LogPackageLoadingMessage( PACKAGE_DEBUG,
        "call LoadPackageDocumentation for not loaded packages",
        "GAP" );
    for name in RecNames( GAPInfo.PackagesInfo ) do
      if not IsBound( GAPInfo.PackagesLoaded.( name ) ) then
        # Note that the info records for each package are sorted
        # w.r.t. decreasing version number.
        record:= First( GAPInfo.PackagesInfo.( name ), IsRecord );
        if record <> fail then
          LoadPackageDocumentation( record );
        fi;
      fi;
    od;
    LogPackageLoadingMessage( PACKAGE_DEBUG,
        "LoadPackageDocumentation for not loaded packages done",
        "GAP" );

    Unbind( GAPInfo.ExcludeFromAutoload );

    LogPackageLoadingMessage( PACKAGE_DEBUG,
        "return from AutoloadPackages",
        "GAP" );
    end );


#############################################################################
##
#F  GAPDocManualLab(<pkgname>) . create manual.lab for package w/ GAPDoc docs
##
InstallGlobalFunction( GAPDocManualLabFromSixFile,
    function( bookname, sixfilepath )
    local stream, entries, SecNumber, esctex, file;

    stream:= InputTextFile( sixfilepath );

    atomic readonly HELP_REGION do
      entries:= HELP_BOOK_HANDLER.GapDocGAP.ReadSix( stream ).entries;
    od;

    SecNumber:= function( list )
      if IsEmpty( list ) or list[1] = 0 then
        return "";
      fi;
      while Last( list ) = 0 do
        Remove( list );
      od;
      return JoinStringsWithSeparator( List( list, String ), "." );
    end;

    # throw away TeX critical characters here
    esctex:= function( str )
      return Filtered( _StripEscapeSequences( str ), c -> not c in "%#$&^_~" );
    end;

    bookname:= LowercaseString( bookname );
    entries:= List( entries,
                     entry -> Concatenation( "\\makelabel{", bookname, ":",
                                       esctex(NormalizedWhitespace(entry[1])),
                                       "}{", SecNumber( entry[3] ), "}{",
                                       entry[7], "}\n" ) );
    # forget entries that contain a character from "\\*+/=" in label,
    # these were never allowed, so no old manual will refer to them
    entries := Filtered(entries, entry ->
                    not ForAny("\\*+/=", c-> c in entry{[9..Length(entry)]}));
    file:= Concatenation( sixfilepath{ [ 1 .. Length( sixfilepath ) - 3 ] },
                          "lab" );
    # add marker line
    entries := Concatenation (
        [Concatenation ("\\GAPDocLabFile{", bookname,"}\n")],
        entries);
    FileString( file, Concatenation( entries ) );
    Info( InfoWarning, 1, "File: ", file, " written." );
end );

InstallGlobalFunction( GAPDocManualLab, function(pkgname)
  local pinf, book, file;

  if not IsString(pkgname) then
    Error("argument <pkgname> should be a string\n");
  fi;
  pkgname := LowercaseString(pkgname);
  LoadPackage(pkgname);
  if not IsBound(GAPInfo.PackagesInfo.(pkgname)) then
    Error("Could not load package ", pkgname, ".\n");
  fi;
  if LoadPackage("GAPDoc") <> true then
    Error("package `GAPDoc' not installed. Please install `GAPDoc'\n" );
  fi;

  pinf := GAPInfo.PackagesInfo.(pkgname)[1];
  for book in pinf.PackageDoc do
    file := Filename([Directory(pinf.InstallationPath)], book.SixFile);
    if file = fail or not IsReadableFile(file) then
      Error("could not open `manual.six' file of package `", pkgname, "'.\n",
            "Please compile its documentation\n");
    fi;
    GAPDocManualLabFromSixFile( book.BookName, file );
  od;
end );


#############################################################################
##
#F  DeclareAutoreadableVariables( <pkgname>, <filename>, <varlist> )
##
InstallGlobalFunction( DeclareAutoreadableVariables,
    function( pkgname, filename, varlist )
    CallFuncList( AUTO, Concatenation( [
      function( x )
        # Avoid nested calls to `RereadPackage',
        # which could cause that `REREADING' is set to `false' too early.
        if REREADING then
          ReadPackage( pkgname, filename );
        else
          RereadPackage( pkgname, filename );
        fi;
      end, filename ], varlist ) );
    end );


#############################################################################
##
##  Tests whether loading a package works and does not obviously break
##  anything.
##  (This is very preliminary.)
##


#############################################################################
##
#F  ValidatePackageInfo( <info> )
##
InstallGlobalFunction( ValidatePackageInfo, function( info )
    local record, pkgdir, i, IsStringList, IsRecordList, IsProperBool, IsURL,
          IsFilename, IsFilenameList, result, TestOption, TestMandat, subrec,
          list, CheckDateValidity;

    if IsString( info ) then
      if IsReadableFile( info ) then
        Unbind( GAPInfo.PackageInfoCurrent );
        Read( info );
        if IsBound( GAPInfo.PackageInfoCurrent ) then
          record:= GAPInfo.PackageInfoCurrent;
          Unbind( GAPInfo.PackageInfoCurrent );
        else
          Error( "the file <info> is not a `PackageInfo.g' file" );
        fi;
        pkgdir:= "./";
        for i in Reversed( [ 1 .. Length( info ) ] ) do
          if info[i] = '/' then
            pkgdir:= info{ [ 1 .. i ] };
            break;
          fi;
        od;
      else
        Error( "<info> is not the name of a readable file" );
      fi;
    elif IsRecord( info ) then
      pkgdir:= fail;
      record:= info;
    else
      Error( "<info> must be either a record or a filename" );
    fi;

    IsStringList:= x -> IsList( x ) and ForAll( x, IsString );
    IsRecordList:= x -> IsList( x ) and ForAll( x, IsRecord );
    IsProperBool:= x -> x = true or x = false;
    IsFilename:= x -> IsString( x ) and Length( x ) > 0 and
        ( pkgdir = fail or
          ( x[1] <> '/' and IsReadableFile( Concatenation( pkgdir, x ) ) ) );
    IsFilenameList:= x -> IsList( x ) and ForAll( x, IsFilename );
    IsURL := x -> ForAny(["http://","https://","ftp://"], s -> StartsWith(x,s));

    result:= true;

    TestOption:= function( record, name, type, typename )
    if IsBound( record.( name ) ) and not type( record.( name ) ) then
      if ValueOption( "quiet" ) <> true then
        Print( "#E  component `", name, "', if present, must be bound to ",
               typename, "\n" );
      fi;
      result:= false;
      return false;
    fi;
    return true;
    end;

    TestMandat:= function( record, name, type, typename )
    if not IsBound( record.( name ) ) or not type( record.( name ) ) then
      if ValueOption( "quiet" ) <> true then
        Print( "#E  component `", name, "' must be bound to ",
               typename, "\n" );
      fi;
      result:= false;
      return false;
    fi;
    return true;
    end;

    TestMandat( record, "PackageName",
        x -> IsString(x) and 0 < Length(x),
        "a nonempty string" );
    TestMandat( record, "Subtitle", IsString, "a string" );
    TestMandat( record, "Version",
        x -> IsString(x) and 0 < Length(x) and x[1] <> '=',
        "a nonempty string that does not start with `='" );

    # check if the date is valid
    CheckDateValidity := function(x)
      local date;

      if not ( IsString(x) and Length(x) = 10
        and ( ( ForAll( x{ [1,2,4,5,7,8,9,10] }, IsDigitChar )
        and  x{ [3,6] } = "//" ) or ( ForAll( x{ [1,2,3,4,6,7,9,10] }, IsDigitChar ) and x{ [5,8] } = "--" ) ) ) then
        return false;
      elif x{ [3,6] } = "//" then # for the old format split at '/'
        date := List( SplitString( x, "/" ), Int);
      elif x{ [5,8] } = "--" then # for the yyyy-mm-dd format split at '-'
        date := List( SplitString( x, "-" ), Int);
        date := date{ [3,2,1] }; # sort such that date=[dd,mm,yyyy]
      fi;
      return date[2] in [1..12] and date[3] >= 1999 # GAP 4 appeared in 1999
          and date[1] in [1..DaysInMonth( date[2], date[3] )];
    end;

    TestMandat( record, "Date", CheckDateValidity, Concatenation( "a string of the form yyyy-mm-dd or dd/mm/yyyy",
    " that represents a date since 1999") );

    # If the date is in the format `dd/mm/yyyy` a message is printed
    # code to be used after an adaption period for packages
    #if IsBound( record.Date ) and  CheckDateValidity( record.Date ) and record.Date{ [3,6] } = "//" then
    #   Info( InfoPackageLoading, 2, Concatenation( record.PackageName, ": Please be advised to change the date format to `yyyy-mm-dd`") );
    #fi;

    TestMandat( record, "License",
        x -> IsString(x) and 0 < Length(x),
        "a nonempty string containing an SPDX ID" );
    TestMandat( record, "ArchiveURL", IsURL, "a string started with http://, https:// or ftp://" );
    TestMandat( record, "ArchiveFormats", IsString, "a string" );
    TestOption( record, "TextFiles", IsStringList, "a list of strings" );
    TestOption( record, "BinaryFiles", IsStringList, "a list of strings" );
    TestOption( record, "TextBinaryFilesPatterns",
        x -> IsStringList(x) and
             ForAll( x, i -> Length(i) > 0 ) and
             ForAll( x, i -> i[1] in ['T','B'] ),
        "a list of strings, each started with 'T' or 'B'" );
    if Number( [ IsBound(record.TextFiles),
                 IsBound(record.BinaryFiles),
                 IsBound(record.TextBinaryFilesPatterns) ],
               a -> a=true ) > 1 then
      if ValueOption( "quiet" ) <> true then
        Print("#W  only one of TextFiles, BinaryFiles or TextBinaryFilesPatterns\n");
        Print("#W  components must be bound\n");
      fi;
    fi;
    if     TestOption( record, "Persons", IsRecordList, "a list of records" )
       and IsBound( record.Persons ) then
      for subrec in record.Persons do
        TestMandat( subrec, "LastName", IsString, "a string" );
        TestOption( subrec, "FirstNames", IsString, "a string" );
        if not (    IsBound( subrec.IsAuthor )
                 or IsBound( subrec.IsMaintainer ) ) then
          if ValueOption( "quiet" ) <> true then
            Print( "#E  one of the components `IsAuthor', `IsMaintainer' ",
                   "must be bound\n" );
          fi;
          result:= false;
        fi;
        TestOption( subrec, "IsAuthor", IsProperBool, "`true' or `false'" );
        TestOption( subrec, "IsMaintainer", IsProperBool,
            "`true' or `false'" );
        if IsBound( subrec.IsMaintainer ) then
          if subrec.IsMaintainer = true and
               not ( IsBound( subrec.Email ) or
                     IsBound( subrec.WWWHome ) or
                     IsBound( subrec.PostalAddress ) ) then
            if ValueOption( "quiet" ) <> true then
              Print( "#E  one of the components `Email', `WWWHome', `PostalAddress'\n",
                     "#E  must be bound for each package maintainer\n" );
            fi;
            result:= false;
          fi;
        fi;
        TestOption( subrec, "Email", IsString, "a string" );
        TestOption( subrec, "WWWHome", IsURL, "a string started with http://, https:// or ftp://" );
        TestOption( subrec, "PostalAddress", IsString, "a string" );
        TestOption( subrec, "Place", IsString, "a string" );
        TestOption( subrec, "Institution", IsString, "a string" );
      od;
    fi;

    TestMandat( record, "README_URL", IsURL, "a string started with http://, https:// or ftp://" );
    TestMandat( record, "PackageInfoURL", IsURL, "a string started with http://, https:// or ftp://" );

    if TestOption( record, "SourceRepository", IsRecord, "a record" ) then
      if IsBound( record.SourceRepository ) then
        TestMandat( record.SourceRepository, "Type", IsString, "a string" );
        TestMandat( record.SourceRepository, "URL", IsString, "a string" );
      fi;
    fi;
    TestOption( record, "IssueTrackerURL", IsURL, "a string started with http://, https:// or ftp://" );
    TestOption( record, "SupportEmail", IsString, "a string" );
    TestMandat( record, "AbstractHTML", IsString, "a string" );
    TestMandat( record, "PackageWWWHome", IsURL, "a string started with http://, https:// or ftp://" );
    if TestMandat( record, "PackageDoc",
           x -> IsRecord( x ) or IsRecordList( x ),
           "a record or a list of records" ) then
      if IsRecord( record.PackageDoc ) then
        list:= [ record.PackageDoc ];
      else
        list:= record.PackageDoc;
      fi;
      for subrec in list do
        TestMandat( subrec, "BookName", IsString, "a string" );
        if IsBound(subrec.Archive) and ValueOption( "quiet" ) <> true then
          Print("#W  PackageDoc.Archive is withdrawn, use PackageDoc.ArchiveURLSubset instead\n");
        fi;
        TestMandat( subrec, "ArchiveURLSubset", IsFilenameList,
            "a list of strings denoting relative paths to readable files or directories" );
        TestMandat( subrec, "HTMLStart", IsFilename,
                    "a string denoting a relative path to a readable file" );
        TestMandat( subrec, "PDFFile", IsFilename,
                    "a string denoting a relative path to a readable file" );
        TestMandat( subrec, "SixFile", IsFilename,
                    "a string denoting a relative path to a readable file" );
        TestMandat( subrec, "LongTitle", IsString, "a string" );
      od;
    fi;
    if     TestOption( record, "Dependencies", IsRecord, "a record" )
       and IsBound( record.Dependencies ) then
      TestOption( record.Dependencies, "GAP", IsString, "a string" );
      TestOption( record.Dependencies, "NeededOtherPackages",
          comp -> IsList( comp ) and ForAll( comp,
                      l -> IsList( l ) and Length( l ) = 2
                                       and ForAll( l, IsString ) ),
          "a list of pairs `[ <pkgname>, <pkgversion> ]' of strings" );
      TestOption( record.Dependencies, "SuggestedOtherPackages",
          comp -> IsList( comp ) and ForAll( comp,
                      l -> IsList( l ) and Length( l ) = 2
                                       and ForAll( l, IsString ) ),
          "a list of pairs `[ <pkgname>, <pkgversion> ]' of strings" );
      TestOption( record.Dependencies, "ExternalConditions",
          comp -> IsList( comp ) and ForAll( comp,
                      l -> IsString( l ) or ( IsList( l ) and Length( l ) = 2
                                      and ForAll( l, IsString ) ) ),
          "a list of strings or of pairs `[ <text>, <URL> ]' of strings" );

      # If the package is a needed package of GAP then all its needed
      # packages must also occur in the list of needed packages of GAP.
      list:= List( GAPInfo.Dependencies.NeededOtherPackages,
                   x -> LowercaseString( x[1] ) );
      if     IsBound( record.PackageName )
         and IsString( record.PackageName )
         and LowercaseString( record.PackageName ) in list
         and IsBound( record.Dependencies.NeededOtherPackages )
         and IsList( record.Dependencies.NeededOtherPackages ) then
        list:= Filtered( record.Dependencies.NeededOtherPackages,
                         x ->     IsList( x ) and IsBound( x[1] )
                              and IsString( x[1] )
                              and not LowercaseString( x[1] ) in list );
        if not IsEmpty( list ) then
          if ValueOption( "quiet" ) <> true then
            Print( "#E  the needed packages in '",
                   List( list, x -> x[1] ), "'\n",
                   "#E  are currently not needed packages of GAP\n" );
          fi;
          result:= false;
        fi;
      fi;
    fi;
    TestOption( record, "Extensions",
        comp -> IsList( comp ) and ForAll( comp,
                    r -> IsRecord( r ) and
                         IsBound( r.needed ) and IsList( r.needed ) and
                         ForAll( r.needed,
                             l -> IsList( l ) and Length( l ) = 2 and
                                  ForAll( l, IsString ) ) and
                         IsBound( r.filename ) and IsString( r.filename ) ),
        "a list of records with components `needed' and `filename'" );
    TestOption( record, "AvailabilityTest", IsFunction, "a function" );
    TestOption( record, "BannerFunction", IsFunction, "a function" );
    TestOption( record, "BannerString", IsString, "a string" );
    TestOption( record, "TestFile", IsFilename,
                "a string denoting a relative path to a readable file" );
    TestOption( record, "Keywords", IsStringList, "a list of strings" );

    return result;
    end );


#############################################################################
##
#V  GAPInfo.PackagesRestrictions
##
##  <ManSection>
##  <Var Name="GAPInfo.PackagesRestrictions"/>
##
##  <Description>
##  This is a mutable record, each component being the name of a package
##  <A>pkg</A> (in lowercase letters) that is required/recommended to be
##  updated to a certain version,
##  the value being a record with the following components.
##  <P/>
##  <List>
##  <Mark><C>OnInitialization</C></Mark>
##  <Item>
##      a function that takes one argument, the record stored in the
##      <F>PackageInfo.g</F> file of the package,
##      and returns <K>true</K> if the package can be loaded,
##      and returns <K>false</K> if not.
##      The function is allowed to change components of the argument record.
##      It should not print any message,
##      this should be left to the <C>OnLoad</C> component,
##  </Item>
##  <Mark><C>OnLoad</C></Mark>
##  <Item>
##      a function that takes one argument, the record stored in the
##      <F>PackageInfo.g</F> file of the package, and can print a message
##      when the availability of the package is checked for the first time;
##      this message is intended to explain why the package cannot loaded due
##      to the <K>false</K> result of the <C>OnInitialization</C> component,
##      or as a warning about known problems (when the package is in fact
##      loaded), and it might give hints for upgrading the package.
##  </Item>
##  </List>
##  </Description>
##  </ManSection>
##
GAPInfo.PackagesRestrictions := AtomicRecord(rec(
  anupq := MakeImmutable(rec(
    OnInitialization := function( pkginfo )
        if CompareVersionNumbers( pkginfo.Version, "1.3" ) = false then
          return false;
        fi;
        return true;
        end,
    OnLoad := function( pkginfo )
        if CompareVersionNumbers( pkginfo.Version, "1.3" ) = false then
          Print( "  The package `anupq'",
              " should better be upgraded at least to version 1.3,\n",
              "  the given version (", pkginfo.Version,
              ") is known to be incompatible\n",
              "  with the current version of GAP.\n",
              "  It is strongly recommended to update to the ",
              "most recent version, see URL\n",
              "      http://www.math.rwth-aachen.de/~Greg.Gamble/ANUPQ\n" );
        fi;
        end )),

  autpgrp := MakeImmutable(rec(
    OnInitialization := function( pkginfo )
        return true;
        end,
    OnLoad := function( pkginfo )
        if CompareVersionNumbers( pkginfo.Version, "1.1" ) = false then
          Print( "  The package `autpgrp'",
              " should better be upgraded at least to version 1.1,\n",
              "  the given version (", pkginfo.Version,
              ") is known to be incompatible\n",
              "  with the current version of GAP.\n",
              "  It is strongly recommended to update to the ",
              "most recent version, see URL\n",
              "      https://www.gap-system.org/Packages/autpgrp.html\n" );
        fi;
        end )) ));


#############################################################################
##
#F  SuggestUpgrades( versions ) . . compare installed with distributed versions
##
InstallGlobalFunction( SuggestUpgrades, function( suggestedversions )
    local ok, outstr, out, entry, inform, info;

    suggestedversions := Set( suggestedversions, ShallowCopy );
    ok:= true;
    # We collect the output in a string, because availability test may
    # cause some intermediate printing. This way the output of the present
    # function comes after such texts.
    outstr := "";
    out := OutputTextString(outstr, true);
    PrintTo(out, "#I ======================================================",
                 "================ #\n",
                 "#I      Result of 'SuggestUpgrades':\n#I\n"
                 );
    # Deal with the kernel and library versions.
    entry:= First( suggestedversions, x -> x[1] = "GAPLibrary" );
    if entry = fail then
      PrintTo(out,  "#E  no info about suggested GAP library version ...\n" );
      ok:= false;
    elif not CompareVersionNumbers( GAPInfo.Version, entry[2] ) then
      PrintTo(out,  "#E  You are using version ", GAPInfo.Version,
             " of the GAP library.\n",
             "#E  Please upgrade to version ", entry[2], ".\n\n" );
      ok:= false;
    elif not CompareVersionNumbers( entry[2], GAPInfo.Version ) then
      PrintTo(out,  "#E  You are using version ", GAPInfo.Version,
             " of the GAP library.\n",
             "#E  This is newer than the distributed version ",
             entry[2], ".\n\n" );
      ok:= false;
    fi;
    RemoveSet( suggestedversions, entry );

    entry:= First( suggestedversions, x -> x[1] = "GAPKernel" );
    if entry = fail then
      PrintTo(out,  "#E  no info about suggested GAP kernel version ...\n" );
      ok:= false;
    elif not CompareVersionNumbers( GAPInfo.KernelVersion, entry[2] ) then
      PrintTo(out,  "#E  You are using version ", GAPInfo.KernelVersion,
             " of the GAP kernel.\n",
             "#E  Please upgrade to version ", entry[2], ".\n\n" );
      ok:= false;
    elif not CompareVersionNumbers( entry[2], GAPInfo.KernelVersion ) then
      PrintTo(out,  "#E  You are using version ", GAPInfo.KernelVersion,
             " of the GAP kernel.\n",
             "#E  This is newer than the distributed version ",
             entry[2], ".\n\n" );
      ok:= false;
    fi;
    RemoveSet( suggestedversions, entry );

    # Deal with present packages which are not distributed.
    inform := Difference(RecNames(GAPInfo.PackagesInfo),
              List(suggestedversions, x-> LowercaseString(x[1])));
    if not IsEmpty( inform ) then
      PrintTo(out,  "#I  The following GAP packages are present but not ",
                    "officially distributed.\n" );
      for entry in inform do
        info := GAPInfo.PackagesInfo.(entry)[1];
        PrintTo(out,  "#I    ", info.PackageName, " ", info.Version, "\n" );
      od;
      PrintTo(out,  "\n" );
      ok:= false;
    fi;


    # Deal with packages that are not installed.
    inform := Filtered( suggestedversions, entry -> not IsBound(
                   GAPInfo.PackagesInfo.( LowercaseString( entry[1] ) ) )
                 and ForAll( GAPInfo.PackagesInfoRefuseLoad,
                             r -> LowercaseString( entry[1] )
                                  <> LowercaseString( r.PackageName ) ) );
    if not IsEmpty( inform ) then
      PrintTo(out,  "#I  The following distributed GAP packages are ",
                    "not installed.\n" );
      for entry in inform do
        PrintTo(out,  "#I    ", entry[1], " ", entry[2], "\n" );
      od;
      PrintTo(out,  "\n" );
      ok:= false;
    fi;
    SubtractSet( suggestedversions, inform );

    # Deal with packages whose installed versions are not available
    # (without saying anything about the reason).
#T Here it would be desirable to omit those packages that cannot be loaded
#T on the current platform; e.g., Windoofs users need not be informed about
#T packages for which no Windoofs version is available.
    # These packages can be up to date or outdated.
    for entry in suggestedversions do
      Add( entry, InstalledPackageVersion( entry[1] ) );
#T Here we may get print statements from the availability testers;
#T how to avoid this?
    od;
    inform:= Filtered( suggestedversions, entry -> entry[3] = fail );
    if not IsEmpty( inform ) then
      PrintTo(out,  "#I  The following GAP packages are present ",
             "but cannot be used.\n" );
      for entry in inform do
        PrintTo(out,  "#I    ", entry[1], " ",
             GAPInfo.PackagesInfo.( LowercaseString( entry[1] ) )[1].Version,
             "\n" );
        if not ForAny( GAPInfo.PackagesInfo.( LowercaseString( entry[1] ) ),
                   r -> CompareVersionNumbers( r.Version, entry[2] ) ) then
          PrintTo(out,  "#I         (distributed version is newer:   ",
                   entry[2], ")\n" );
        fi;
      od;
      PrintTo(out, "\n" );
      ok:= false;
    fi;
    SubtractSet( suggestedversions, inform );

    # Deal with packages in *newer* (say, dev-) versions than the
    # distributed ones.
    inform:= Filtered( suggestedversions, entry -> not CompareVersionNumbers(
                 entry[2], entry[3] ) );
    if not IsEmpty( inform ) then
      PrintTo(out,
             "#I  Your following GAP packages are *newer* than the ",
             "distributed version.\n" );
      for entry in inform do
        PrintTo(out,  "#I    ", entry[1], " ", entry[3],
               " (distributed is ", entry[2], ")\n" );
      od;
      PrintTo(out,  "\n" );
      ok:= false;
    fi;
    # Deal with packages whose installed versions are not up to date.
    inform:= Filtered( suggestedversions, entry -> not CompareVersionNumbers(
                 entry[3], entry[2] ) );
    if not IsEmpty( inform ) then
      PrintTo(out,
             "#I  The following GAP packages are available but outdated.\n" );
      for entry in inform do
        PrintTo(out,  "#I    ", entry[1], " ", entry[3],
               " (please upgrade to ", entry[2], ")\n" );
      od;
      PrintTo(out,  "\n" );
      ok:= false;
    fi;

    if ok then
      PrintTo(out,  "#I  Your GAP installation is up to date with the ",
      "official distribution.\n\n" );
    fi;
    CloseStream(out);
    Print( outstr );
    end );


#############################################################################
##
#F  BibEntry( "GAP"[, <key>] )
#F  BibEntry( <pkgname>[, <key>] )
#F  BibEntry( <pkginfo>[, <key>] )
##
Unicode:= "dummy";
Encode:= "dummy";

InstallGlobalFunction( BibEntry, function( arg )
    local key, pkgname, pkginfo, GAP, ps, val, entry, author;

    key:= false;
    if   Length( arg ) = 1 and IsString( arg[1] ) then
      pkgname:= arg[1];
    elif Length( arg ) = 2 and IsString( arg[1] ) and IsString( arg[2] ) then
      pkgname:= arg[1];
      key:= arg[2];
    elif Length( arg ) = 1 and IsRecord( arg[1] ) then
      pkginfo:= arg[1];
    elif Length( arg ) = 2 and IsRecord( arg[1] ) and IsString( arg[2] ) then
      pkginfo:= arg[1];
      key:= arg[2];
    else
      Error( "usage: BibEntry( \"GAP\"[, <key>] ), ",
             "BibEntry( <pkgname>[, <key>] ), ",
             "BibEntry( <pkginfo>[, <key>] )" );
    fi;

    GAP:= false;
    if IsBound( pkgname ) then
      if pkgname = "GAP" then
        GAP:= true;
      else
        pkginfo:= InstalledPackageVersion( pkgname );
        pkginfo:= First( PackageInfo( pkgname ), r -> r.Version = pkginfo );
        if pkginfo = fail then
          return "";
        fi;
      fi;
    fi;

    if key = false then
      if GAP then
        key:= "GAP";
      else
        key:= pkginfo.PackageName;
      fi;
    fi;

    # Make sure that output of BibEntry is in UTF-8 encoding (also
    # if PackageInfo.g is in latin1 encoding)
    ps:= function( str )
      local uni;

      uni:= Unicode( str, "UTF-8" );
      if uni = fail then
        uni:= Unicode( str, "ISO-8859-1" );
      fi;
      return Encode( uni, "UTF-8" );
    end;

    # According to <Cite Key="La85"/>,
    # the supported fields of a Bib&TeX; entry of <C>@misc</C> type are
    # the following.
    # <P/>
    # <List>
    # <Mark><C>author</C></Mark>
    # <Item>
    #   computed from the <C>Persons</C> component of the package,
    #   not distinguishing authors and maintainers,
    #   keeping the ordering of entries,
    # </Item>
    # <Mark><C>title</C></Mark>
    # <Item>
    #   computed from the <C>PackageName</C> and <C>Subtitle</C> components
    #   of the package,
    # </Item>
    # <Mark><C>month</C> and <C>year</C></Mark>
    # <Item>
    #   computed from the <C>Date</C> component of the package,
    # </Item>
    # <Mark><C>note</C></Mark>
    # <Item>
    #   the string <C>"Refereed \\textsf{GAP} package"</C> or
    #   <C>"\\textsf{GAP} package"</C>,
    # </Item>
    # <Mark><C>howpublished</C></Mark>
    # <Item>
    #   the <C>PackageWWWHome</C> component of the package.
    # </Item>
    # </List>
    # <P/>
    # Also the <C>edition</C> component seems to be supported;
    # it is computed from the <C>Version</C> component of the package.

    # Bib&Tex;'s <C>@manual</C> type seems to be not appropriate,
    # since this type does not support a URL component
    # in the base bib styles of La&TeX;.
    # Instead we can use the <C>@misc</C> type and its <C>howpublished</C>
    # component.
    # We put the version information into the <C>title</C> component since
    # the <C>edition</C> component is not supported in the base styles.

    if GAP then
      val:= SplitString( GAPInfo.Date, "-" );
      if Length( val ) = 3 then
        if Int( val[2] ) in [ 1 .. 12 ] then
          val:= Concatenation( "  <month>", NameMonth[ Int( val[2] ) ],
                               "</month>\n  <year>", val[1], "</year>\n" );
        else
          val:= Concatenation( "  <month>", val[2],
                               "</month>\n  <year>", val[1], "</year>\n" );
        fi;
      else
        val:= "";
      fi;
      entry:= Concatenation(
        "<entry id=\"", key, "\"><misc>\n",
        "  <title><C>GAP</C> –",
        " <C>G</C>roups, <C>A</C>lgorithms,\n",
        "         and <C>P</C>rogramming,",
        " <C>V</C>ersion ", GAPInfo.Version, "</title>\n",
        "  <howpublished><URL>https://www.gap-system.org</URL></howpublished>\n",
        val,
        "  <key>GAP</key>\n",
        "  <keywords>groups; *; gap; manual</keywords>\n",
        "  <other type=\"organization\">The GAP <C>G</C>roup</other>\n",
        "</misc></entry>" );
    else
      entry:= Concatenation( "<entry id=\"", key, "\"><misc>\n" );
      author:= List( Filtered( pkginfo.Persons,
        person -> person.IsAuthor or person.IsMaintainer ),
          person -> Concatenation(
            "    <name>",
            CallFuncList( function(x) if IsBound( x.FirstNames ) then
              return Concatenation( "<first>", person.FirstNames, "</first>" );
              else return ""; fi; end, [ person ] ),
            "<last>", person.LastName, "</last></name>\n" ) );
      if not IsEmpty( author ) then
        Append( entry, Concatenation(
          "  <author>\n",
          ps( Concatenation( author ) ),
          "  </author>\n" ) );
      fi;
      Append( entry, Concatenation(
        "  <title><C>", pkginfo.PackageName, "</C>" ) );
      if IsBound( pkginfo.Subtitle ) then
        Append( entry, Concatenation(
          ", <C>", ps( pkginfo.Subtitle ), "</C>" ) );
      fi;
      if IsBound( pkginfo.Version ) then
        Append( entry, Concatenation(
          ",\n         <C>V</C>ersion ", pkginfo.Version ) );
      fi;
      Append( entry, "</title>\n" );
      if IsBound( pkginfo.PackageWWWHome ) then
        Append( entry, Concatenation(
          "  <howpublished><URL>", pkginfo.PackageWWWHome,
          "</URL></howpublished>\n" ) );
      fi;
      if IsBound( pkginfo.Date ) and IsDenseList( pkginfo.Date )
                                 and Length( pkginfo.Date ) = 10 then
        if Int( pkginfo.Date{ [ 4, 5 ] } ) in [ 1 .. 12 ] then
          Append( entry, Concatenation(
            "  <month>", NameMonth[ Int( pkginfo.Date{ [ 4, 5 ] } ) ],
            "</month>\n",
            "  <year>", pkginfo.Date{ [ 7 .. 10 ] }, "</year>\n" ) );
        else
          Append( entry, Concatenation(
            "  <month>", pkginfo.Date{ [ 4, 5 ] }, "</month>\n",
            "  <year>", pkginfo.Date{ [ 7 .. 10 ] }, "</year>\n" ) );
        fi;
      fi;
      Append( entry, "  <note>" );
#     Append( entry, "<Package>GAP</Package> package</note>\n" );
      Append( entry, "GAP package</note>\n" );
      if IsBound( pkginfo.Keywords ) then
        Append( entry, Concatenation(
          "  <keywords>",
          JoinStringsWithSeparator( pkginfo.Keywords, "; " ),
          "</keywords>\n" ) );
      fi;
      Append( entry, "</misc></entry>" );
    fi;

    return entry;
end );

# dummy assignments to functions to be read later in the GAPDoc package
ParseBibXMLextString:= "dummy";
StringBibXMLEntry:= "dummy";

InstallGlobalFunction( Cite, function(arg)
  local name, bib, key, parse, year, en, pkginfo;
  if Length(arg)=0 then
    name:="GAP";
  else
    name := NormalizedWhitespace(arg[1]);
  fi;
  if LowercaseString(name) = "gap" then
    name:="GAP";
  else
    # use spelling as in packages PackageInfo.g
    pkginfo := InstalledPackageVersion(name);
    pkginfo := First(PackageInfo(name), r-> r.Version = pkginfo);
    name := pkginfo.PackageName;
  fi;
  if Length(arg)<=1 then
    bib:= BibEntry( name );
  elif Length(arg)>2 then
    Error("`Cite' takes no more than two arguments");
  else
    key:=arg[2];
    bib:= BibEntry( name, key );
  fi;
  if bib="" then
    Print("WARNING: No working version of package ", name, " is available!\n");
    return;
  fi;
  # special handling for "The GAP Team"
  bib:= ReplacedString( bib,
            "<name><first>The</first><last>GAP Team</last></name>",
            "<name><last>The GAP Team</last></name>" );
  parse:= ParseBibXMLextString( bib );
  # use encoding of terminal for printing
  en := function(str)
    local enc;
    enc := GAPInfo.TermEncoding;
    if enc = "UTF-8" then
      return str;
    else
      return Encode(Unicode(str, "UTF-8"), enc);
    fi;
  end;
  Print("Please use one of the following samples\n",
        "to cite ", en(name), " version from this installation\n\n");

  Print("Text:\n\n");
  Print( en(StringBibXMLEntry( parse.entries[1], "Text" )) );

  Print("HTML:\n\n");
  Print( en(StringBibXMLEntry( parse.entries[1], "HTML" )) );

  Print("BibXML:\n\n");
  Print( en(bib), "\n\n" );

  Print("BibTeX:\n\n");
  Print( en(StringBibXMLEntry( parse.entries[1], "BibTeX" )), "\n" );

  if name="GAP" then
    # The format of 'GAPInfo.Date' in released GAP is <year>-<month>-<day>.
    # In 'GAP.dev', the value is "today".
    year:= SplitString( GAPInfo.Date, "-" )[1];

    Print("If you are not using BibTeX, here is the bibliography entry produced \n",
          "by BibTeX (in bibliography style `alpha'):\n\n",
          "\\bibitem[GAP]{GAP4}\n",
          "\\emph{GAP -- Groups, Algorithms, and Programming}, ",
          "Version ", GAPInfo.Version, ",\n",
          "The GAP~Group (", year, "), \\verb+https://www.gap-system.org+.\n\n");
    Print(
    "If you have (predominantly) used one or more particular GAP packages,\n",
    "please cite these packages in addition to GAP itself (either check the\n",
    "package documentation for the suggestions, or use a scheme like:\n\n",

    "[PKG]\n",
    "<Author name(s)>, <package name>, <package long title>, \n",
    "Version <package version> (<package date>), (GAP package),\n",
    "<package URL>.\n\n",

    "You may also produce citation samples for a GAP package by entering\n\n",
    "    Cite(\"packagename\");\n\n",
    "in a GAP installation with the working version of this package available.\n\n");
  fi;
end);

Unbind( ParseBibXMLextString );
Unbind( StringBibXMLEntry );
Unbind( Unicode );
Unbind( Encode );


#############################################################################
##
#F  PackageVariablesInfo( <pkgname>, <version> )
##
NamesSystemGVars := "dummy";   # is not yet defined when this file is read
NamesUserGVars   := "dummy";

InstallGlobalFunction( PackageVariablesInfo, function( pkgname, version )
    local test, cache, cache2, PkgName, realname, new, new_up_to_case,
          redeclared, newmethod, pos, key_dependent_operation, rules,
          localBindGlobal, rule, loaded, args, docmark, done, result,
          subrule, added, prev, subresult, entry, isrelevant, guesssource,
          protected;

    pkgname:= LowercaseString( pkgname );
    test:= TestPackageAvailability( pkgname, version );

    # If the function has been called for this package then
    # return the stored value.
    cache:= Concatenation( pkgname, ":", version );
    if not IsBound( GAPInfo.PackageVariablesInfo ) then
      GAPInfo.PackageVariablesInfo:= rec();
    elif IsBound( GAPInfo.PackageVariablesInfo.( cache ) ) then
      return GAPInfo.PackageVariablesInfo.( cache );
    elif version = "" and test = true then
      cache2:= Concatenation( pkgname, ":",
                   InstalledPackageVersion( pkgname ) );
      if IsBound( GAPInfo.PackageVariablesInfo.( cache2 ) ) then
        return GAPInfo.PackageVariablesInfo.( cache2 );
      fi;
    fi;

    # Check that the package is available but not yet loaded.
    if test = true then
      Info( InfoWarning, 1,
            "the package `", pkgname, "' is already loaded" );
      return [];
    elif test = fail then
      if version = "" then
        Info( InfoWarning, 1,
              "the package `", pkgname, "' cannot be loaded" );
      else
        Info( InfoWarning, 1,
              "the package `", pkgname, "' cannot be loaded in version `",
              version, "'" );
      fi;
      return [];
    fi;

    PkgName:= GAPInfo.PackagesInfo.( pkgname )[1].PackageName;

    realname:= function( name )
        if Last(name) = '@' then
          return Concatenation( name, PkgName );
        else
          return name;
        fi;
    end;

    new:= function( entry )
        local name;

        name:= realname( entry[1][1] );
        if not name in GAPInfo.data.varsThisPackage then
          return fail;
        elif Length( entry[1] ) = 3 and entry[1][3] = "mutable"
             and Length( name  ) > 9 and name{ [ 1 .. 8 ] } = "Computed"
             and Last(name) = 's'
             and IsBoundGlobal( name{ [ 9 .. Length( name ) - 1 ] } ) then
          return fail;
        elif Length( entry[1] ) = 2
             and Length( name  ) > 3 and name{ Length( name ) - [1,0] } = "Op"
             and IsBoundGlobal( name{ [ 1 .. Length( name ) - 2 ] } )
             and ForAny( GAPInfo.data.KeyDependentOperation[2],
                         x -> x[1][1] = name{ [ 1 .. Length( name ) - 2 ] }
                              and x[2] = entry[2]
                              and x[3] = entry[3] ) then
          # Ignore the declaration of the operation created by
          # `KeyDependentOperation'.
          # (We compare filename and line number in the file with these values
          # for the call of `KeyDependentOperation'.)
          return fail;
        else
          return [ name, ValueGlobal( name ), entry[2], entry[3] ];
        fi;
      end;

    new_up_to_case:= function( entry )
        local name;

        name:= realname( entry[1][1] );
        if   not name in GAPInfo.data.varsThisPackage then
          return fail;
        elif LowercaseString( name ) in GAPInfo.data.lowercase_vars then
          return [ name, ValueGlobal( name ), entry[2], entry[3] ];
        else
          return fail;
        fi;
      end;

    redeclared:= function( entry )
        local name;

        name:= realname( entry[1][1] );
        if   not name in GAPInfo.data.varsThisPackage then
          return [ name, ValueGlobal( name ), entry[2], entry[3] ];
        else
          return fail;
        fi;
      end;

    newmethod:= function( entry )
      local name, setter, getter;

      name:= NameFunction( entry[1][1] );
      if IsString( entry[1][2] ) then
        if entry[1][2] in [ "system setter", "system mutable setter",
                            "default method, does nothing" ] then
          setter:= entry[1][1];
          if ForAny( ATTRIBUTES,
                     attr -> IsIdenticalObj( setter, attr[4] ) ) then
            return fail;
          fi;
        elif entry[1][2] in [ "system getter",
          "default method requiring categories and checking properties" ] then
          getter:= entry[1][1];
          if ForAny( ATTRIBUTES,
                     attr -> IsIdenticalObj( getter, attr[3] ) ) then
            return fail;
          fi;
        elif entry[1][2] in [ "default method" ] then
          # Ignore the default methods (for attribute and operation)
          # that are installed in calls to `KeyDependentOperation'.
          # (We compare filename and line number in the file
          # with these values for the call of `KeyDependentOperation'.)
          if 9 < Length( name  ) and name{ [ 1 .. 8 ] } = "Computed"
             and Last(name) = 's'
             and IsBoundGlobal( name{ [ 9 .. Length( name ) - 1 ] } )
             and ForAny( GAPInfo.data.KeyDependentOperation[2],
                         x -> x[1][1] = name{ [ 9 .. Length( name ) - 1 ] }
                              and x[2] = entry[2]
                              and x[3] = entry[3] ) then
            return fail;
          elif IsBoundGlobal( name )
               and ForAny( GAPInfo.data.KeyDependentOperation[2],
                           x -> x[1][1] = name
                                and x[2] = entry[2]
                                and x[3] = entry[3] ) then
            return fail;
          fi;
        fi;
      fi;

      # Omit methods for `FlushCaches'.
      if name = "FlushCaches" then
        return fail;
      fi;

      # Extract a comment if possible.
      if IsString( entry[1][2] ) then
        # Store also the comment for this method installation.
        return [ name, Last(entry[1]),
                 entry[2], entry[3], entry[1][2] ];
      else
        pos:= PositionProperty( entry[1],
                                x -> IsList( x ) and not IsEmpty( x )
                                     and ForAll( x, IsString ) );
        if pos <> fail then
          # Create a comment from the list of strings that describe filters.
          return [ NameFunction( entry[1][1] ),
                   Last(entry[1]),
                   entry[2], entry[3], Concatenation( "for ",
                   JoinStringsWithSeparator( entry[1][ pos ], ", " ) ) ];
        else
          # We know no comment.
          return [ NameFunction( entry[1][1] ),
                   Last(entry[1]),
                   entry[2], entry[3] ];
        fi;
      fi;
      end;

    key_dependent_operation:= IdFunc;

    # List the cases to be dealt with.
    rules:= [
      [ "DeclareGlobalFunction",
        [ "new global functions", new ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareGlobalVariable",
        [ "new global variables", new ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "BindGlobal",
        [ "new global variables", new ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareOperation",
        [ "new operations", new ],
        [ "redeclared operations", redeclared ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareAttribute",
        [ "new attributes", new ],
        [ "redeclared attributes", redeclared ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareProperty",
        [ "new properties", new ],
        [ "redeclared properties", redeclared ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareCategory",
        [ "new categories", new ],
        [ "redeclared categories", redeclared ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareRepresentation",
        [ "new representations", new ],
        [ "redeclared representations", redeclared ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareFilter",
        [ "new plain filters", new ],
        [ "redeclared plain filters", redeclared ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "InstallMethod",
        [ "new methods", newmethod ] ],
      [ "InstallOtherMethod",
        [ "new other methods", newmethod ] ],
      [ "DeclareSynonymAttr",
        [ "new synonyms of attributes", new ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareSynonym",
        [ "new synonyms", new ],
        [ "globals that are new only up to case", new_up_to_case ] ],
      [ "DeclareInfoClass",
        [ "new info classes", new ] ],
      [ "KeyDependentOperation",
        [ "KeyDependentOperation", key_dependent_operation ] ],
      ];

    # Save the relevant global variables, and replace them.
    GAPInfo.data:= rec( userGVars:= NamesUserGVars(),
                        varsThisPackage:= [],
                        pkgpath:= test,
                        pkgname:= pkgname );

    GAPInfo.data.lowercase_vars:= List( Union( NamesSystemGVars(),
        GAPInfo.data.userGVars ), LowercaseString );

    localBindGlobal:= BindGlobal;

    Perform( rules, function(rule)
      local orig_func, usage_locations;
      orig_func:= ValueGlobal( rule[1] );
      usage_locations:= [];
      GAPInfo.data.( rule[1] ):= [ orig_func, usage_locations ];
      MakeReadWriteGlobal( rule[1] );
      UnbindGlobal( rule[1] );
      localBindGlobal( rule[1], function( arg )
        local infile, path;
        infile:= INPUT_FILENAME();
        path:= GAPInfo.data.pkgpath;
        if Length( path ) <= Length( infile ) and
           infile{ [ 1 .. Length( path ) ] } = path then
          Add( usage_locations,
               [ StructuralCopy( arg ), infile, INPUT_LINENUMBER() ] );
        fi;
        CallFuncList( orig_func, arg );
      end );
    end );

    # Redirect `ReadPackage'.
    GAPInfo.data.ReadPackage:= ReadPackage;
    MakeReadWriteGlobal( "ReadPackage" );
    UnbindGlobal( "ReadPackage" );
    localBindGlobal( "ReadPackage",
        function( arg )
        local pos, pkgname, before, res, after;
        if Length( arg ) = 1 then
          pos:= Position( arg[1], '/' );
          pkgname:= LowercaseString( arg[1]{[ 1 .. pos - 1 ]} );
        elif Length( arg ) = 2 then
          pkgname:= LowercaseString( arg[1] );
        else
          pkgname:= fail;
        fi;
        if pkgname = GAPInfo.data.pkgname then
          before:= NamesUserGVars();
        fi;
        res:= CallFuncList( GAPInfo.data.ReadPackage, arg );
        if pkgname = GAPInfo.data.pkgname then
          after:= NamesUserGVars();
          UniteSet( GAPInfo.data.varsThisPackage,
            Filtered( Difference( after, before ), IsBoundGlobal ) );
        fi;
        return res;
        end );

    # Load the package `pkgname'.
    loaded:= LoadPackage( pkgname );

    # Put the original global variables back.
    for rule in rules do
      MakeReadWriteGlobal( rule[1] );
      UnbindGlobal( rule[1] );
      localBindGlobal( rule[1], GAPInfo.data.( rule[1] )[1] );
    od;
    MakeReadWriteGlobal( "ReadPackage" );
    UnbindGlobal( "ReadPackage" );
    localBindGlobal( "ReadPackage", GAPInfo.data.ReadPackage );

    if not loaded then
      Print( "#E  the package `", pkgname, "' could not be loaded\n" );
      return [];
    fi;

    # Functions are printed together with their argument lists.
    args:= function( func )
      local num, nam, str;

      if not IsFunction( func ) then
        return "";
      fi;
      num:= NumberArgumentsFunction( func );
      nam:= NamesLocalVariablesFunction( func );
      if num = -1 then
        str:= "arg";
      elif nam = fail then
        str:= "...";
      else
        str:= JoinStringsWithSeparator( nam{ [ 1 .. num ] }, ", " );
      fi;
      return Concatenation( "( ", str, " )" );
    end;

    # Mark undocumented globals with an asterisk.
    docmark:= function( nam )
      if not ( IsBoundGlobal( nam ) and IsDocumentedWord( nam ) ) then
        return "*";
      else
        return "";
      fi;
    end;

    # Prepare the output.
    done:= [];
    result:= [];
    rules:= Filtered( rules, x -> x[1] <> "KeyDependentOperation" );
    for rule in rules do
      for subrule in rule{ [ 2 .. Length( rule ) ] } do
        added:= [];
        for entry in Filtered( List( GAPInfo.data.( rule[1] )[2],
                                     x -> subrule[2]( x ) ),
                               x -> x <> fail ) do
          if Length( entry ) = 5 then
            Add( added, [ [ entry[1], args( entry[2] ),
                            docmark( entry[1] ), entry[5] ],
                          [ entry[3], entry[4] ] ] );
          else
            Add( added, [ [ entry[1], args( entry[2] ),
                            docmark( entry[1] ) ],
                          [ entry[3], entry[4] ] ] );
          fi;
        od;
        if not IsEmpty( added ) then
          prev:= First( result, x -> x[1] = subrule[1] );
          if prev = fail then
            Add( result, [ subrule[1], added ] );
          else
            Append( prev[2], added );
          fi;
          UniteSet( done, List( added, x -> x[1][1] ) );
        fi;
      od;
    od;
    for subresult in result do
      Sort( subresult[2] );
    od;

    # Mention the remaining new globals.
    isrelevant:= function( name )
      local name2, attr;

      # Omit variables that are not bound anymore.
      # (We have collected the new variables file by file, and it may happen
      # that some of them become unbound in the meantime.)
      if not IsBoundGlobal( name ) then
        return false;
      fi;

      # Omit `Set<attr>' and `Has<attr>' type variables.
      if 3 < Length( name ) and name{ [ 1 .. 3 ] } in [ "Has", "Set" ] then
        name2:= name{ [ 4 .. Length( name ) ] };
        if not IsBoundGlobal( name2 ) then
          return true;
        fi;
        attr:= ValueGlobal( name2 );
        if ForAny( ATTRIBUTES, entry -> IsIdenticalObj( attr, entry[3] ) ) then
          return false;
        fi;
      fi;

      # Omit operation and attribute created by `KeyDependentOperation'.
      if 9 < Length( name  ) and name{ [ 1 .. 8 ] } = "Computed"
         and Last(name) = 's'
         and IsBoundGlobal( name{ [ 9 .. Length( name ) - 1 ] } )
         and ForAny( GAPInfo.data.KeyDependentOperation[2],
                     x -> x[1][1] = name{ [ 9 .. Length( name ) - 1 ] } ) then
        return false;
      fi;
      if 3 < Length( name  ) and name{ Length( name ) - [1,0] } = "Op"
         and IsBoundGlobal( name{ [ 1 .. Length( name ) - 2 ] } )
         and ForAny( GAPInfo.data.KeyDependentOperation[2],
                     x -> x[1][1] = name{ [ 1 .. Length( name ) - 2 ] } ) then
        return false;
      fi;

      return true;
    end;

    added:= Filtered( Difference( GAPInfo.data.varsThisPackage, done ),
                      isrelevant );

    # Distinguish write protected variables from others.
    guesssource:= function( nam )
      local val;

      val:= ValueGlobal( nam );
      if IsFunction( val ) then
        return [ FilenameFunc( val ), StartlineFunc( val ) ];
      else
        return [ fail, fail ];
      fi;
    end;

    protected:= Filtered( added, IsReadOnlyGVar );
    if not IsEmpty( protected ) then
      Add( result, [ "other new globals (write protected)",
                     List( SortedList( protected ),
                           nam -> [ [ nam, args( ValueGlobal( nam ) ),
                                      docmark( nam ) ],
                                    guesssource( nam ) ] ) ] );
    fi;
    added:= Difference( added, protected );
    if not IsEmpty( added ) then
      Add( result, [ "other new globals (not write protected)",
                     List( SortedList( added ),
                           nam -> [ [ nam, args( ValueGlobal( nam ) ),
                                      docmark( nam ) ],
                                    guesssource( nam ) ] ) ] );
    fi;

    # Delete the auxiliary component from `GAPInfo'.
    Unbind( GAPInfo.data );

    # Store the data.
    GAPInfo.PackageVariablesInfo.( cache ):= result;
    if version = "" then
      Append( cache, InstalledPackageVersion( pkgname ) );
      GAPInfo.PackageVariablesInfo.( cache ):= result;
    fi;

    return result;
    end );

Unbind( NamesSystemGVars );
Unbind( NamesUserGVars );


#############################################################################
##
#F  ShowPackageVariables( <pkgname>[, <version>][, <arec>] )
##
InstallGlobalFunction( ShowPackageVariables, function( arg )
    local version, arec, pkgname, info, show, documented, undocumented,
          private, result, len, format, entry, first, subentry, str;

    # Get and check the arguments.
    version:= "";
    arec:= rec();
    if   Length( arg ) = 1 and IsString( arg[1] ) then
      pkgname:= LowercaseString( arg[1] );
    elif Length( arg ) = 2 and IsString( arg[1] ) and IsString( arg[2] ) then
      pkgname:= LowercaseString( arg[1] );
      version:= arg[2];
    elif Length( arg ) = 2 and IsString( arg[1] ) and IsRecord( arg[2] ) then
      pkgname:= LowercaseString( arg[1] );
      arec:= arg[2];
    elif Length( arg ) = 3 and IsString( arg[1] ) and IsString( arg[2] )
                           and IsRecord( arg[3] ) then
      pkgname:= LowercaseString( arg[1] );
      version:= arg[2];
      arec:= arg[3];
    else
      Error( "usage: ShowPackageVariables( <pkgname>[, <version>]",
             "[, <arec>] )" );
    fi;

    # Compute the data.
    info:= PackageVariablesInfo( pkgname, version );

    # Evaluate the optional record.
    if IsBound( arec.show ) and IsList( arec.show ) then
      show:= arec.show;
    else
      show:= List( info, entry -> entry[1] );
    fi;
    documented:= not IsBound( arec.showDocumented )
                 or arec.showDocumented <> false;
    undocumented:= not IsBound( arec.showUndocumented )
                   or arec.showUndocumented <> false;
    private:= not IsBound( arec.showPrivate )
              or arec.showPrivate <> false;

    # Render the relevant data.
    result:= "";
    len:= SizeScreen()[1] - 2;
    if IsBoundGlobal( "FormatParagraph" ) then
      format:= ValueGlobal( "FormatParagraph" );
    else
      format:= function( arg ) return Concatenation( arg[1], "\n" ); end;
    fi;
    for entry in info do
      if entry[1] in show then
        first:= true;
        for subentry in entry[2] do
          if ( ( documented and subentry[1][3] = "" ) or
               ( undocumented and subentry[1][3] = "*" ) ) and
             ( private or not '@' in subentry[1][1] ) then
            if first then
              Append( result, entry[1] );
              Append( result, ":\n" );
              first:= false;
            fi;
            Append( result, "  " );
            for str in subentry[1]{ [ 1 .. 3 ] } do
              Append( result, str );
            od;
            Append( result, "\n" );
            if Length( subentry[1] ) = 4 and not IsEmpty( subentry[1][4] ) then
              Append( result,
                      format( subentry[1][4], len, "left", [ "    ", "" ] ) );
            fi;
          fi;
        od;
        if not first then
          Append( result, "\n" );
        fi;
      fi;
    od;

    # Show the relevant data.
    if IsBound( arec.Display ) then
      arec.Display( result );
    else
      Print( result );
    fi;
    end );

[Konzepte0.88Was zu einem Entwurf gehörtWie die Entwicklung von Software durchgeführt wird2026-05-01]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge