prjbase/src/PrjDesc.m3


---------------------------------------------------------------------------
MODULE PrjDesc;

IMPORT Text, TextSeq, TextTextTbl, FileRd, Rd, FileWr, Wr, Pathname,
       OSError, Thread, Fmt, Time, RCS_Date, TextWr, MxConfig;
IMPORT TextTextTextTbl, PkgBase, PoolSet, DependencyGraph, FSUtils,
       StdDepGraphNodeSeq, Tag, TextUtils, PathRepr, RegEx, Checkpoint,
       ProcessEnv, MsgX, MsgIF, Confirmation, PkgVC;
IMPORT SMsg AS Msg;
FROM TextReadingUtils IMPORT GetTokenOrString;
FROM TextUtils IMPORT MemberOfTextSeq, SubstEnvVars, SubstituteVariables;
---------------------------------------------------------------------------
REVEAL
  T = Public BRANDED "PrjDesc Type 0.0" OBJECT
    collectionRootOrig : TEXT;
    collectionRoot     : TEXT;
    collectionList : TextSeq.T;
    collectionTbl  : TextTextTbl.T; (* collection names -> path *)
    packageList    : TextSeq.T;
    ignoredList    : TextSeq.T;
    missingList    : TextSeq.T;
    locationTbl    : TextTextTbl.T; (* package names -> path *)
    packageTbl     : TextTextTbl.T; (* package names -> collection names *)
    snapshotTbl    : TextTextTextTbl.T;
    releaseTbl     : TextTextTextTbl.T;
    poolset        : PoolSet.T;
    pkgKind        : PkgBase.Kind;
    depGraph       : DependencyGraph.T;
    depsMandatory  : BOOLEAN;
    externalShell  : TEXT;
    varTbl         : TextTextTbl.T;
    stateLabelTbl  : TextTextTextTbl.T;
    oldState       : Checkpoint.T;
    changedPkgDirs : TextSeq.T;
    useCache       : BOOLEAN;
    env            : ProcessEnv.T;
    msgif          : MsgIF.T;
    pkgvcAcc       : PoolSet.PkgVCAccessor;
    locs           : TextSeq.T := NIL;
    locsText       : TEXT := NIL;
    (* attributes *)
    name  : TEXT;
    desc  : TEXT;
    user  : TEXT;
    cdate : Time.T;
    mdate : Time.T;
  METHODS
    addRoot(p : Pathname.T) : Pathname.T := AddRoot;
    varTableCopy() : TextTextTbl.T := VarTableCopy;
  OVERRIDES
    init := Init;
    reinit := ReInit;
    parse := Parse;
    loadFile := LoadFile;
    pkgsOkay := PkgsOkay;
    getPoolSet := GetPoolSet;
    setExternalShell := SetExternalShell;
    setPreferredPkgKind := SetPreferredPkgKind;
    defineGlobalVar := DefineGlobalVar;
    deleteGlobalVar := DeleteGlobalVar;
    defineGlobalVars := DefineGlobalVars;
    collectionPath := CollectionPath;
    packages := Packages;
    locations := Locations;
    packagesCollection := PackagesCollection;
    collections := Collections;
    collectionsLocation := CollectionsLocation;
    snapshots := Snapshots;
    releases := Releases;
    snapshot := Snapshot;
    release := Release;
    defineSnapshot := DefineSnapshot;
    defineRelease := DefineRelease;
    getTags := GetTags;
    newSnapshot := NewSnapshot;
    newRelease := NewRelease;
    checkoutSnapshot := CheckoutSnapshot;
    checkoutRelease := CheckoutRelease;
    checkoutHead := CheckoutHead;
    checkoutTrunkOrBranchHead := CheckoutTrunkOrBranchHead;
    checkoutPackages := CheckoutPackages;
    readDepGraph  := ReadDepGraph;
    buildDepGraph := BuildDepGraph;
    writeDepGraph := WriteDepGraph;
    readDepGraphAsText  := ReadDepGraphAsText;
    writeDepGraphAsText := WriteDepGraphAsText;
    write := Write;
    writeSnapshot := WriteSnapshot;
    writeRelease := WriteRelease;
    packageUpdateSequence := PackageUpdateSequence;
    ignoredPackages := IgnoredPackages;
    missingPackages := MissingPackages;
    applyToPackages := ApplyToPackages;
    selectPackages  := SelectPackages;
    applyCmdListDirectly := ApplyCmdListDirectly;
    selectByCmdList := SelectByCmdList;
    dependendPackages := DependendPackages;
    packageDependencies := PackageDependencies;
    addDependingPackages := AddDependingPackages;
    modifiedPackages := ModifiedPackages;
    modifiedAndDependingPackages := ModifiedAndDependingPackages;
    upToDatePackages := UpToDatePackages;
    outOfDatePackages := OutOfDatePackages;
    outOfDateAndDependingPackages := OutOfDateAndDependingPackages;
    packagesWithConflicts := PackagesWithConflicts;
    testAllPackagesReleased := TestAllPackagesReleased;
    testNoPackageModified := TestNoPackageModified;
    checkCurrentLabels := CheckCurrentLabels;
    checkCurrentLabelsGen := CheckCurrentLabelsGen;
    cacheAllStateLabels := CacheAllStateLabels;
    checkLabelsOfSnapshot := CheckLabelsOfSnapshot;
    checkLabelsOfRelease := CheckLabelsOfRelease;
    newCheckpoint := NewCheckpoint;
    loadNewCheckpoint := LoadNewCheckpoint;
    writeCheckpoint := WriteCheckpoint;
    invalidateCachedUnsureVersionInfo := InvalidateCachedUnsureVersionInfo;
    invalidateCachedBuildInfo := InvalidateCachedBuildInfo;
    getName := GetName;
    getCreationDate := GetCreationDate;
    getModificationDate := GetModificationDate;
    getUser := GetUser;
    getDescription := GetDescription;
    setName := SetName;
    setCreationDate := SetCreationDate;
    setModificationDate := SetModificationDate;
    setUser := SetUser;
    setDescription := SetDescription;
    logText := LogText;
    snapshotText := SnapshotText;
    releaseText := ReleaseText;
    toText := ToText;
  END;
---------------------------------------------------------------------------
PROCEDURE GetName(self : T) : TEXT =
  BEGIN
    RETURN self.name;
  END GetName;
---------------------------------------------------------------------------
PROCEDURE GetCreationDate(self : T) : Time.T =
  BEGIN
    RETURN self.cdate;
  END GetCreationDate;
---------------------------------------------------------------------------
PROCEDURE GetModificationDate(self : T) : Time.T =
  BEGIN
    RETURN self.mdate;
  END GetModificationDate;
---------------------------------------------------------------------------
PROCEDURE GetUser(self : T) : TEXT =
  BEGIN
    RETURN self.user;
  END GetUser;
---------------------------------------------------------------------------
PROCEDURE GetDescription(self : T) : TEXT =
  BEGIN
    RETURN self.desc;
  END GetDescription;
---------------------------------------------------------------------------
PROCEDURE SetName(self : T; name : TEXT) =
  BEGIN
    self.name := name;
  END SetName;
---------------------------------------------------------------------------
PROCEDURE SetCreationDate(self : T; date : Time.T) =
  BEGIN
    self.cdate := date;
  END SetCreationDate;
---------------------------------------------------------------------------
PROCEDURE SetModificationDate(self : T; date : Time.T) =
  BEGIN
    self.mdate := date;
  END SetModificationDate;
---------------------------------------------------------------------------
PROCEDURE SetUser(self : T; user : TEXT) =
  BEGIN
    self.user := user;
  END SetUser;
---------------------------------------------------------------------------
PROCEDURE SetDescription(self : T; desc : TEXT) =
  BEGIN
    self.desc := desc;
  END SetDescription;
---------------------------------------------------------------------------
PROCEDURE LogText(self : T) : TEXT =
  VAR
    res  : TEXT;
    pkg  : TEXT;
    ver  : TEXT;
    snap : TextTextTbl.T;
  BEGIN
    res := "snapshot/release description " & self.name & NL &
           "created by " & self.user;
    IF self.cdate = 0.0d0 THEN
      res := res & NL & "no creation date available" & NL;
    ELSE
      res := res & " at " & RCS_Date.FromTime(self.cdate) & NL;
    END;
    IF self.mdate = 0.0d0 THEN
      res := res & "no modification date available" & NL;
    ELSE
      res := res & "last modified at " & RCS_Date.FromTime(self.cdate) & NL;
    END;
    res := res & NL & "description:" & NL &
           self.desc & NL &
           "package versions:" & NL;
    IF NOT self.releaseTbl.get(self.name, snap) THEN
      IF NOT self.snapshotTbl.get(self.name, snap) THEN
        RETURN res & " !!! not found !!!" & NL;
      END;
    END;
    WITH iter = snap.iterate() DO
      WHILE iter.next(pkg, ver) DO
        res := res & "  " & pkg & "\t" & ver & NL;
      END;
    END;
    RETURN res;
  END LogText;
---------------------------------------------------------------------------
PROCEDURE AddRoot(self : T; p : Pathname.T) : Pathname.T =
  VAR pn := PathRepr.Native(p);
  BEGIN
    IF Pathname.Absolute(pn) THEN
      RETURN pn;
    ELSE
      RETURN Pathname.Join(self.collectionRoot, pn, NIL);
    END;
  END AddRoot;
---------------------------------------------------------------------------
PROCEDURE Locations(self : T) : TextSeq.T =
  VAR
    loc :  TEXT;
  BEGIN
    IF self.locs = NIL THEN
      self.locs := NEW(TextSeq.T).init(self.packageList.size());
      FOR i := 0 TO self.packageList.size() - 1 DO
        WITH pkg = self.packageList.get(i) DO
          EVAL self.locationTbl.get(pkg, loc);
          self.locs.addhi(Pathname.Join(loc, pkg, NIL));
        END;
      END;
    END;
    RETURN self.locs;
  END Locations;
---------------------------------------------------------------------------
PROCEDURE ParseProjectDescription(self : T; rd : Rd.T) RAISES {Error} =
  VAR
    token, name, location : TEXT;
    tokenValid := FALSE;
    colNr := 1;
    alreadyKnown : BOOLEAN;

    (*-----------------------------------------------------------------------*)
    PROCEDURE NextToken() : TEXT RAISES {Error} =
      BEGIN
        TRY
          token := GetTokenOrString(rd);
        EXCEPT
          Rd.EndOfFile => token := "";
        | Rd.Failure => RAISE Error("error reading project description file");
        | Thread.Alerted => RAISE Error("interrupted while reading "
          & "project description file");
        END;
        RETURN token;
      END NextToken;

    (*-----------------------------------------------------------------------*)
    PROCEDURE CheckName(name, where : TEXT) RAISES {Error} =
      BEGIN
        IF Text.Length(name) = 0 THEN
          RAISE Error("empty argument name after " & where);
        END;
      END CheckName;

    (*-----------------------------------------------------------------------*)
    PROCEDURE CheckName2(name, what : TEXT) RAISES {Error} =
      BEGIN
        IF Text.Length(name) = 0 THEN
          RAISE Error("empty " & what);
        END;
      END CheckName2;

    (*-----------------------------------------------------------------------*)
    PROCEDURE ParsePkgVersionBlock(what : TEXT) : TextTextTbl.T
      RAISES {Error} =
      VAR
        tbl := NEW(TextTextTbl.Default).init();
        name, version, res : TEXT;
      BEGIN
        TRY
	  WHILE NOT Rd.EOF(rd) DO
	    name := NextToken();
	    IF Text.Length(token) > 0 AND Text.GetChar(token, 0) = '#' THEN
	      (* comment till end of line *)
	      EVAL Rd.GetLine(rd);
	    ELSIF Text.Equal(token, "end") THEN
	      RETURN tbl; (* the one correct exit *)
	    ELSE
	      CheckName2(name, "package name in " & what & " block");
	      version := NextToken();
	      CheckName2(version, "version in " & what & " block");
	      IF tbl.get(name, res) THEN
		RAISE Error("multiple entry for package " & name &
		      " in " & what);
	      ELSE
		EVAL tbl.put(name, version);
	      END;
	    END;
	  END;
        EXCEPT
          Rd.Failure => RAISE Error("error reading project description file");
        | Rd.EndOfFile => RAISE Error("premature eof in project description"
          & " file");
        | Thread.Alerted => RAISE Error("interrupted while reading "
          & "project description file");
        END;
        RAISE Error("syntax error: missing `end' after " & what & " block");
      END ParsePkgVersionBlock;

    PROCEDURE ReadDesc() RAISES {Rd.Failure, Thread.Alerted, Rd.EndOfFile} =
      VAR
        lines := NEW(TextSeq.T).init();
        cont := TRUE;
        line :  TEXT;
      BEGIN
        EVAL Rd.GetLine(rd);
        WHILE NOT Rd.EOF(rd) AND cont DO
          line := Rd.GetLine(rd);
          IF Text.Equal(line, "end") THEN
            cont := FALSE;
          ELSE
            lines.addhi(line);
          END;
        END;
        self.desc := TextUtils.TextSeqToText(lines, sep := NL);
      END ReadDesc;

  BEGIN (* ParseProjectDescription *)
    TRY
      WHILE NOT Rd.EOF(rd) DO
        IF tokenValid THEN
          tokenValid := FALSE;
        ELSE
          EVAL NextToken();
        END;
        IF Text.Length(token) > 0 THEN
	  IF Text.GetChar(token, 0) = '#' THEN
	    (* comment till end of line *)
	    EVAL Rd.GetLine(rd);
	  ELSIF Text.Equal(token, "collection") THEN
	    name := PathRepr.Native(NextToken());
	    CheckName(name, "collection");
            EVAL NextToken();
            IF Text.Equal(token, "at") OR
               Text.Equal(token, "=") THEN
              EVAL NextToken();
              location := PathRepr.Native(token);
            ELSE
              location := PathRepr.Native(name);
              name := "collection_" & Fmt.Int(colNr);
              INC(colNr);
              tokenValid := TRUE;
            END;
            alreadyKnown := MemberOfTextSeq(self.collectionList, name);
            IF NOT alreadyKnown THEN
              self.collectionList.addhi(name);
              EVAL self.collectionTbl.put(name, location);
            END;
	  ELSIF Text.Equal(token, "package") THEN
	    name := NextToken();
	    CheckName(name, "package");
            alreadyKnown := MemberOfTextSeq(self.packageList, name);
            IF NOT alreadyKnown THEN
              self.packageList.addhi(name);
            END;
            EVAL NextToken();
            IF Text.Equal(token, "in") OR
               Text.Equal(token, "from") THEN
              EVAL NextToken();
              IF NOT alreadyKnown THEN
		EVAL self.packageTbl.put(name, token);
		IF self.collectionTbl.get(token, location) THEN
		  EVAL self.locationTbl.put(name, self.addRoot(location));
		ELSE
		  RAISE Error("collection " & token & " not found");
		END;
              END;
            ELSE
              tokenValid := TRUE;
            END;
	  ELSIF Text.Equal(token, "collectionroot") OR
                Text.Equal(token, "collectionpath") OR
                Text.Equal(token, "root") OR
                Text.Equal(token, "prefix") THEN
            name := token;
	    location := PathRepr.Native(NextToken());
	    CheckName(location, name);
            self.collectionRootOrig := location;
            IF NOT ProcessEnv.Defined(self.env, "HOME") THEN
              ProcessEnv.Set(self.env, "HOME", PathRepr.Native("/"));
            END;
            IF NOT ProcessEnv.Defined(self.env, "USER") THEN
              ProcessEnv.Set(self.env, "USER", "nobody");
            END;
            IF ProcessEnv.Defined(self.env, "PRJ_ROOT") THEN
                ProcessEnv.Set(
                    self.env, "PRJ_ROOT",
                    PathRepr.Native(
                        ProcessEnv.Value(self.env, "PRJ_ROOT")));
            ELSE
              IF ProcessEnv.Defined(self.env, "collectionroot") THEN
                ProcessEnv.Set(
                    self.env, "PRJ_ROOT",
                    PathRepr.Native(
                        ProcessEnv.Value(self.env, "collectionroot")));
              ELSIF ProcessEnv.Defined(self.env, "COLLECTIONROOT") THEN
                ProcessEnv.Set(
                    self.env, "PRJ_ROOT",
                    PathRepr.Native(
                        ProcessEnv.Value(self.env, "COLLECTIONROOT")));
              ELSE
                ProcessEnv.Set(self.env, "PRJ_ROOT", "undefined_PRJ_ROOT");
              END;
            END;
            WITH prjRoot = ProcessEnv.Value(self.env, "PRJ_ROOT") DO
              IF Text.Equal(prjRoot, Pathname.Current) OR
                 Text.Equal(prjRoot,
                            FSUtils.CanonicalPathname(Pathname.Current)) THEN
                MsgX.Warning(self.msgif,
                             "project root dirctory (PRJ_ROOT) is set to " &
                             Pathname.Current);
              ELSIF NOT FSUtils.IsDir(prjRoot) THEN
                MsgX.Fatal(self.msgif,
                           "project root dirctory (PRJ_ROOT) " & prjRoot &
                           " does not xist");
              END;
            END;
            self.collectionRoot := SubstEnvVars(location, self.env);
            self.collectionRoot := SubstituteVariables(self.collectionRoot,
                                                       self.env);
            self.collectionRoot :=
                FSUtils.CanonicalPathname(self.collectionRoot);
	  ELSIF Text.Equal(token, "snapshot") THEN
	    name := NextToken();
	    CheckName(name, "snapshot");
            IF Text.Empty(self.name) THEN self.name := name END;
	    EVAL self.snapshotTbl.put(name, ParsePkgVersionBlock("snapshot"));
	  ELSIF Text.Equal(token, "release") THEN
	    name := NextToken();
	    CheckName(name, "release");
            IF Text.Empty(self.name) THEN self.name := name END;
	    EVAL self.releaseTbl.put(name, ParsePkgVersionBlock("release"));
	  ELSIF Text.Equal(token, "name") THEN
	    self.name := NextToken();
	  ELSIF Text.Equal(token, "user") OR Text.Equal(token, "creator") THEN
	    self.user := NextToken();
	  ELSIF Text.Equal(token, "mtime") THEN
	    token := NextToken();
            self.mdate := RCS_Date.ToTimeApprox(token);
	  ELSIF Text.Equal(token, "ctime") THEN
	    token := NextToken();
            self.cdate := RCS_Date.ToTimeApprox(token);
          ELSIF Text.Equal(token, "description") THEN
            ReadDesc();
	  ELSE
	    RAISE Error("syntax error at token " & token);
	  END;
        END;
      END;
    EXCEPT
      Rd.Failure => RAISE Error("error reading project description file");
    | Rd.EndOfFile => RAISE Error("premature eof in project description"
      & " file");
    | Thread.Alerted => RAISE Error("interrupted while reading "
      & "project description file");
    | TextUtils.Error(e) =>
      RAISE Error("undefined variable in collectionroot " & e);
    | FSUtils.E(e) => RAISE Error(e);
    END;
  END ParseProjectDescription;
---------------------------------------------------------------------------
PROCEDURE Init(self : T; fn : TEXT; cfg : PkgBase.T; check := FALSE;
               defaultCollectionRoot : TEXT := NIL; cpfn : TEXT;
               useCache := TRUE; env : ProcessEnv.T := NIL;
               msgif : MsgIF.T := NIL;
               pkgvcAcc : PoolSet.PkgVCAccessor := NIL;
               verboseCacheMsgs := TRUE;
	       preferredPkgKind : TEXT := NIL;
               depsMandatory := TRUE;
               cacheEarly := FALSE) : T
  RAISES {Error} =
  VAR
    rd  : FileRd.T;
    res : TEXT;
  BEGIN
    self.name := "";
    self.desc := "";
    self.user := "unknown";
    self.cdate := 0.0d0;
    self.mdate := 0.0d0;
    self.msgif := msgif;
    self.pkgvcAcc := pkgvcAcc;
    self.depsMandatory := depsMandatory;
    self.locs := NIL;
    self.locsText := NIL;
    IF env = NIL THEN
      self.env := ProcessEnv.Current();
    ELSE
      self.env := env;
    END;
    self.depGraph := NIL;
    self.useCache := useCache;
    TRY
      IF defaultCollectionRoot = NIL THEN
        WITH cur = FSUtils.CanonicalPathname(Pathname.Current) DO
          self.collectionRoot := cur;
          self.collectionRootOrig := cur;
        END;
      ELSE
        WITH defroot = FSUtils.CanonicalPathname(defaultCollectionRoot) DO
          self.collectionRoot := defroot;
          self.collectionRootOrig := defroot;
        END;
      END;
    EXCEPT
      FSUtils.E(e) => RAISE Error(e);
    END;
    self.collectionList := NEW(TextSeq.T).init(5);
    self.collectionTbl := NEW(TextTextTbl.Default).init(5);
    self.packageList := NEW(TextSeq.T).init(40);
    self.locationTbl := NEW(TextTextTbl.Default).init(40);
    self.packageTbl := NEW(TextTextTbl.Default).init(40);
    self.snapshotTbl := NEW(TextTextTextTbl.Default).init(40);
    self.releaseTbl := NEW(TextTextTextTbl.Default).init(40);
    TRY
      self.poolset := NEW(PoolSet.T).init(cfg, cpfn, useCache,
                                          msgif := self.msgif,
                                          pkgvcAcc := self.pkgvcAcc,
                                          verboseCache := verboseCacheMsgs,
                                          prefkind := preferredPkgKind,
                                          cacheEarly := cacheEarly);
    EXCEPT
      PoolSet.Error(e) => RAISE Error(e);
    END;
    self.ignoredList := NIL;
    self.missingList := NIL;
    self.externalShell := NIL;
    self.varTbl := NEW(TextTextTbl.Default).init(10);
    self.stateLabelTbl := NEW(TextTextTextTbl.Default).init(40);
    IF cpfn = NIL OR NOT useCache OR NOT FSUtils.IsFile(cpfn) THEN
      self.oldState := NIL;
    ELSE
      self.oldState := Checkpoint.New(self.poolset.getFileCache(), self.msgif);
      TRY
        self.oldState.fromFile(cpfn);
        IF debugStateCache AND Msg.dFlag THEN
          self.poolset.dumpStateCache("PrjDesc.Init(): " &
            "current state:");
          MsgX.D(self.msgif, "old state:", level := 2);
          MsgX.D(self.msgif, self.oldState.toText(), level := 2);
          MsgX.D(self.msgif,
                 "(current state and old state should be exactly the same)",
                 level := 2);
        END;
      EXCEPT
        Checkpoint.Error(e) => RAISE Error("error reading checkpoint: " & e);
      END;
    END;
    (* if there is no file to parse, we quit the initialization here *)
    IF fn = NIL THEN RETURN self END;
    TRY
      rd := FileRd.Open(fn);
    EXCEPT
      OSError.E => RAISE Error("cannot open file " & fn);
    END;
    ParseProjectDescription(self, rd);
    TRY
      Rd.Close(rd);
    EXCEPT ELSE END;
    (* fn parsed without errors *)
    FOR i := 0 TO self.collectionList.size() - 1 DO
      VAR loc : TEXT; BEGIN
        WITH col = self.collectionList.get(i) DO
          IF self.collectionTbl.get(col, loc) THEN
            TRY
              self.poolset.appendPool(self.addRoot(loc));
            EXCEPT
              PoolSet.Error(e) => RAISE Error(e);
            END;
          ELSE
            RAISE Error("internal error: collection " & col & " not in table")
          END;
        END;
      END;
    END;
    (* all collections of packages added as pools to be searched *)
    IF check OR self.useCache  THEN
      IF NOT self.poolset.checkAll(self.packageList, res,
                                   self.missingList,
                                   self.locationTbl,
                                   checkHomogeneity := FALSE,
                                   ignoreMissingPackages := TRUE) THEN
        IF check THEN
          RAISE Error(res);
        END;
      END;
    END;
    (* all packages exist and are of the same type *)
    IF self.packageList.size() > 0 THEN
      self.pkgKind := self.poolset.pkgType(self.packageList.get(0));
    ELSE
      self.pkgKind := NIL;
    END;
    (* some state cache tests *)
    (*
    VAR
      sc1 := Checkpoint.New(self.poolset.getFileCache(), self.msgif);
      sc2 :  Checkpoint.T;
    BEGIN
      sc1.update("/usr/tmp");
      MsgX.T(self.msgif, "sc1 1:"); MsgX.T(self.msgif, sc1.toText());
      sc1.update("/usr/tmp");
      MsgX.T(self.msgif, "sc1 w:"); MsgX.T(self.msgif, sc1.toText());
      sc2 := Checkpoint.New(self.poolset.getFileCache(), self.msgif);
      sc2.update("/usr/tmp");
      MsgX.T(self.msgif, "sc2 1:"); MsgX.T(self.msgif, sc2.toText());
      sc2.update("/usr/tmp");
      MsgX.T(self.msgif, "sc2 w:"); MsgX.T(self.msgif, sc2.toText());
    END;
    *)
    (* end state cache tests *)
    IF self.oldState = NIL THEN
      self.oldState := self.poolset.cachedState();
    END;
    EvaluateStateCacheChanges(self, self.oldState);
    (* -- this cannot be done properly until the dependency graph is built *)
    RETURN self;
  END Init;
---------------------------------------------------------------------------
PROCEDURE ReInit(self : T; fn : TEXT; cfg : PkgBase.T; check := FALSE;
                 defaultCollectionRoot : TEXT := NIL; cpfn : TEXT;
                 useCache := TRUE; env : ProcessEnv.T := NIL;
                 msgif : MsgIF.T := NIL;
                 pkgvcAcc : PoolSet.PkgVCAccessor := NIL)
  RAISES {Error} =
  VAR
    rd  : FileRd.T;
    res : TEXT;
  BEGIN
    self.msgif := msgif;
    self.pkgvcAcc := pkgvcAcc;
    IF env = NIL THEN
      self.env := ProcessEnv.Current();
    ELSE
      self.env := env;
    END;
    self.depGraph := NIL;
    self.useCache := useCache;
    self.locs := NIL;
    self.locsText := NIL;
    TRY
      IF defaultCollectionRoot = NIL THEN
        WITH cur = FSUtils.CanonicalPathname(Pathname.Current) DO
          self.collectionRoot := cur;
          self.collectionRootOrig := cur;
        END;
      ELSE
        WITH defroot = FSUtils.CanonicalPathname(defaultCollectionRoot) DO
          self.collectionRoot := defroot;
          self.collectionRootOrig := defroot;
        END;
      END;
    EXCEPT
      FSUtils.E(e) => RAISE Error(e);
    END;
    EVAL self.collectionList.init(5);
    EVAL NARROW(self.collectionTbl, TextTextTbl.Default).init(5);
    EVAL self.packageList.init(40);
    EVAL NARROW(self.locationTbl, TextTextTbl.Default).init(40);
    EVAL NARROW(self.packageTbl, TextTextTbl.Default).init(40);
    EVAL NARROW(self.snapshotTbl, TextTextTextTbl.Default).init(40);
    EVAL NARROW(self.releaseTbl, TextTextTextTbl.Default).init(40);
    TRY
      EVAL self.poolset.init(cfg, cpfn, useCache, msgif := self.msgif,
                             pkgvcAcc := self.pkgvcAcc);
    EXCEPT
      PoolSet.Error(e) => RAISE Error(e);
    END;
    self.ignoredList := NIL;
    self.missingList := NIL;
    self.externalShell := NIL;
    EVAL NARROW(self.varTbl, TextTextTbl.Default).init(10);
    EVAL NARROW(self.stateLabelTbl, TextTextTextTbl.Default).init(40);
    IF cpfn = NIL OR NOT FSUtils.IsFile(cpfn) THEN
      self.oldState := NIL;
    ELSE
      self.oldState := Checkpoint.New(self.poolset.getFileCache(), self.msgif);
      TRY
        self.oldState.fromFile(cpfn);
        IF debugStateCache AND Msg.dFlag THEN
          self.poolset.dumpStateCache("PrjDesc.Init(): " &
            "current state:");
          MsgX.D(self.msgif, "old state:", level := 2);
          MsgX.D(self.msgif, self.oldState.toText(), level := 2);
          MsgX.D(self.msgif,
                 "(current state and old state should be exactly the same)",
                 level := 2);
        END;
      EXCEPT
        Checkpoint.Error(e) => RAISE Error("error reading checkpoint: " & e);
      END;
    END;
    TRY
      rd := FileRd.Open(fn);
    EXCEPT
      OSError.E => RAISE Error("cannot open file " & fn);
    END;
    ParseProjectDescription(self, rd);
    TRY
      Rd.Close(rd);
    EXCEPT ELSE END;
    (* fn parsed without errors *)
    FOR i := 0 TO self.collectionList.size() - 1 DO
      VAR loc : TEXT; BEGIN
        WITH col = self.collectionList.get(i) DO
          IF self.collectionTbl.get(col, loc) THEN
            TRY
              self.poolset.appendPool(self.addRoot(loc));
            EXCEPT
              PoolSet.Error(e) => RAISE Error(e);
            END;
          ELSE
            RAISE Error("internal error: collection " & col & " not in table")
          END;
        END;
      END;
    END;
    (* all collections of packages added as pools to be searched *)
    IF check OR self.useCache  THEN
      IF NOT self.poolset.checkAll(self.packageList, res,
                                   self.missingList,
                                   self.locationTbl,
                                   checkHomogeneity := FALSE,
                                   ignoreMissingPackages := TRUE) THEN
        IF check THEN
          RAISE Error(res);
        END;
      END;
    END;
    (* all packages exist and are of the same type *)
    IF self.packageList.size() > 0 THEN
      self.pkgKind := self.poolset.pkgType(self.packageList.get(0));
    ELSE
      self.pkgKind := NIL;
    END;
    IF self.oldState = NIL THEN
      self.oldState := self.poolset.cachedState();
    END;
    EvaluateStateCacheChanges(self, self.oldState);
    (* -- this cannot be done properly until the dependency graph is built *)
  END ReInit;
---------------------------------------------------------------------------
PROCEDURE EvaluateStateCacheChanges(self : T; oldState : Checkpoint.T)
  RAISES {Error} =
  VAR
    pkgdirs : TextSeq.T;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, "evaluating state cache changes...");
    IF debugStateCache THEN
      self.poolset.dumpStateCache("EvaluateStateCacheChanges(): " &
        "current state:");
      MsgX.T(self.msgif, "old state:");
      TRY MsgX.T(self.msgif, self.oldState.toText()); EXCEPT ELSE END;
    END;
    self.changedPkgDirs := self.poolset.cachedState().diff(oldState);
    (* `changedPkgDirs' contains a list of directories whose fingerprint
       is different from that recorded in the last checkpoint (oldState).
       Since we cannot know from the changed fingerprint if anything
       relevant has changed, we need to reset certain cache atributes.
       For all packages with changed fingerprint the dependencies need
       to be recomputed, they need to be rebuilt and re-checked for
       their modification status.
    *)
    pkgdirs := self.changedPkgDirs;
    FOR i := 0 TO pkgdirs.size() - 1 DO
      WITH dir = pkgdirs.get(i) DO
        TRY
          self.poolset.updateStateCache(dir, "need-mkdep-build-ship", 0);
          self.poolset.updateStateCache(dir, "clear-mod-unmod", 0);
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        END;
      END;
    END;
    IF debugStateCache THEN
      self.poolset.dumpStateCache("EvaluateStateCacheChanges(end): " &
        "current state:");
    END;
    (* self.changedPkgDirs is now defined *)
  END EvaluateStateCacheChanges;
---------------------------------------------------------------------------
PROCEDURE EvaluateStateCacheChangesForDependendPackages(
    self : T;
    pkgs : TextSeq.T := NIL)
  RAISES {Error} =
  VAR
    pkgdirs : TextSeq.T;
    dir     : TEXT;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, "readjusting state cache...");
    IF debugStateCache THEN
      self.poolset.dumpStateCache(
               "EvaluateStateCacheChangesForDependendPackages(): " &
               "current state:");
    END;
    <* ASSERT self.depGraph # NIL *>
    <* ASSERT self.changedPkgDirs # NIL *>
    (* If packages have changed, they need to be rebuilt and reshipped.
       Consequently, all packages depending on all changed packages
       need to be rebuilt and reshipped, too.
    *)
    IF pkgs = NIL THEN
      pkgs := NEW(TextSeq.T).init();
      FOR i := 0 TO self.changedPkgDirs.size() - 1 DO
        WITH pkg = Pathname.Last(self.changedPkgDirs.get(i)) DO
          pkgs.addhi(pkg);
        END;
      END;
    END;
    pkgs := AddDependingPackages(self, pkgs);
    pkgdirs := NEW(TextSeq.T).init(pkgs.size());
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        IF self.locationTbl.get(pkg, dir) THEN
          pkgdirs.addhi(Pathname.Join(dir, pkg, NIL));
        ELSE
          RAISE Error("no location known for package " & pkg);
        END;
      END;
    END;
    (* depGraph # NIL AND pkgdirs = dirs(changed + dependend pkgs) *)
    FOR i := 0 TO pkgdirs.size() - 1 DO
      WITH dir = pkgdirs.get(i) DO
        TRY
          self.poolset.updateStateCache(dir, "need-build-ship", 0);
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        END;
      END;
    END;
    IF debugStateCache THEN
      self.poolset.dumpStateCache(
               "EvaluateStateCacheChangesForDependendPackages(end): " &
               "current state");
    END;
  END EvaluateStateCacheChangesForDependendPackages;
---------------------------------------------------------------------------
PROCEDURE Parse(self : T; rd : Rd.T) RAISES {Error} =
  BEGIN
    self.locs := NIL;
    self.locsText := NIL;
    ParseProjectDescription(self, rd);
    IF self.useCache THEN
      self.changedPkgDirs := self.poolset.cachedState().diff(self.oldState);
    END;
  END Parse;
---------------------------------------------------------------------------
PROCEDURE LoadFile(self : T; fn : TEXT) RAISES {Error} =
  VAR
    rd  : FileRd.T;
  BEGIN
    TRY
      rd := FileRd.Open(fn);
    EXCEPT
      OSError.E => RAISE Error("cannot open file " & fn);
    END;
    Parse(self, rd);
    TRY
      Rd.Close(rd);
    EXCEPT ELSE END;
  END LoadFile;
---------------------------------------------------------------------------
PROCEDURE PkgsOkay(self : T; VAR res : TEXT) : BOOLEAN =
  BEGIN
    RETURN self.poolset.checkAll(self.packageList, res, self.missingList,
                                 self.locationTbl, FALSE);
  END PkgsOkay;
---------------------------------------------------------------------------
PROCEDURE GetPoolSet(self : T) : PoolSet.T =
  BEGIN
    RETURN self.poolset;
  END GetPoolSet;
---------------------------------------------------------------------------
PROCEDURE SetExternalShell(self : T; shell : TEXT) =
  BEGIN
    self.externalShell := shell;
  END SetExternalShell;
---------------------------------------------------------------------------
PROCEDURE SetPreferredPkgKind(self : T; k : PkgBase.Kind) =
  BEGIN
    self.poolset.setPreferredPkgKind(k);
  END SetPreferredPkgKind;
---------------------------------------------------------------------------
PROCEDURE DefineGlobalVar(self : T; name, val : TEXT) =
  BEGIN
    EVAL self.varTbl.put(name, val);
  END DefineGlobalVar;
---------------------------------------------------------------------------
PROCEDURE DeleteGlobalVar(self : T; name : TEXT) =
  VAR value : TEXT;
  BEGIN
    EVAL self.varTbl.delete(name, value);
  END DeleteGlobalVar;
---------------------------------------------------------------------------
PROCEDURE DefineGlobalVars(self : T; vars : TextTextTbl.T) =
  VAR
    name, val : TEXT;
    iter := vars.iterate();
  BEGIN
    WHILE iter.next(name, val) DO
      EVAL self.varTbl.put(name, val);
    END;
  END DefineGlobalVars;
---------------------------------------------------------------------------
PROCEDURE VarTableCopy(self : T) : TextTextTbl.T =
  VAR
    n, v :  TEXT;
    res  := NEW(TextTextTbl.Default).init(self.varTbl.size() + 10);
    iter := self.varTbl.iterate();
  BEGIN
    WHILE iter.next(n, v) DO
      EVAL res.put(n, v);
    END;
    RETURN res;
  END VarTableCopy;
---------------------------------------------------------------------------
PROCEDURE CollectionPath(self : T) : TEXT =
  BEGIN
    RETURN self.collectionRoot;
  END CollectionPath;
---------------------------------------------------------------------------
PROCEDURE Collections(self : T) : TextSeq.T =
  BEGIN
    RETURN self.collectionList;
  END Collections;
---------------------------------------------------------------------------
PROCEDURE CollectionsLocation(self : T) : TextTextTbl.T =
  BEGIN
    RETURN self.collectionTbl;
  END CollectionsLocation;
---------------------------------------------------------------------------
PROCEDURE Packages(self : T) : TextSeq.T =
  BEGIN
    RETURN self.packageList;
  END Packages;
---------------------------------------------------------------------------
PROCEDURE PackagesCollection(self : T) : TextTextTbl.T =
  BEGIN
    RETURN self.packageTbl;
  END PackagesCollection;
---------------------------------------------------------------------------
PROCEDURE Snapshots(self : T) : TextSeq.T =
  VAR
    iter := self.snapshotTbl.iterate();
    res := NEW(TextSeq.T).init(self.snapshotTbl.size());
    name : TEXT;
    tbl  : TextTextTbl.T;
  BEGIN
    WHILE iter.next(name, tbl) DO
      res.addhi(name);
    END;
    RETURN res;
  END Snapshots;
---------------------------------------------------------------------------
PROCEDURE Releases(self : T) : TextSeq.T =
  VAR
    iter := self.releaseTbl.iterate();
    res := NEW(TextSeq.T).init(self.releaseTbl.size());
    name : TEXT;
    tbl  : TextTextTbl.T;
  BEGIN
    WHILE iter.next(name, tbl) DO
      res.addhi(name);
    END;
    RETURN res;
  END Releases;
---------------------------------------------------------------------------
PROCEDURE MakeHead(self : T) : TextTextTbl.T =
  VAR
    head := NEW(TextTextTbl.Default).init(self.packageList.size());
  BEGIN
    FOR i := 0 TO self.packageList.size() - 1 DO
      WITH pkg = self.packageList.get(i) DO
        EVAL head.put(pkg, "head");
      END;
    END;
    RETURN head;
  END MakeHead;
---------------------------------------------------------------------------
PROCEDURE Snapshot(self : T; name : TEXT) : TextTextTbl.T =
  VAR
    tbl  : TextTextTbl.T;
  BEGIN
    IF Text.Equal(name, "head") THEN
      RETURN MakeHead(self);
    ELSIF self.snapshotTbl.get(name, tbl) THEN
      RETURN tbl;
    ELSE
      RETURN NIL;
    END;
  END Snapshot;
---------------------------------------------------------------------------
PROCEDURE Release(self : T; name : TEXT) : TextTextTbl.T =
  VAR
    tbl  : TextTextTbl.T;
  BEGIN
    IF self.releaseTbl.get(name, tbl) THEN
      RETURN tbl;
    ELSE
      RETURN NIL;
    END;
  END Release;
---------------------------------------------------------------------------
PROCEDURE CurrentTagList(self : T) : TextTextTbl.T
  RAISES {Error} =
  VAR
    pkg, fn :  TEXT;
    tagText :  TEXT;
    tag     := NEW(Tag.T);
    snap    :  TextTextTbl.T;
  BEGIN
    snap := NEW(TextTextTbl.Default).init(self.packageList.size());
    TRY
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- looking for current version of package " &
            pkg & " ---");
        END;
        IF self.poolset.execAction(pkg, "currenttag",
                                   tagText,
                                   self.externalShell,
                                   self.varTbl) = 0 THEN
          fn := "PkgCT";
        ELSE
          RAISE Error("cannot get current tag for package " & pkg);
        END;
        IF tagText = NIL THEN
          tagText := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
          self.poolset.setVal(pkg, "current-tag", tagText);
        END;
        EVAL tag.initFromText(tagText);
        IF NOT tag.okay() THEN
          RAISE Error("tag `" & tagText & "' for package " & pkg &
                " seems to be incorrect");
        END;
        EVAL snap.put(pkg, tag.denotation());
      END;
    EXCEPT
      Error(e) => RAISE Error(e);
      | PoolSet.Error(e) => RAISE Error(e);
    END;
    RETURN snap;
  END CurrentTagList;
---------------------------------------------------------------------------
PROCEDURE DefineSnapshot(self : T; name : TEXT;
                         snap : TextTextTbl.T) RAISES {Error} =
  VAR
    dummy   :  TextTextTbl.T;
  BEGIN
    IF self.snapshotTbl.get(name, dummy) THEN
      RAISE Error("snapshot " & name & " already exists");
    END;
    EVAL self.snapshotTbl.put(name, snap); (* cannot exist already *)
  END DefineSnapshot;
---------------------------------------------------------------------------
PROCEDURE DefineRelease(self : T; name : TEXT;
                        snap : TextTextTbl.T) RAISES {Error} =
  VAR
    dummy   :  TextTextTbl.T;
  BEGIN
    IF self.releaseTbl.get(name, dummy) THEN
      RAISE Error("release " & name & " already exists");
    END;
    EVAL self.releaseTbl.put(name, snap); (* cannot exist already *)
  END DefineRelease;
---------------------------------------------------------------------------
PROCEDURE GetTags(self : T; packageList : TextSeq.T) : TextTextTbl.T
  RAISES {Error} =
  VAR
    pkg, fn :  TEXT;
    tagText :  TEXT;
    tag     := NEW(Tag.T);
    snap    :  TextTextTbl.T;
  BEGIN
    snap := NEW(TextTextTbl.Default).init(packageList.size());
    TRY
      FOR i := 0 TO packageList.size() - 1 DO
        pkg := packageList.get(i);
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- package " & pkg & " ---");
        END;
        IF self.poolset.execAction(pkg, "isrelease", tagText,
                                   self.externalShell,
                                   self.varTbl) = 0 THEN
          EVAL self.poolset.execAction(pkg, "currentreleasetag", tagText,
                                       self.externalShell, self.varTbl);
          fn := "PkgCRT";
        ELSIF self.poolset.execAction(pkg, "currentdeveltag", tagText,
                                      self.externalShell,
                                      self.varTbl) = 0 THEN
          fn := "PkgCDT";
        ELSE
          RAISE Error("cannot get tag for package " & pkg);
        END;
        IF tagText = NIL THEN
          tagText := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
          IF Text.Equal(fn, "PkgCRT") THEN
            self.poolset.setVal(pkg, "current-release-tag", tagText);
          ELSE
            self.poolset.setVal(pkg, "current-devel-tag", tagText);
          END;
        END;
        EVAL tag.initFromText(tagText);
        IF NOT tag.okay() THEN
          RAISE Error("tag `" & tagText & "' for package " & pkg &
                " seems to be incorrect");
        END;
        EVAL snap.put(pkg, tag.denotation());
      END;
    EXCEPT
      Error(e) => RAISE Error(e);
    | PoolSet.Error(e) => RAISE Error(e);
    END;
    RETURN snap;
  END GetTags;
---------------------------------------------------------------------------
PROCEDURE NewSnapshot(self : T; name : TEXT) RAISES {Error} =
  VAR
    pkg, fn :  TEXT;
    tagText :  TEXT;
    dummy   :  TEXT;
    tag     := NEW(Tag.T);
    snap    :  TextTextTbl.T;
  BEGIN
    IF self.snapshotTbl.get(name, snap) THEN
      RAISE Error("snapshot " & name & " already exists");
    END;
    snap := NEW(TextTextTbl.Default).init(self.packageList.size());
    TRY
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- package " & pkg & " ---");
        END;
        IF self.poolset.execAction(pkg, "modified", dummy,
                                   self.externalShell,
                                   self.varTbl) = 0 THEN
          RAISE Error("package " & pkg & " is locally modified. " &
                "Please commit your changes first.");
        ELSIF self.poolset.execAction(pkg, "isrelease", tagText,
                                      self.externalShell,
                                      self.varTbl) = 0 THEN
          EVAL self.poolset.execAction(pkg, "currentreleasetag", tagText,
                                       self.externalShell, self.varTbl);
          fn := "PkgCRT";
        ELSIF self.poolset.execAction(pkg, "currentdeveltag", tagText,
                                      self.externalShell,
                                      self.varTbl) = 0 THEN
          fn := "PkgCDT";
        ELSE
          RAISE Error("cannot get tag for package " & pkg);
        END;
        IF tagText = NIL THEN
          tagText := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
          IF Text.Equal(fn, "PkgCRT") THEN
            self.poolset.setVal(pkg, "current-release-tag", tagText);
          ELSE
            self.poolset.setVal(pkg, "current-devel-tag", tagText);
          END;
        END;
        EVAL tag.initFromText(tagText);
        IF NOT tag.okay() THEN
          RAISE Error("tag `" & tagText & "' for package " & pkg &
                " seems to be incorrect");
        END;
        EVAL snap.put(pkg, tag.denotation());
      END;
    EXCEPT
      Error(e) => RAISE Error(e);
    | PoolSet.Error(e) => RAISE Error(e);
    END;
    EVAL self.snapshotTbl.put(name, snap); (* cannot exist already *)
  END NewSnapshot;
---------------------------------------------------------------------------
PROCEDURE NewRelease(self : T; name : TEXT) RAISES {Error} =
  VAR
    pkg, fn :  TEXT;
    tagText :  TEXT;
    dummy   :  TEXT;
    tag     := NEW(Tag.T);
    rel     :  TextTextTbl.T;
  BEGIN
    IF self.releaseTbl.get(name, rel) THEN
      RAISE Error("release " & name & " already exists");
    END;
    rel := NEW(TextTextTbl.Default).init(self.packageList.size());
    TRY
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- package " & pkg & " ---");
        END;
        IF self.poolset.execAction(pkg, "modified", dummy,
                                   self.externalShell,
                                   self.varTbl) = 0 THEN
          RAISE Error("package " & pkg & " is locally modified. " &
                "Please commit your changes first.");
        ELSIF self.poolset.execAction(pkg, "isrelease", tagText,
                                      self.externalShell,
                                      self.varTbl) = 0 THEN
          EVAL self.poolset.execAction(pkg, "currentreleasetag", tagText,
                                       self.externalShell,
                                       self.varTbl);
          fn := "PkgCRT";
        ELSE
          RAISE Error("cannot get release tag for package " & pkg);
        END;
        IF tagText = NIL THEN
          tagText := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
          IF Text.Equal(fn, "PkgCRT") THEN
            self.poolset.setVal(pkg, "current-release-tag", tagText);
          ELSE
            self.poolset.setVal(pkg, "current-devel-tag", tagText);
          END;
        END;
        EVAL tag.initFromText(tagText);
        IF NOT tag.okay() THEN
          RAISE Error("tag `" & tagText & "' for package " & pkg &
                " seems to be incorrect");
        END;
        EVAL rel.put(pkg, tag.denotation());
      END;
    EXCEPT
      Error(e) => RAISE Error(e);
    | PoolSet.Error(e) => RAISE Error(e);
    END;
    EVAL self.releaseTbl.put(name, rel); (* cannot exist already *)
  END NewRelease;
---------------------------------------------------------------------------
PROCEDURE Checkout(self : T; name : TEXT; tab : TextTextTbl.T;
                   heedModifiedPackages := TRUE)
  RAISES {Error} =

  PROCEDURE GetPackage(pkg, tag : TEXT) RAISES {Error} =
    BEGIN
      IF Msg.tFlag THEN
        MsgX.T(self.msgif, "--- get package " & pkg & " with tag " &
          tag & " ---");
      END;
      IF NOT self.locationTbl.get(pkg, loc) THEN
        RAISE Error("package " & pkg & " not found");
      END;
      TRY
        exists := self.poolset.exists(pkg, loc);
      EXCEPT
        PoolSet.Error(e) => RAISE Error(e);
      END;
      IF exists THEN
        (* package is already checked out, just update it *)
        TRY
          IF self.poolset.execAction(pkg, "modified", dummy,
                                     self.externalShell,
                                     self.varTbl) = 0 THEN
            IF heedModifiedPackages THEN
              WITH msg = "package " & pkg & " is locally modified. " &
                  "Continue" DO
                IF NOT PkgVC.confirmation.okay(msg) THEN
                  RAISE Error("package " & pkg & " is locally modified. " &
                        "Please commit your changes first.");
                END;
              END;
            ELSE
              MsgX.Warning(self.msgif, "package " & pkg &
                " is locally modified.");
            END
          END
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        | Error(e) => RAISE Error(e);
        END;
        par := self.varTableCopy();
        EVAL par.put("TAG", tag);
        TRY
          IF self.poolset.execAction(pkg, "update", dummy,
                                     self.externalShell, par) # 0 THEN
            IF dummy = NIL THEN
              dummy := "(no further explanation)";
            END;
            RAISE Error("update of package " & pkg & " failed:" & NL & dummy);
          END;
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        | Error(e) => RAISE Error(e);
        END;
      ELSE
        (* package must be checked out *)
        IF NOT self.packageTbl.get(pkg, col) THEN
          RAISE Error("collection for package " & pkg & " not found");
        END;
        IF NOT self.collectionTbl.get(col, loc) THEN
          RAISE Error("location of collection " & col & " not found");
        END;
        par := self.varTableCopy();
        EVAL par.put("TAG", tag);
        EVAL par.put("LOCATION", loc);
        EVAL par.put("PKG", pkg);
        TRY
          IF self.poolset.checkout(pkg, "checkout", self.externalShell,
                                   self.collectionRoot, par) # 0 THEN
            RAISE Error("checkout of package " & pkg & " failed");
          END;
          (* remove the package from the list of missing packages,
             assuming it is at most listed once... *)
          IF self.missingList # NIL AND self.missingList.size() > 0 THEN
            IF Text.Equal(self.missingList.getlo(), pkg) THEN
              EVAL self.missingList.remlo();
            ELSIF Text.Equal(self.missingList.gethi(), pkg) THEN
              EVAL self.missingList.remhi();
            ELSE
              FOR i := 1 TO self.missingList.size() - 2 DO
                WITH act = self.missingList.get(i) DO
                  IF Text.Equal(act, pkg) THEN
                    self.missingList.put(i, self.missingList.remhi());
                    EXIT;
                  END;
                END;
              END;
            END;
          END;
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        | Error(e) => RAISE Error(e);
        END;
      END;
    END GetPackage;

  VAR
    iter : TextTextTbl.Iterator;
    pkg, tag, col, loc, dummy : TEXT;
    exists : BOOLEAN;
    par : TextTextTbl.T;
  BEGIN
    IF tab = NIL THEN
      (* If we haven't got a configuration (mapping from packages to
         versions), we try to get the packages using the given name. *)
      IF self = NIL OR self.packageList = NIL THEN
        RAISE Error("undefined project");
      END;
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        GetPackage(pkg, name);
      END;
    ELSE
      iter := tab.iterate();
      WHILE iter.next(pkg, tag) DO
        GetPackage(pkg, tag);
      END;
    END;
  END Checkout;
---------------------------------------------------------------------------
PROCEDURE CheckoutSnapshot(self : T; name : TEXT) RAISES {Error} =
  VAR
    snap := self.snapshot(name);
  BEGIN
    Checkout(self, name, snap);
  END CheckoutSnapshot;
---------------------------------------------------------------------------
PROCEDURE CheckoutRelease(self : T; name : TEXT) RAISES {Error} =
  VAR
    release := self.release(name);
  BEGIN
    Checkout(self, name, release);
  END CheckoutRelease;
---------------------------------------------------------------------------
PROCEDURE CheckoutHead(self : T) RAISES {Error} =
  BEGIN
    Checkout(self, "", MakeHead(self), heedModifiedPackages := FALSE);
  END CheckoutHead;
---------------------------------------------------------------------------
PROCEDURE CheckoutTrunkOrBranchHead(self : T; pkgs : TextSeq.T)
  RAISES {Error} =
  VAR
    head    := NEW(TextTextTbl.Default).init(self.packageList.size());
    tagText :  TEXT;
    fn      :  TEXT;
    tag     := NEW(Tag.T);
    isrel   :  BOOLEAN;
  BEGIN
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- inspecting package " & pkg & " ---");

        END;
        TRY
	  IF self.poolset.execAction(pkg, "isrelease", tagText,
				     self.externalShell,
                                     self.varTbl) = 0 THEN
	    EVAL self.poolset.execAction(pkg, "currentreleasetag", tagText,
					 self.externalShell,
                                         self.varTbl);
	    fn := "PkgCRT";
	    isrel := TRUE;
	  ELSIF self.poolset.execAction(pkg, "currentdeveltag", tagText,
					self.externalShell,
                                        self.varTbl) = 0 THEN
	    fn := "PkgCDT";
	    isrel := FALSE;
	  ELSE
	    RAISE Error("cannot get tag for package " & pkg);
	  END;
          IF tagText = NIL THEN
            tagText := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
            IF Text.Equal(fn, "PkgCRT") THEN
              self.poolset.setVal(pkg, "current-release-tag", tagText);
            ELSE
              self.poolset.setVal(pkg, "current-devel-tag", tagText);
            END;
          END;
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        END;
        EVAL tag.initFromText(tagText);
        IF NOT tag.okay() THEN
          RAISE Error("tag `" & tagText & "' for package " & pkg &
                " seems to be incorrect");
        END;
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "    current tag is " & tagText);
        END;
        IF isrel THEN
          tag := Tag.NewStableBranch(tag);
          tagText := tag.denotation();
        ELSE
          tagText := "head";
        END;
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "    tip of branch tag is " & tagText);
        END;
        EVAL head.put(pkg, tagText);
      END;
    END;
    Checkout(self, "", head, FALSE);
  END CheckoutTrunkOrBranchHead;
---------------------------------------------------------------------------
PROCEDURE CheckoutPackages(self : T; pkgs : TextSeq.T; tag := "head")
  RAISES {Error} =
  VAR
    head := NEW(TextTextTbl.Default).init(self.packageList.size());
    snap : TextTextTbl.T := NIL;
  BEGIN
    IF NOT Text.Equal(tag, "head") THEN
      snap := self.release(tag);
      IF snap = NIL THEN
        snap := self.snapshot(tag);
      END;
    END;
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        IF snap = NIL THEN
          EVAL head.put(pkg, tag);
        ELSE
          VAR
            t : TEXT;
          BEGIN
            EVAL snap.get(pkg, t);
            IF t = NIL THEN
              t := tag;
            END;
            EVAL head.put(pkg, t);
          END;
        END;
      END;
    END;
    Checkout(self, "", head, FALSE);
  END CheckoutPackages;
---------------------------------------------------------------------------
PROCEDURE BuildDepGraph(
    self : T;
    confirmationCl : Confirmation.Closure := NIL) RAISES {Error} =
  VAR
    pkg  :  TEXT;
    loc  :  TEXT;
    res  :  TEXT;
    path :  Pathname.T;
    errs := "";
    okay := TRUE;
    par  :  TextTextTbl.T;
    locs := self.locations();
    dummy: TEXT;
    depFileExists : BOOLEAN;
    thisPkgDepsOkay : BOOLEAN;
  BEGIN
    MsgX.T(self.msgif, "building dependency graph...");
    IF self.missingList # NIL AND self.missingList.size() > 0 THEN
      MsgX.T(self.msgif, "checking out missing packages first...");
      WITH msg = "The following packages are missing in your workspace:" & NL &
           TextUtils.TextSeqToText(self.missingList, NL) & NL &
           "Shall I check them out now" DO
        IF confirmationCl # NIL THEN
          IF confirmationCl.okay(msg) THEN
            self.checkoutPackages(self.missingList);
          ELSE
            RAISE Error("cannot build dependency graph without all packages");
          END;
        END;
      END;
    END;
    self.depGraph := DependencyGraph.New(NIL);
    FOR i := 0 TO self.packageList.size() - 1 DO
      pkg := self.packageList.get(i);
      self.depGraph.addElem(pkg, action := NIL, phony := TRUE);
    END;
    IF self.locationTbl.size() < self.packageList.size() THEN
      IF NOT self.poolset.checkAll(self.packageList, res, self.missingList,
                                   self.locationTbl,
                                   checkHomogeneity := FALSE,
                                   ignoreMissingPackages := FALSE) THEN
        RAISE Error(res);
      END;
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        EVAL self.locationTbl.put(
                      pkg,
                      Pathname.Prefix(self.poolset.pkgPath(pkg)));
      END;
    END;
    TRY
      par := self.varTableCopy();
      IF self.locsText = NIL THEN
        self.locsText := TextUtils.TextSeqToText(locs);
      END;
      EVAL par.put("LOCATIONS", self.locsText);
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- package " & pkg & ": mkdep ---");
        END;
        EVAL par.put("PKG", pkg);
        EVAL self.locationTbl.get(pkg, loc);
        EVAL par.put("LOCATION", loc);
        IF NOT self.poolset.exists(pkg, loc) THEN
          RAISE Error("package " & pkg & " has disappeared from location " &
                loc);
        END;
        thisPkgDepsOkay := TRUE;
        path := Pathname.Join(self.poolset.pkgPath(pkg), "PkgDep", NIL);
        IF self.poolset.execAction(pkg, "mkdep", dummy,
                                   self.externalShell, par) # 0 THEN
          errs := errs & "building dependencies for package " & pkg &
                      " at " & path & " failed" & NL;
          thisPkgDepsOkay := FALSE;
          IF self.depsMandatory THEN
            okay := FALSE;
          END;
        END;
        (* dependencies should now be listed in collection/pkg/PkgDep *)
        depFileExists := FSUtils.Exists(path);
        IF depFileExists THEN
          IF NOT self.depGraph.addFromDependFile(path) THEN
            errs := errs & "adding dependencies for package " & pkg &
                        " at " & path & " failed" & NL;
            thisPkgDepsOkay := FALSE;
            IF self.depsMandatory THEN
              okay := FALSE;
            END;
          END;
        ELSE
          thisPkgDepsOkay := FALSE;
          IF self.depsMandatory THEN
            okay := FALSE;
          END;
        END;
        IF NOT self.depsMandatory AND NOT thisPkgDepsOkay THEN
          MsgX.Warning(self.msgif, "no dependecies for package " & pkg);
        END;
      END;
    EXCEPT
      PoolSet.Error(e) => RAISE Error(e);
    END;
    IF self.depsMandatory AND NOT okay THEN
      RAISE Error(errs);
    END;
    IF self.useCache THEN
      EvaluateStateCacheChangesForDependendPackages(self);
    END;
  END BuildDepGraph;
---------------------------------------------------------------------------
PROCEDURE ReadDepGraph(self : T; fn : TEXT) RAISES {Error} =
  BEGIN
    IF self.depGraph = NIL THEN
      self.depGraph := DependencyGraph.New(NIL);
    END;
    IF NOT self.depGraph.load(fn) THEN
      RAISE Error("reading dependency graph from file " & fn & "failed");
    END;
    IF self.useCache THEN
      EvaluateStateCacheChangesForDependendPackages(self);
    END;
  END ReadDepGraph;
---------------------------------------------------------------------------
PROCEDURE WriteDepGraph(self : T; fn : TEXT) RAISES {Error} =
  BEGIN
    IF NOT self.depGraph.save(fn) THEN
      RAISE Error("writing dependency graph to file " & fn & "failed");
    END;
  END WriteDepGraph;
---------------------------------------------------------------------------
PROCEDURE ReadDepGraphAsText(self : T; fn : TEXT) RAISES {Error} =
  BEGIN
    IF self.depGraph = NIL THEN
      self.depGraph := DependencyGraph.New(NIL);
    END;
    IF NOT self.depGraph.loadAsText(fn) THEN
      RAISE Error("reading dependency graph from file " & fn & "failed");
    END;
    IF self.useCache THEN
      EvaluateStateCacheChangesForDependendPackages(self);
    END;
  END ReadDepGraphAsText;
---------------------------------------------------------------------------
PROCEDURE WriteDepGraphAsText(self : T; fn : TEXT) RAISES {Error} =
  BEGIN
    IF NOT self.depGraph.saveAsText(fn) THEN
      RAISE Error("writing dependency graph to file " & fn & "failed");
    END;
  END WriteDepGraphAsText;
---------------------------------------------------------------------------
PROCEDURE PutPkgVersionBlock(wr : Wr.T; tbl : TextTextTbl.T)
  RAISES {Wr.Failure, Thread.Alerted} =
  VAR
    iter := tbl.iterate();
    name, version : TEXT;
  BEGIN
    WHILE iter.next(name, version) DO
      Wr.PutText(wr, "  " & name & " " & version & NL);
    END;
    Wr.PutText(wr, "end" & NL);
  END PutPkgVersionBlock;
---------------------------------------------------------------------------
PROCEDURE ConvertToText(self : T; snapshotName : TEXT := NIL;
                        releaseName  : TEXT := NIL) : TEXT RAISES {Error} =
  VAR
    wr  := TextWr.New();
    res :  TEXT;
  BEGIN
    TRY
      WriteInternalWr(self, wr, snapshotName, releaseName);
    FINALLY
      TRY
        res := TextWr.ToText(wr);
        Wr.Close(wr);
      EXCEPT ELSE
        RAISE Error("closing text writer failed");
      END;
    END;
    RETURN res;
  END ConvertToText;
---------------------------------------------------------------------------
PROCEDURE SnapshotText(self : T; snapshotName : TEXT) : TEXT RAISES {Error} =
  BEGIN
    RETURN ConvertToText(self, snapshotName, NIL);
  END SnapshotText;
---------------------------------------------------------------------------
PROCEDURE ReleaseText(self : T; snapshotName : TEXT) : TEXT RAISES {Error} =
  BEGIN
    RETURN ConvertToText(self, NIL, snapshotName);
  END ReleaseText;
---------------------------------------------------------------------------
PROCEDURE ToText(self : T) : TEXT RAISES {Error} =
  BEGIN
    RETURN ConvertToText(self, "", "");
  END ToText;
---------------------------------------------------------------------------
PROCEDURE WriteInternal(self : T; fn : TEXT;
                        snapshotName : TEXT := NIL; (* "" <=> all *)
                        releaseName : TEXT := NIL)  (* "" <=> all *)
  RAISES {Error} =
  VAR wr : Wr.T;
  BEGIN
    TRY
      wr := FileWr.Open(fn);
    EXCEPT ELSE
      RAISE Error("opening file " & fn & " failed");
    END;
    TRY
      WriteInternalWr(self, wr, snapshotName, releaseName);
    FINALLY
      TRY
        Wr.Close(wr);
      EXCEPT ELSE
        RAISE Error("closing file " & fn & " failed");
      END;
    END;
  END WriteInternal;
---------------------------------------------------------------------------
PROCEDURE WriteInternalWr(self : T; wr : Wr.T;
                          snapshotName : TEXT := NIL; (* "" <=> all *)
                          releaseName : TEXT := NIL)  (* "" <=> all *)
  RAISES {Error} =
  VAR
    name : TEXT;
    loc  : TEXT;
    tbl  : TextTextTbl.T;
    nameDone := FALSE;
  BEGIN
    TRY
      Wr.PutText(wr, "collectionroot " & self.collectionRootOrig & NL);
      FOR i := 0 TO self.collectionList.size() - 1 DO
        name := self.collectionList.get(i);
        EVAL self.collectionTbl.get(name, loc);
        Wr.PutText(wr, "collection " & name & "	at " & loc & NL);
      END;
      Wr.PutText(wr, NL);
      (* collections written *)

      FOR i := 0 TO self.packageList.size() - 1 DO
        name := self.packageList.get(i);
        EVAL self.packageTbl.get(name, loc);
        Wr.PutText(wr, "package " & name & "	in " & loc & NL);
      END;
      Wr.PutText(wr, NL);
      (* packages written *)

      IF snapshotName # NIL AND Text.Empty(snapshotName) THEN
        VAR
          iter := self.snapshotTbl.iterate();
	BEGIN
	  WHILE iter.next(name, tbl) DO
	    Wr.PutText(wr, "snapshot " & name & NL);
	    PutPkgVersionBlock(wr, tbl);
	  END;
	END;
        Wr.PutText(wr, "name \"" & self.name & "\"" & NL);
        nameDone := TRUE;
      ELSIF snapshotName # NIL THEN
        IF self.snapshotTbl.get(snapshotName, tbl) THEN
          Wr.PutText(wr, "snapshot " & snapshotName & NL);
          PutPkgVersionBlock(wr, tbl);
        ELSE
          RAISE Error("snapshot " & snapshotName & " not found");
        END;
      END;
      Wr.PutText(wr, NL);
      (* snapshots written *)

      IF releaseName # NIL AND Text.Empty(releaseName)THEN
	VAR
	  iter := self.releaseTbl.iterate();
	BEGIN
	  WHILE iter.next(name, tbl) DO
	    Wr.PutText(wr, "release " & name & NL);
	    PutPkgVersionBlock(wr, tbl);
	  END;
	END;
        IF NOT nameDone THEN
          Wr.PutText(wr, "name \"" & self.name & "\"" & NL);
        END;
      ELSIF releaseName # NIL THEN
        IF self.releaseTbl.get(releaseName, tbl) THEN
          Wr.PutText(wr, "release " & releaseName & NL);
          PutPkgVersionBlock(wr, tbl);
        ELSE
          RAISE Error("release " & releaseName & " not found");
        END;
      END;
      Wr.PutText(wr, NL);
      (* releases written *)

      Wr.PutText(wr, "creator \"" & self.user & "\"" & NL);
      IF self.cdate # 0.0d0 THEN
        Wr.PutText(wr, "ctime \"" & RCS_Date.FromTime(self.cdate) & "\"" & NL);
      END;
      IF self.mdate # 0.0d0 THEN
        Wr.PutText(wr, "mtime \"" & RCS_Date.FromTime(self.mdate) & "\"" & NL);
      END;
      Wr.PutText(wr, "description" & NL);
      Wr.PutText(wr, self.desc);
      Wr.PutText(wr, NL & "end" & NL);
    EXCEPT
      Wr.Failure => RAISE Error("writing project description failed");
    | Thread.Alerted => RAISE Error("interrupted writing project description");
    END;
  END WriteInternalWr;
---------------------------------------------------------------------------
PROCEDURE Write(self : T; fn : TEXT) RAISES {Error} =
  BEGIN
    WriteInternal(self, fn, "", "");
  END Write;
---------------------------------------------------------------------------
PROCEDURE WriteSnapshot(self : T; fn : TEXT; name : TEXT) RAISES {Error} =
  BEGIN
    WriteInternal(self, fn, name, NIL);
  END WriteSnapshot;
---------------------------------------------------------------------------
PROCEDURE WriteRelease(self : T; fn : TEXT; name : TEXT) RAISES {Error} =
  BEGIN
    WriteInternal(self, fn, NIL, name);
  END WriteRelease;
---------------------------------------------------------------------------
PROCEDURE PackageUpdateSequence(self : T) : TextSeq.T =
  BEGIN
    IF self.depGraph = NIL THEN
      TRY
        BuildDepGraph(self);
      EXCEPT
        Error(e) =>
        MsgX.Fatal(self.msgif, "cannot build package dependency graph: " & e);
      END;
    END;
    VAR
      nodes := self.depGraph.topologicalSort();
      res   := NEW(TextSeq.T).init(self.packageList.size());
    BEGIN
      FOR i := 0 TO nodes.size() - 1 DO
        WITH act = nodes.get(i) DO
          IF MemberOfTextSeq(self.packageList, act.name()) THEN
            res.addhi(act.name());
          END;
        END;
      END;
      RETURN res;
    END;
  END PackageUpdateSequence;
---------------------------------------------------------------------------
PROCEDURE IgnoredPackages(self : T) : TextSeq.T =
  BEGIN
    IF self.ignoredList # NIL THEN
      RETURN self.ignoredList;
    END;
    VAR
      graphNodes := self.depGraph.nodes();
    BEGIN
      self.ignoredList := NEW(TextSeq.T).init(graphNodes.size());
      FOR i := 0 TO graphNodes.size() - 1 DO
        WITH act = graphNodes.get(i) DO
          IF NOT MemberOfTextSeq(self.packageList, act.name()) THEN
            self.ignoredList.addhi(act.name());
          END;
        END;
      END;
    END;
    RETURN self.ignoredList;
  END IgnoredPackages;
---------------------------------------------------------------------------
PROCEDURE MissingPackages(self : T) : TextSeq.T =
  BEGIN
    RETURN self.missingList;
  END MissingPackages;
---------------------------------------------------------------------------
PROCEDURE ApplyToPackages(self : T;
                          action  : PkgBase.Action;
                          action2 : PkgBase.Action := NIL;
                          action3 : PkgBase.Action := NIL;
                          cpkgs   : TextSeq.T := NIL;
                          ordered := TRUE;
                          breakOnZeroReturn := FALSE;
                          breakOnError := TRUE;
                          breakOnFailure := TRUE;
                          tag1Values : TextTextTbl.T := NIL;
                          tag2Values : TextTextTbl.T := NIL) : INTEGER
  RAISES {Error} =

  PROCEDURE CheckStateCacheIfBuild(pkg, action : TEXT) RAISES {Error} =
    VAR
      loc, dir : TEXT;
      ret : INTEGER;
      res : TEXT;
    BEGIN
      IF self.useCache AND TextUtils.Contains(action, "build") THEN
        (* If we execute a build action, we need to invalidate the
           attributes that indicate `built or shipped' for all
           packages that depend on the newly built package if the
           build action is really executed. *)
        IF NOT self.locationTbl.get(pkg, loc) THEN
          RAISE Error("no location for package " & pkg);
        END;
        dir := Pathname.Join(loc, pkg, NIL);
        TRY
          IF NOT self.poolset.actionProbablyNeeded(pkg, dir, action,
                                                   ret, res) THEN
            RETURN;
          END;
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        END;
        WITH deps = NEW(TextSeq.T).init() DO
          deps.addhi(pkg);
          FOR i := 0 TO deps.size() - 1 DO
            WITH p = deps.get(i) DO
              IF NOT self.locationTbl.get(p, loc) THEN
                RAISE Error("no location for package " & p);
              END;
              dir := Pathname.Join(loc, p, NIL);
              IF NOT Text.Equal(p, pkg) THEN
                TRY
                  self.poolset.updateStateCache(dir, "need-build-ship", 0);
                EXCEPT
                  PoolSet.Error(e) => RAISE Error(e);
                END;
              END;
            END;
          END;
        END;
      END;
    END CheckStateCacheIfBuild;

  PROCEDURE CheckAndDisplayResult(res : INTEGER; rtext : TEXT) =
    BEGIN
      IF rtext # NIL THEN
        IF NOT Text.Empty(rtext) THEN
          MsgX.T(self.msgif, rtext);
        END;
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "exit code was " & Fmt.Int(res));
        END;
      END;
    END CheckAndDisplayResult;

  VAR
    loc  :  TEXT;
    upds :  TextSeq.T;
    pkgs :  TextSeq.T;
    res  :  INTEGER;
    locs := self.locations();
    par  := self.varTableCopy();
    rtext:  TEXT;
  BEGIN
    IF ordered THEN
      IF cpkgs = NIL THEN
        pkgs := self.packageUpdateSequence();
      ELSE
        upds := self.packageUpdateSequence();
        pkgs := NEW(TextSeq.T).init(cpkgs.size());
        FOR i := 0 TO upds.size() - 1 DO
          WITH pkg = upds.get(i) DO
            IF MemberOfTextSeq(cpkgs, pkg) THEN
              pkgs.addhi(pkg);
            END;
          END;
        END;
      END;
    ELSE
      IF cpkgs = NIL THEN
        pkgs := self.packageList;
      ELSE
        pkgs := cpkgs;
      END;
    END;
    IF self.locsText = NIL THEN
      self.locsText := TextUtils.TextSeqToText(locs);
    END;
    EVAL par.put("LOCATIONS", self.locsText );
    res := 0;
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        IF Msg.tFlag THEN
          MsgX.T(self.msgif, "--- package " & pkg & " ---");
        END;
        EVAL par.put("PKG", pkg);
        EVAL self.locationTbl.get(pkg, loc);
        EVAL par.put("LOCATION", loc);
        VAR val : TEXT; BEGIN
          IF tag1Values # NIL AND tag1Values.get(pkg, val) AND val # NIL THEN
            EVAL par.put("TAG1", val);
          END;
          IF tag2Values # NIL AND tag2Values.get(pkg, val) AND val # NIL THEN
            EVAL par.put("TAG2", val);
          END;
        END;
        TRY
          CheckStateCacheIfBuild(pkg, action);
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "--- package " & pkg & ": applying action 1: " &
              action & " ---", level := 2);
          END;
          res := ExecutionFailure;
          rtext := NIL;
          res := self.poolset.execAction(pkg, action, rtext,
                                         self.externalShell, par);
          CheckAndDisplayResult(res, rtext);
          IF action2 # NIL THEN
            IF NOT breakOnError OR (res = 0 AND NOT breakOnZeroReturn) OR
              (res # 0 AND breakOnZeroReturn) THEN
              CheckStateCacheIfBuild(pkg, action2);
              IF Msg.vFlag THEN
                MsgX.V(self.msgif, "--- package " & pkg &
                  ": applying action 2: " & action2 & " ---", level := 2);
              END;
              res := ExecutionFailure;
              rtext := NIL;
              res := self.poolset.execAction(pkg, action2, rtext,
                                             self.externalShell, par);
              CheckAndDisplayResult(res, rtext);
            END;
          END;
          IF action3 # NIL THEN
            IF NOT breakOnError OR (res = 0 AND NOT breakOnZeroReturn) OR
              (res # 0 AND breakOnZeroReturn) THEN
              CheckStateCacheIfBuild(pkg, action3);
              IF Msg.vFlag THEN
                MsgX.V(self.msgif, "--- package " & pkg &
                  ": applying action 3: " & action3 & " ---", level := 2);
              END;
              rtext := NIL;
              res := ExecutionFailure;
              res := self.poolset.execAction(pkg, action3, rtext,
                                             self.externalShell, par);
              CheckAndDisplayResult(res, rtext);
            END;
          END;
          IF breakOnError THEN
            IF breakOnZeroReturn THEN
              IF res = 0 THEN
                EXIT;
              END;
            ELSE
              IF res # 0 THEN
                EXIT;
              END;
            END;
          END;
        EXCEPT
          PoolSet.Error(e) =>
          IF breakOnFailure THEN
            RAISE Error(e);
          ELSE
            (* silently skip exception *)
            MsgX.V(self.msgif, "caught and ignored exception");
          END;
        END;
      END;
    END;
    RETURN res;
  END ApplyToPackages;
---------------------------------------------------------------------------
PROCEDURE ApplyCmdListDirectly(self    : T;
                               cmd     : TEXT;
                               cpkgs   : TextSeq.T := NIL;
                               ordered := TRUE;
                               breakOnZeroReturn := FALSE;
                               breakOnError := TRUE;
                               breakOnFailure := TRUE) : INTEGER
  RAISES {Error} =
  VAR
    upds : TextSeq.T;
    pkgs : TextSeq.T;
    res  : INTEGER;
  BEGIN
    IF ordered THEN
      IF cpkgs = NIL THEN
        pkgs := self.packageUpdateSequence();
      ELSE
        upds := self.packageUpdateSequence();
        pkgs := NEW(TextSeq.T).init(cpkgs.size());
        FOR i := 0 TO upds.size() - 1 DO
          WITH pkg = upds.get(i) DO
            IF MemberOfTextSeq(cpkgs, pkg) THEN
              pkgs.addhi(pkg);
            END;
          END;
        END;
      END;
    ELSE
      IF cpkgs = NIL THEN
        pkgs := self.packageList;
      ELSE
        pkgs := cpkgs;
      END;
    END;
    res := 0;
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        IF Msg.tFlag THEN
          MsgX.T(self.msgif, "--- package " & pkg & " ---");
        END;
        TRY
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "--- package " & pkg & ": applying command: " &
              cmd & " ---", level := 2);
          END;
          res := ExecutionFailure;
          res := self.poolset.execCmdList(pkg, cmd, self.externalShell);
          IF breakOnError THEN
            IF breakOnZeroReturn THEN
              IF res = 0 THEN
                EXIT;
              END;
            ELSE
              IF res # 0 THEN
                EXIT;
              END;
            END;
          END;
        EXCEPT
          PoolSet.Error(e) =>
          IF breakOnFailure THEN
            RAISE Error(e);
          ELSE
            (* silently skip exception *)
            MsgX.V(self.msgif, "caught and ignored exception");
          END;
        END;
      END;
    END;
    RETURN res;
  END ApplyCmdListDirectly;
---------------------------------------------------------------------------
PROCEDURE SelectPackages(self : T;
                         pred : PkgBase.Action;
                         ordered := TRUE;
                         selectOnZeroReturn := TRUE;
                         breakOnFailure := TRUE;
                         tag1Values : TextTextTbl.T := NIL;
                         tag2Values : TextTextTbl.T := NIL) : TextSeq.T
  RAISES {Error} =
  VAR
    loc  :  TEXT;
    pkgs :  TextSeq.T;
    res  := NEW(TextSeq.T).init();
    ret  := 0;
    locs := self.locations();
    par  := self.varTableCopy();
    done :  BOOLEAN;
    dummy:  TEXT;
  BEGIN
    IF ordered THEN
      pkgs := self.packageUpdateSequence();
    ELSE
      pkgs := self.packageList;
    END;
    IF self.locsText = NIL THEN
      self.locsText := TextUtils.TextSeqToText(locs);
    END;
    EVAL par.put("LOCATIONS", self.locsText);
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        done := FALSE;
        IF NOT done THEN
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "--- package " & pkg & ": selecting by " &
              pred & " ---");
          END;
          EVAL par.put("PKG", pkg);
          EVAL self.locationTbl.get(pkg, loc);
          EVAL par.put("LOCATION", loc);
          VAR val : TEXT; BEGIN
            IF tag1Values # NIL AND tag1Values.get(pkg, val) AND val # NIL THEN
              EVAL par.put("TAG1", val);
            END;
            IF tag2Values # NIL AND tag2Values.get(pkg, val) AND val # NIL THEN
              EVAL par.put("TAG2", val);
            END;
          END;
          TRY
            ret := ExecutionFailure;
            ret := self.poolset.execAction(pkg, pred, dummy,
                                           self.externalShell, par);
            done := TRUE;
          EXCEPT
            PoolSet.Error(e) =>
            IF breakOnFailure THEN
              RAISE Error(e);
            ELSE
              (* silently skip exception *)
              MsgX.V(self.msgif,
                     "caught and ignored exception testing package " & pkg);
            END;
          END;
        END;
        IF done THEN
          IF selectOnZeroReturn THEN
            IF ret = 0 THEN
              res.addhi(pkg);
            END;
          ELSE
            IF ret # 0 THEN
              res.addhi(pkg);
            END;
          END;
        END;
      END;
    END;
    RETURN res;
  END SelectPackages;
---------------------------------------------------------------------------
PROCEDURE SelectByCmdList(self : T;
                          cmd  : TEXT;
                          ordered := TRUE;
                          selectOnZeroReturn := TRUE;
                          breakOnFailure := TRUE) : TextSeq.T RAISES {Error} =
  VAR
    pkgs : TextSeq.T;
    res  := NEW(TextSeq.T).init();
    ret  := 0;
  BEGIN
    IF ordered THEN
      pkgs := self.packageUpdateSequence();
    ELSE
      pkgs := self.packageList;
    END;
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH pkg = pkgs.get(i) DO
        IF Msg.vFlag THEN
          MsgX.V(self.msgif, "--- package " & pkg & ": selecting by `" &
            cmd & "' ---");
        END;
        TRY
          ret := ExecutionFailure;
          ret := self.poolset.execCmdList(pkg, cmd, self.externalShell);
          IF selectOnZeroReturn THEN
            IF ret = 0 THEN
              res.addhi(pkg);
            END;
          ELSE
            IF ret # 0 THEN
              res.addhi(pkg);
            END;
          END;
        EXCEPT
          PoolSet.Error(e) =>
          IF breakOnFailure THEN
            RAISE Error(e);
          ELSE
            (* silently skip exception *)
            MsgX.V(self.msgif, "caught and ignored exception");
          END;
        END;
      END;
    END;
    RETURN res;
  END SelectByCmdList;
---------------------------------------------------------------------------
PROCEDURE DependendPackages(self : T; pkg : TEXT) : TextSeq.T RAISES {} =
  VAR
    dep  : StdDepGraphNodeSeq.T;
    res  : TextSeq.T := NEW(TextSeq.T).init();
  BEGIN
    dep := self.depGraph.dependingNodes(pkg);
    FOR j := 0 TO dep.size() - 1 DO
      WITH dpkg = dep.get(j).name() DO
        res.addhi(dpkg);
      END;
    END;
    RETURN res;
  END DependendPackages;
---------------------------------------------------------------------------
PROCEDURE PackageDependencies(self : T; pkg : TEXT) : TextSeq.T RAISES {} =
  VAR
    dep  : StdDepGraphNodeSeq.T;
    res  : TextSeq.T := NEW(TextSeq.T).init();
  BEGIN
    dep := self.depGraph.nodeDependencies(pkg);
    FOR j := 0 TO dep.size() - 1 DO
      WITH dpkg = dep.get(j).name() DO
        res.addhi(dpkg);
      END;
    END;
    RETURN res;
  END PackageDependencies;
---------------------------------------------------------------------------
PROCEDURE ModifiedPackages(self : T) : TextSeq.T RAISES {Error} =
  VAR
    res : TextSeq.T;
  BEGIN
    res := self.selectPackages("modified", ordered := FALSE,
                               selectOnZeroReturn := TRUE,
                               breakOnFailure := TRUE);
    RETURN res;
  END ModifiedPackages;
---------------------------------------------------------------------------
PROCEDURE OutOfDatePackages(self : T) : TextSeq.T RAISES {Error} =
  VAR
    res : TextSeq.T;
  BEGIN
    res := self.selectPackages("uptodate", ordered := FALSE,
                               selectOnZeroReturn := FALSE,
                               breakOnFailure := TRUE);
    RETURN res;
  END OutOfDatePackages;
---------------------------------------------------------------------------
PROCEDURE UpToDatePackages(self : T) : TextSeq.T RAISES {Error} =
  VAR
    res : TextSeq.T;
  BEGIN
    res := self.selectPackages("uptodate", ordered := FALSE,
                               selectOnZeroReturn := TRUE,
                               breakOnFailure := TRUE);
    RETURN res;
  END UpToDatePackages;
---------------------------------------------------------------------------
PROCEDURE AddDependingPackages(self : T; pkgs : TextSeq.T) : TextSeq.T =
  VAR
    dep  : StdDepGraphNodeSeq.T;
    depp : TextSeq.T := NEW(TextSeq.T).init();
    res  : TextSeq.T := NEW(TextSeq.T).init();
    uds  : TextSeq.T;
  BEGIN
    IF self.depGraph = NIL THEN
      TRY
        BuildDepGraph(self);
      EXCEPT
        Error(e) =>
        MsgX.Fatal(self.msgif, "cannot build package dependency graph: " & e);
      END;
    END;
    FOR i := 0 TO pkgs.size() - 1 DO
      WITH mpkg = pkgs.get(i) DO
        depp.addhi(mpkg);
        dep := self.depGraph.dependingNodes(mpkg);
        FOR j := 0 TO dep.size() - 1 DO
          WITH dpkg = dep.get(j).name() DO
            depp.addhi(dpkg);
          END;
        END;
      END;
    END;
    (* depp contains all elements of pkgs and dependend packages, but probably
       a number of times and in wrong order *)
    uds := self.packageUpdateSequence();
    (* uds contains the correct order of updates for packages *)
    FOR i := 0 TO uds.size() - 1 DO
      WITH pkg = uds.get(i) DO
        IF MemberOfTextSeq(depp, pkg) THEN
          res.addhi(pkg);
        END;
      END;
    END;
    (* all the packages in depp have been added to res in the order of
       their occurence in uds *)
    RETURN res;
  END AddDependingPackages;
---------------------------------------------------------------------------
PROCEDURE ModifiedAndDependingPackages(self : T) : TextSeq.T RAISES {Error} =
  VAR
    mod  : TextSeq.T;
    res  : TextSeq.T;
  BEGIN
    mod := self.selectPackages("modified", ordered := FALSE,
                               selectOnZeroReturn := TRUE,
                               breakOnFailure := TRUE);
    (* mod contains all locally modified packages *)
    res := self.addDependingPackages(mod);
    RETURN res;
  END ModifiedAndDependingPackages;
---------------------------------------------------------------------------
PROCEDURE OutOfDateAndDependingPackages(self : T) : TextSeq.T RAISES {Error} =
  VAR
    ood  : TextSeq.T;
    res  : TextSeq.T;
  BEGIN
    ood := self.selectPackages("uptodate", ordered := FALSE,
                               selectOnZeroReturn := FALSE,
                               breakOnFailure := TRUE);
    (* mod contains all locally modified packages *)
    res := self.addDependingPackages(ood);
    RETURN res;
  END OutOfDateAndDependingPackages;
---------------------------------------------------------------------------
PROCEDURE PackagesWithConflicts(self : T) : TextSeq.T RAISES {Error} =
  VAR
    res : TextSeq.T;
  BEGIN
    res := self.selectPackages("conflicts", ordered := FALSE,
                               selectOnZeroReturn := TRUE,
                               breakOnFailure := TRUE);
    RETURN res;
  END PackagesWithConflicts;
---------------------------------------------------------------------------
PROCEDURE TestAllPackagesReleased(self : T) : BOOLEAN =
  BEGIN
    TRY
      RETURN self.applyToPackages("isrelease", ordered := FALSE,
                                  breakOnError := TRUE,
                                  breakOnFailure := TRUE) = 0;
    EXCEPT
      Error => RETURN FALSE;
    END;
  END TestAllPackagesReleased;
---------------------------------------------------------------------------
PROCEDURE TestNoPackageModified(self : T) : BOOLEAN =
  VAR sel : TextSeq.T;
  BEGIN
    TRY
      sel := self.selectPackages("modified", ordered := FALSE,
                                 selectOnZeroReturn := TRUE,
                                 breakOnFailure := TRUE);
      (* sel contains all modified packages *)
      RETURN sel.size() = 0;
    EXCEPT
      Error => RETURN FALSE;
    END;
  END TestNoPackageModified;
---------------------------------------------------------------------------
PROCEDURE BuildStateLabelTable(self : T; lazy := FALSE) : TextTextTextTbl.T
  RAISES {Error} =
  VAR
    pkg, fn  :  TEXT;
    tag      :  TEXT;
    label    :  TEXT;
    tabText  :  TEXT;
    tabLines :  TextSeq.T;
    tbl      :  TextTextTbl.T;      (* tag -> label *)
    res      :  TextTextTextTbl.T;  (* pkgname -> ( tag -> label ) *)
    found    :  BOOLEAN;
    dummy    :  TEXT;
  BEGIN
    res := NEW(TextTextTextTbl.Default).init(self.packageList.size());
    TRY
      fn := ".labels";
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        IF lazy THEN
          TRY
            tabText := self.poolset.fileContents(pkg, fn);
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "--- using old state labels of package " &
                pkg & " ---");
            END;
            found := TRUE;
          EXCEPT ELSE
            found := FALSE;
          END;
        END;
        IF NOT lazy OR NOT found THEN
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "--- reading state labels from package " &
              pkg & " ---");
          END;
	  IF self.poolset.execAction(pkg, "listlabels", dummy,
				     self.externalShell,
				     self.varTbl) = 0 THEN
	  ELSE
	    RAISE Error("cannot get state labels for package " & pkg);
	  END;
	  tabText := self.poolset.fileContents(pkg, fn);
        END;
        tbl := NEW(TextTextTbl.Default).init();
        tabLines := TextUtils.Split(tabText, "\n");
        FOR j := 0 TO tabLines.size() - 1 DO
          WITH line = tabLines.get(j) DO
            WITH elems = TextUtils.Split(line, "-->") DO
              IF elems.size() = 2 THEN
                tag   := TextUtils.Compress(elems.get(0));
                label := TextUtils.Compress(elems.get(1));
                EVAL tbl.put(tag, label);
              ELSE
                IF NOT Text.Empty(TextUtils.Compress(line)) THEN
                  MsgX.Warning(self.msgif,
                               "invalid line in state list table ignored: " &
                               line);
                END;
              END;
            END;
          END;
        END;
        EVAL res.put(pkg, tbl);
      END;
    EXCEPT
      Error(e) => RAISE Error(e);
      | PoolSet.Error(e) => RAISE Error(e);
    END;
    RETURN res;
  END BuildStateLabelTable;
---------------------------------------------------------------------------
PROCEDURE CacheAllStateLabels(self : T; lazy := FALSE) RAISES {Error} =
  BEGIN
    self.stateLabelTbl := BuildStateLabelTable(self, lazy);
  END CacheAllStateLabels;
---------------------------------------------------------------------------
PROCEDURE CheckLabelsInternal(self : T; snap : TextTextTbl.T;
                              pattern : TEXT) : TextSeq.T
  RAISES {Error} =
  VAR
    res  := NEW(TextSeq.T).init();
    iter := snap.iterate();
    tbl  :  TextTextTbl.T;
    re   :  RegEx.Pattern;
    pkg, tag, label : TEXT;
  BEGIN
    TRY
      re := RegEx.Compile(pattern);
    EXCEPT
      RegEx.Error(t) => RAISE Error("cannot compile regex " & pattern &
                                    ": " & t);
    END;
    WHILE iter.next(pkg, tag) DO
      IF self.stateLabelTbl.get(pkg, tbl) THEN
        IF tbl.get(tag, label) THEN
          IF RegEx.Execute(re, label) # -1 THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "  found match in package " & pkg &
                " version " & tag & " label " & label);
            END;
            res.addhi(pkg);
          END;
        ELSE
          MsgX.Error(self.msgif, "cannot get state label for pkg " & pkg &
            " tag " & tag);
        END;
      ELSE
        MsgX.Error(self.msgif, "cannot get state labels for pkg " & pkg);
      END;
    END;
    RETURN res;
  END CheckLabelsInternal;
---------------------------------------------------------------------------
PROCEDURE CheckCurrentLabelsGen(self : T; pattern : TEXT;
                                useCachedLabels := FALSE) : TextSeq.T
  RAISES {Error} =
  VAR
    snap := CurrentTagList(self);
  BEGIN
    IF NOT useCachedLabels THEN
      CacheAllStateLabels(self);
    END;
    RETURN CheckLabelsInternal(self, snap, pattern);
  END CheckCurrentLabelsGen;
---------------------------------------------------------------------------
PROCEDURE CheckCurrentLabels(self : T; pattern : TEXT;
                             lazy := FALSE) : TextSeq.T
  RAISES {Error} =
  VAR
    pkg, fn  :  TEXT;
    label    :  TEXT;
    res      := NEW(TextSeq.T).init();
    found    :  BOOLEAN;
    re       :  RegEx.Pattern;
  BEGIN
    TRY
      re := RegEx.Compile(pattern);
    EXCEPT
      RegEx.Error(t) => RAISE Error("cannot compile regex " & pattern &
                                    ": " & t);
    END;
    TRY
      fn := ".label";
      FOR i := 0 TO self.packageList.size() - 1 DO
        pkg := self.packageList.get(i);
        IF lazy THEN
          TRY
            label := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "--- using old state label of package " &
                pkg & " ---");
            END;
            found := TRUE;
          EXCEPT ELSE
            found := FALSE;
          END;
        END;
        IF NOT lazy OR NOT found THEN
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "--- reading state label from package " &
              pkg & " ---");
          END;
	  IF self.poolset.execAction(pkg, "currentlabel", label,
				     self.externalShell,
				     self.varTbl) = 0 THEN
	  ELSE
	    RAISE Error("cannot get current state label for package " & pkg);
	  END;
          IF label = NIL THEN
            label := TextUtils.Compress(self.poolset.fileContents(pkg, fn));
            self.poolset.setVal(pkg, "current-label", label);
          END;
        END;
        IF RegEx.Execute(re, label) # -1 THEN
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "  found match in package " & pkg & " label "
            & label);
          END;
          res.addhi(pkg);
        END;
      END;
    EXCEPT
      Error(e) => RAISE Error(e);
      | PoolSet.Error(e) => RAISE Error(e);
    END;
    RETURN res;
  END CheckCurrentLabels;
---------------------------------------------------------------------------
PROCEDURE CheckLabelsOfSnapshot(self : T; name : TEXT; pattern : TEXT;
                                useCachedLabels := FALSE) : TextSeq.T
  RAISES {Error} =
  VAR
    snap : TextTextTbl.T;
  BEGIN
    IF NOT useCachedLabels THEN
      CacheAllStateLabels(self);
    END;
    snap := Snapshot(self, name);
    IF snap = NIL THEN
      RAISE Error("there is no snapshot " & name);
    END;
    RETURN CheckLabelsInternal(self, snap, pattern);
  END CheckLabelsOfSnapshot;
---------------------------------------------------------------------------
PROCEDURE CheckLabelsOfRelease(self : T; name : TEXT; pattern : TEXT;
                               useCachedLabels := FALSE) : TextSeq.T
  RAISES {Error} =
  VAR
    snap : TextTextTbl.T;
  BEGIN
    IF NOT useCachedLabels THEN
      CacheAllStateLabels(self);
    END;
    snap := Release(self, name);
    IF snap = NIL THEN
      RAISE Error("there is no release " & name);
    END;
    RETURN CheckLabelsInternal(self, snap, pattern);
  END CheckLabelsOfRelease;
---------------------------------------------------------------------------
PROCEDURE NewCheckpoint(self : T; fn : TEXT; update := FALSE) RAISES {Error} =
  VAR
    cp : Checkpoint.T;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    TRY
      cp := self.poolset.newCheckpoint(update);
      cp.toFile(fn);
    EXCEPT
      PoolSet.Error(e) => RAISE Error(e);
    | Checkpoint.Error(e) => RAISE Error("NewCheckpoint: " & e);
    END
  END NewCheckpoint;
---------------------------------------------------------------------------
PROCEDURE LoadNewCheckpoint(self : T; fn : TEXT;
                            update := FALSE) RAISES {Error} =
  VAR
    cp : Checkpoint.T;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, "loading new checkpoint from file " & fn);
    self.oldState := self.poolset.cachedState();
    TRY
      cp := Checkpoint.New(self.poolset.getFileCache(), self.msgif);
      cp.fromFile(fn);
      IF update THEN
        IF NOT Msg.vFlag THEN
          MsgX.T(self.msgif,
                 "scanning all packages (loading new checkpoint)...");
        END;
        cp.update();
      END;
    EXCEPT
      Checkpoint.Error(e) => RAISE Error("error reading checkpoint: " & e);
    END;
    self.poolset.replaceStateCache(cp);
  END LoadNewCheckpoint;
---------------------------------------------------------------------------
PROCEDURE WriteCheckpoint(self : T; fn : TEXT) RAISES {Error} =
  VAR
    cp : Checkpoint.T;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, "writing checkpoint to file " & fn);
    TRY
      cp := self.poolset.cachedState();
      cp.toFile(fn);
    EXCEPT
      Checkpoint.Error(e) => RAISE Error("WriteCheckpoint: " & e);
    END
  END WriteCheckpoint;
---------------------------------------------------------------------------
PROCEDURE InvalidateCachedUnsureVersionInfo(self : T) RAISES {Error} =
  VAR pkg, loc : TEXT;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, "purging unsure version info...");
    FOR i := 0 TO self.packageList.size() - 1 DO
      pkg := self.packageList.get(i);
      IF NOT self.locationTbl.get(pkg, loc) THEN
        RAISE Error("no location for package " & pkg);
      END;
      WITH dir = Pathname.Join(loc, pkg, NIL) DO
        TRY
          self.poolset.updateStateCache(dir, "clear-utd-nocfl", 0);
          self.poolset.updateStateCache(dir, "clear-mod-unmod", 0);
          self.poolset.updateStateCache(dir, "clear-tags", 0);
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        END;
      END;
    END;
  END InvalidateCachedUnsureVersionInfo;
---------------------------------------------------------------------------
PROCEDURE InvalidateCachedBuildInfo(self : T) RAISES {Error} =
  VAR pkg, loc : TEXT;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, "purging build info...");
    FOR i := 0 TO self.packageList.size() - 1 DO
      pkg := self.packageList.get(i);
      IF NOT self.locationTbl.get(pkg, loc) THEN
        RAISE Error("no location for package " & pkg);
      END;
      WITH dir = Pathname.Join(loc, pkg, NIL) DO
        TRY
          self.poolset.updateStateCache(dir, "need-build-ship", 0);
          self.poolset.updateStateCache(dir, "need-mkdep-build-ship", 0);
        EXCEPT
          PoolSet.Error(e) => RAISE Error(e);
        END;
      END;
    END;
  END InvalidateCachedBuildInfo;
---------------------------------------------------------------------------
VAR NL := "\n";
BEGIN
  IF Text.Equal(MxConfig.HOST_OS_TYPE, "WIN32") THEN
    NL := "\r\n";
  END;
END PrjDesc.

interface TextUtils is in:


interface RegEx is in: