prjbase/src/PoolSet.m3


---------------------------------------------------------------------------
MODULE PoolSet;

IMPORT Text, TextSeq, TextTextTbl, Pathname, Thread, Process,
       Rd, FileRd, Fmt, TextConv, ASCII;
IMPORT PkgBase, System, TextUtils, PathRepr, FileInfo, Checkpoint,
       PkgVC, TextPkgVCTbl, Tag, VCUtils, SMsg AS Msg,
       APN AS APN, FSUtils, RegEx, MsgX, MsgIF;
---------------------------------------------------------------------------
CONST
  Undefined = "%$#&!42?";
---------------------------------------------------------------------------
PROCEDURE NoVCIF(<*UNUSED*> self : PkgVCAccessor;
                 <*UNUSED*> dir  : Pathname.T) : PkgVC.T RAISES {} =
  BEGIN
    RETURN NIL;
  END NoVCIF;
---------------------------------------------------------------------------
PROCEDURE NewVCIF(self : PkgVCCreator; dir : Pathname.T) : PkgVC.T
  RAISES {Error} =
  VAR res : PkgVC.T;
  BEGIN
    IF dir = NIL THEN
      RAISE Error("cannot create version control object: directory NIL");
    END;
    TRY
      dir := FSUtils.CanonicalPathname(dir);
    EXCEPT
      FSUtils.E(e) => RAISE Error(e);
    END;
    res := NEW(PkgVC.T).init(self.msgif);
    TRY
      res.setPackageRoot(APN.New(dir));
      IF self.env # NIL THEN
        res.setEnvironment(self.env);
      END;
    EXCEPT
      PkgVC.E(e) => RAISE Error("cannot create version control object: " & e);
    END;
    RETURN res;
  END NewVCIF;
---------------------------------------------------------------------------
REVEAL
  T = Public BRANDED "PoolSet Type 0.0" OBJECT
    pools      : TextSeq.T;     (* list of pools *)
    location   : TextTextTbl.T; (* mapping from package name -> pool (path) *)
    type       : TextTextTbl.T; (* mapping from package name -> package type *)
    pkgvc      : TextPkgVCTbl.T;(* mapping from package name -> vc interface *)
    cfg        : PkgBase.T;
    prefkind   : PkgBase.Kind;
    fileCache  : FileInfo.T;
    stateCache : Checkpoint.T;
    useCache   : BOOLEAN;
    cacheEarly : BOOLEAN;
    msgif      : MsgIF.T;
    pkgvcAcc   : PkgVCAccessor;
    internalVC : BOOLEAN;
    verboseCache : BOOLEAN;
  METHODS
  OVERRIDES
    init := Init;
    prependPool := PrependPool;
    appendPool := AppendPool;
    setPreferredPkgKind := SetPreferredPkgKind;
    exists := Exists;
    pkgPath := PkgPath;
    pkgType := PkgType;
    pkgVCIF := PkgVCIF;
    checkAll := CheckAll;
    execAction := ExecAction;
    execCmdList := ExecCmdList;
    getCmdOutput := GetCmdOutput;
    getAndCacheVersionState := GetAndCacheVersionState;
    fileContents := FileContents;
    checkout := Checkout;
    getFileCache := GetFileCache;
    cachedState := CachedState;
    updateCache := UpdateCache;
    newCheckpoint := NewCheckpoint;
    replaceStateCache := ReplaceStateCache;
    setAttr := SetAttr;
    clearAttr := ClearAttr;
    attrIsSet := AttrIsSet;
    setVal := SetVal;
    getVal := GetVal;
    delVal := DelVal;
    updateStateCache := UpdateStateCache;
    actionProbablyNeeded := ActionProbablyNeeded;
    dumpStateCache := DumpStateCache;
  END;
---------------------------------------------------------------------------
PROCEDURE Init(self : T; cfg : PkgBase.T;
               fn  : TEXT := NIL;
               useCache := TRUE;
               p1  : Pathname.T := NIL;
               p2  : Pathname.T := NIL;
               p3  : Pathname.T := NIL;
               p4  : Pathname.T := NIL;
               p5  : Pathname.T := NIL;
               msgif  : MsgIF.T := NIL;
               pkgvcAcc : PkgVCAccessor := NIL;
               verboseCache := TRUE;
               prefkind : TEXT := NIL;
               cacheEarly := FALSE) : T RAISES {Error} =
    (*
       Initialize the search list with the given paths. `p1' has the greatest
       priority. The package type and action configuration must be contained
       in `cfg'.
    *)
  BEGIN
    self.pkgvcAcc := pkgvcAcc;
    self.internalVC := pkgvcAcc # NIL;
    self.msgif := msgif;
    self.useCache := useCache;
    self.verboseCache := verboseCache;
    self.cfg := cfg;
    self.pools := NEW(TextSeq.T).init(5);
    self.location := NEW(TextTextTbl.Default).init(40);
    self.type := NEW(TextTextTbl.Default).init(40);
    self.cacheEarly := cacheEarly;
    IF self.internalVC THEN
      self.pkgvc := NEW(TextPkgVCTbl.Default).init(40);
    ELSE
      self.pkgvc := NEW(TextPkgVCTbl.Default).init(1);
    END;
    self.prefkind := prefkind;
    self.fileCache := NEW(FileInfo.T).init(1000, APN.New(PathRepr.RootDir),
                                           self.msgif);
    self.cfg.setCache(self.fileCache);
    self.stateCache := Checkpoint.New(self.fileCache, self.msgif);
    IF useCache AND fn # NIL AND FSUtils.IsFile(fn) THEN
      TRY
        self.stateCache.fromFile(fn);
      EXCEPT
        Checkpoint.Error(e) => RAISE Error(e);
      END;
    END;
    IF p1 # NIL THEN
      AppendPool(self, p1);
    END;
    IF p2 # NIL THEN
      AppendPool(self, p2);
    END;
    IF p3 # NIL THEN
      AppendPool(self, p3);
    END;
    IF p4 # NIL THEN
      AppendPool(self, p4);
    END;
    IF p5 # NIL THEN
      AppendPool(self, p5);
    END;
    RETURN self;
  END Init;
---------------------------------------------------------------------------
PROCEDURE PrependPool(self : T; p : Pathname.T) RAISES {Error} =
    (* Prepend `p' to the search list. *)
  BEGIN
    TRY
      WITH pn = FSUtils.CanonicalPathname(PathRepr.Native(p)) DO
        self.pools.addlo(pn);
        IF self.cacheEarly AND self.useCache THEN
          IF self.verboseCache AND NOT Msg.vFlag THEN
            MsgX.T(self.msgif, "caching " & pn);
          END;
          self.fileCache.updateRec(APN.New(pn), NIL, NIL,
                                   Checkpoint.skipDirExpr,
                                   Checkpoint.skipFileExpr);
        END;
      END;
    EXCEPT
      FSUtils.E(e) => RAISE Error(e);
    END;
  END PrependPool;
---------------------------------------------------------------------------
PROCEDURE AppendPool(self : T; p : Pathname.T) RAISES {Error} =
    (* Append `p' to the search list. *)
  BEGIN
    TRY
      WITH pn = FSUtils.CanonicalPathname(PathRepr.Native(p)) DO
        self.pools.addhi(pn);
        IF self.cacheEarly AND self.useCache THEN
          IF self.verboseCache AND NOT Msg.vFlag THEN
            MsgX.T(self.msgif, "caching " & pn);
          END;
          self.fileCache.updateRec(APN.New(pn), NIL, NIL,
                                   Checkpoint.skipDirExpr,
                                   Checkpoint.skipFileExpr);
        END;
      END;
    EXCEPT
      FSUtils.E(e) => RAISE Error(e);
    END;
  END AppendPool;
---------------------------------------------------------------------------
PROCEDURE SetPreferredPkgKind(self : T; k : PkgBase.Kind) =
  BEGIN
    self.prefkind := k;
  END SetPreferredPkgKind;
---------------------------------------------------------------------------
PROCEDURE Exists(self : T; pkg : PkgBase.Name;
                 hint : Pathname.T := NIL) : BOOLEAN RAISES {Error} =
    (*
       Check for the existence (and the type) of package `pkg' in the list
       of pools and remember the results in an internal cache. Return
       TRUE if the package was found in one of the pools.
    *)
  VAR
    cached :  BOOLEAN;
    path   :  TEXT;
    kind   :  PkgBase.Kind;
    found  := FALSE;
    hintt  :  TEXT;
  BEGIN
    IF hint = NIL THEN
      hintt := "NIL";
    ELSE
      TRY
        hint := FSUtils.CanonicalPathname(hint);
      EXCEPT
        FSUtils.E(e) => RAISE Error(e);
      END;
      hintt := hint;
    END;
    IF Msg.dFlag THEN
      MsgX.D(self.msgif, "PoolSet.Exists(" & pkg & ", " & hintt & ")");
    END;
    cached := self.location.get(pkg, path);
    IF NOT cached THEN
      IF hint # NIL THEN
        path := Pathname.Join(PathRepr.Native(hint), pkg, NIL);
        kind := self.prefkind;
	found := self.cfg.kindFound(path, kind);
      ELSE
        FOR i := 0 TO self.pools.size() - 1 DO
          path := Pathname.Join(self.pools.get(i), pkg, NIL);
          kind := self.prefkind;
          found := self.cfg.kindFound(path, kind);
          IF found THEN EXIT END;
          IF Msg.dFlag THEN
            MsgX.D(self.msgif, "package " & pkg & " not found at " & path);
          END;
        END;
      END;
      IF found THEN
        EVAL self.location.put(pkg, path);
        EVAL self.type.put(pkg, kind);
        IF Msg.dFlag THEN
          MsgX.D(self.msgif, "package " & pkg & " kind " & kind &
            " found at " & path);
          MsgX.D(self.msgif, "checkpointing package " & path);
        END;
        TRY
          IF self.useCache THEN
            IF self.verboseCache AND NOT Msg.vFlag THEN
              MsgX.T(self.msgif, "scanning " & path);
            END;
            self.stateCache.update(path, self.cacheEarly);
          END;
        EXCEPT
          Checkpoint.Error(e) => RAISE Error(e);
        END;
      ELSE
        EVAL self.location.put(pkg, Undefined);
        EVAL self.type.put(pkg, Undefined);
      END;
      RETURN found;
    END;
    RETURN NOT Text.Equal(path, Undefined);
  END Exists;
---------------------------------------------------------------------------
PROCEDURE PkgPath(self : T; name : PkgBase.Name) : Pathname.T =
    (*
      Return the path of the package with name `name' if cached,
      NIL else.
    *)
  VAR path : Pathname.T;
  BEGIN
    IF self.location.get(name, path) THEN
      RETURN path;
    ELSE
      RETURN NIL;
    END;
  END PkgPath;
---------------------------------------------------------------------------
PROCEDURE PkgVCIF(self : T; name : PkgBase.Name) : PkgVC.T RAISES {Error} =
  VAR
    res : PkgVC.T;
    dir : TEXT;
  BEGIN
    IF NOT self.internalVC THEN RETURN NIL END;
    IF NOT self.pkgvc.get(name, res) THEN
      dir := PkgPath(self, name);
      IF dir = NIL THEN
        RAISE Error("cannot create version control object for missing " &
              "package: " & name);
      END;
      res := self.pkgvcAcc.getVCIF(dir);
      EVAL self.pkgvc.put(name, res);
    END;
    RETURN res;
  END PkgVCIF;
---------------------------------------------------------------------------
PROCEDURE CheckAll(self : T; pkgList : TextSeq.T; VAR res : TEXT;
                   VAR missingPackages : TextSeq.T;
                   hints : TextTextTbl.T := NIL; checkHomogeneity := TRUE;
                   ignoreMissingPackages := FALSE) : BOOLEAN =
    (*
      Check for the existence and (type) homogeneity of all packages
      in `pkgList'. Return TRUE if all packages exist and are of the
      same type.
    *)
  VAR
    pkg     : PkgBase.Name := "";
    actPkg  : PkgBase.Name;
    kind    : PkgBase.Kind := NIL;
    actKind : PkgBase.Kind;
    hint    : TEXT;
  BEGIN
    FOR i := 0 TO pkgList.size() - 1 DO
      actPkg := pkgList.get(i);
      IF hints # NIL THEN
        IF hints.get(actPkg, hint) THEN
          hint := PathRepr.Native(hint);
        ELSE
          hint := NIL;
        END;
      ELSE
        hint := NIL;
      END;
      TRY
        IF NOT self.exists(actPkg, hint) THEN
          IF missingPackages = NIL THEN
            missingPackages := NEW(TextSeq.T).init();
          END;
          missingPackages.addhi(actPkg);
          IF NOT ignoreMissingPackages THEN
            res := actPkg & " does not exist";
            RETURN FALSE;
          END;
        END;
      EXCEPT
        Error(e) => res := e; RETURN FALSE;
      END;
      (* package exists, location and kind cached *)
      IF kind = NIL THEN
        kind := self.pkgType(actPkg);
        pkg := actPkg;
      ELSE
        actKind := self.pkgType(actPkg);
        IF checkHomogeneity AND NOT Text.Equal(kind, actKind) THEN
          res := "different kinds of packages found:  " &
                 pkg & "->" & kind & ", " & actPkg & "->" & actKind;
          RETURN FALSE;
        END;
      END;
    END;
    RETURN TRUE;
  END CheckAll;
---------------------------------------------------------------------------
PROCEDURE PkgType(self : T; name : PkgBase.Name) : PkgBase.Kind =
    (* Return the type of package `name'. *)
  VAR kind : PkgBase.Kind;
  BEGIN
    IF self.type.get(name, kind) THEN
      RETURN kind;
    ELSE
      RETURN NIL;
    END;
  END PkgType;
---------------------------------------------------------------------------
PROCEDURE UpdateStateCache(self : T; dir : Pathname.T;
                           action : PkgBase.Action; ret : INTEGER;
                           rescan := TRUE)
  RAISES {Error} =

  PROCEDURE SetOnSuccess(attr : Checkpoint.Attr) =
    BEGIN
      IF ret = 0 THEN
        attrs := attrs + Checkpoint.AttrSet{attr};
      ELSE
        attrs := attrs - Checkpoint.AttrSet{attr};
      END;
    END SetOnSuccess;

  PROCEDURE ClearOnSuccess(attr : Checkpoint.Attr) =
    BEGIN
      IF ret = 0 THEN
        attrs := attrs - Checkpoint.AttrSet{attr};
      END;
    END ClearOnSuccess;

  PROCEDURE SetOnFailure(attr : Checkpoint.Attr) =
    BEGIN
      IF ret = 0 THEN
        attrs := attrs - Checkpoint.AttrSet{attr};
      ELSE
        attrs := attrs + Checkpoint.AttrSet{attr};
      END;
    END SetOnFailure;

  PROCEDURE Clear(attr : Checkpoint.Attr) =
    BEGIN
      attrs := attrs - Checkpoint.AttrSet{attr};
    END Clear;

  PROCEDURE SetDefaultUnbuilt() =
    BEGIN
      attrs := attrs - Checkpoint.AttrSet{
                         Checkpoint.Attr.DepMade,
                         Checkpoint.Attr.BuildOk,
                         Checkpoint.Attr.BuildOkL,
                         Checkpoint.Attr.BuildFailed,
                         Checkpoint.Attr.ShippedToLP,
                         Checkpoint.Attr.ShippedToPP,
                         Checkpoint.Attr.ShippedToGP
                       };
    END SetDefaultUnbuilt;

  PROCEDURE SetDefaultRebuild() =
    BEGIN
      attrs := attrs - Checkpoint.AttrSet{
                         Checkpoint.Attr.BuildOk,
                         Checkpoint.Attr.BuildOkL,
                         Checkpoint.Attr.BuildFailed,
                         Checkpoint.Attr.ShippedToLP,
                         Checkpoint.Attr.ShippedToPP,
                         Checkpoint.Attr.ShippedToGP
                       };
    END SetDefaultRebuild;

  PROCEDURE SetDefaultUnshipped() =
    BEGIN
      attrs := attrs - Checkpoint.AttrSet{
                         Checkpoint.Attr.ShippedToGP,
                         Checkpoint.Attr.ShippedToPP,
                         Checkpoint.Attr.ShippedToLP
                       };
    END SetDefaultUnshipped;

  PROCEDURE ClearPackageTags() RAISES {Error} =
    BEGIN
      TRY
        self.stateCache.delVal(dir, "sticky-tag");
        self.stateCache.delVal(dir, "release-tag");
        self.stateCache.delVal(dir, "current-tag");
        self.stateCache.delVal(dir, "current-release-tag");
        self.stateCache.delVal(dir, "current-devel-tag");
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot delete checkpoint value " & pkg & ": " & e);
      END;
    END ClearPackageTags;

  PROCEDURE ClearPkgVCCache() RAISES {Error} =
    BEGIN
      IF self.pkgvcAcc # NIL THEN
        WITH vc = self.pkgVCIF(pkg) DO
          vc.flushCache();
        END;
      END;
    END ClearPkgVCCache;

  VAR
    pkg    := Pathname.Last(dir);
    attrs  :  Checkpoint.AttrSet;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    TRY
      attrs := self.stateCache.getAttr(dir);
    EXCEPT
      Checkpoint.Error =>
      TRY
        self.stateCache.update(dir);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot set attributes for package " & pkg & ": " & e);
      END;
    END;

    IF Text.Equal(action, "build") THEN
      SetOnSuccess(Checkpoint.Attr.BuildOk);
      ClearOnSuccess(Checkpoint.Attr.BuildOkL);
      SetOnFailure(Checkpoint.Attr.BuildFailed);
      SetDefaultUnshipped();
    ELSIF Text.Equal(action, "buildlocal") THEN
      SetOnSuccess(Checkpoint.Attr.BuildOkL);
      ClearOnSuccess(Checkpoint.Attr.BuildOk);
      SetOnFailure(Checkpoint.Attr.BuildFailed);
      SetDefaultUnshipped();
    ELSIF Text.Equal(action, "checkconflicts") THEN
      SetOnSuccess(Checkpoint.Attr.Conflicts);
      SetOnFailure(Checkpoint.Attr.NoConflicts);
      rescan := FALSE;
    ELSIF Text.Equal(action, "checkmodified") THEN
      SetOnSuccess(Checkpoint.Attr.Modified);
      SetOnFailure(Checkpoint.Attr.Unmodified);
      rescan := FALSE;
    ELSIF Text.Equal(action, "checkout") THEN
      SetDefaultUnbuilt();
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.OutOfDate);
      ClearOnSuccess(Checkpoint.Attr.IsRelease);
      ClearOnSuccess(Checkpoint.Attr.NoRelease);
      ClearPackageTags();
      ClearPkgVCCache();
    ELSIF Text.Equal(action, "checkrelease") THEN
      SetOnSuccess(Checkpoint.Attr.IsRelease);
      SetOnFailure(Checkpoint.Attr.NoRelease);
      rescan := FALSE;
    ELSIF Text.Equal(action, "checkuptodate") THEN
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnFailure(Checkpoint.Attr.OutOfDate);
      rescan := FALSE;
    ELSIF Text.Equal(action, "clean") THEN
      SetDefaultUnbuilt();
    ELSIF Text.Equal(action, "commitdevelmajor") THEN
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.Conflicts);
      ClearOnSuccess(Checkpoint.Attr.IsRelease);
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnSuccess(Checkpoint.Attr.Unmodified);
      SetOnSuccess(Checkpoint.Attr.NoRelease);
      ClearPackageTags();
      ClearPkgVCCache();
    ELSIF Text.Equal(action, "commitdevelminor") THEN
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.Conflicts);
      ClearOnSuccess(Checkpoint.Attr.IsRelease);
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnSuccess(Checkpoint.Attr.Unmodified);
      SetOnSuccess(Checkpoint.Attr.NoRelease);
      ClearPackageTags();
      ClearPkgVCCache();
    ELSIF Text.Equal(action, "commitdevelpatch") THEN
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.Conflicts);
      ClearOnSuccess(Checkpoint.Attr.IsRelease);
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnSuccess(Checkpoint.Attr.Unmodified);
      SetOnSuccess(Checkpoint.Attr.NoRelease);
      ClearPackageTags();
      ClearPkgVCCache();
    ELSIF Text.Equal(action, "commitreleasemajor") THEN
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.Conflicts);
      ClearOnSuccess(Checkpoint.Attr.NoRelease);
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnSuccess(Checkpoint.Attr.Unmodified);
      SetOnSuccess(Checkpoint.Attr.IsRelease);
      ClearPackageTags();
      ClearPkgVCCache();
      SetDefaultUnbuilt();
    ELSIF Text.Equal(action, "commitreleaseminor") THEN
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.Conflicts);
      ClearOnSuccess(Checkpoint.Attr.NoRelease);
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnSuccess(Checkpoint.Attr.Unmodified);
      SetOnSuccess(Checkpoint.Attr.IsRelease);
      ClearPackageTags();
      ClearPkgVCCache();
      SetDefaultUnbuilt();
    ELSIF Text.Equal(action, "commitreleasepatch") THEN
      ClearOnSuccess(Checkpoint.Attr.Modified);
      ClearOnSuccess(Checkpoint.Attr.Conflicts);
      ClearOnSuccess(Checkpoint.Attr.NoRelease);
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnSuccess(Checkpoint.Attr.Unmodified);
      SetOnSuccess(Checkpoint.Attr.IsRelease);
      ClearPackageTags();
      ClearPkgVCCache();
      SetDefaultUnbuilt();
    ELSIF Text.Equal(action, "conflicts") THEN
      SetOnSuccess(Checkpoint.Attr.Conflicts);
      SetOnFailure(Checkpoint.Attr.NoConflicts);
      rescan := FALSE;
    ELSIF Text.Equal(action, "currentdeveltag") THEN
    ELSIF Text.Equal(action, "currentlabel") THEN
    ELSIF Text.Equal(action, "currentreleasetag") THEN
    ELSIF Text.Equal(action, "currenttag") THEN
    ELSIF Text.Equal(action, "externalshell") THEN
      rescan := FALSE;
    ELSIF Text.Equal(action, "getlabel") THEN
    ELSIF Text.Equal(action, "isrelease") THEN
      SetOnSuccess(Checkpoint.Attr.IsRelease);
      SetOnFailure(Checkpoint.Attr.NoRelease);
      rescan := FALSE;
    ELSIF Text.Equal(action, "listlabels") THEN
    ELSIF Text.Equal(action, "mkdep") THEN
      SetOnSuccess(Checkpoint.Attr.DepMade);
    ELSIF Text.Equal(action, "modified") THEN
      SetOnSuccess(Checkpoint.Attr.Modified);
      SetOnFailure(Checkpoint.Attr.Unmodified);
      rescan := FALSE;
    ELSIF Text.Equal(action, "realclean") THEN
      SetDefaultUnbuilt();
    ELSIF Text.Equal(action, "setlabel") THEN
      self.delVal(pkg, "current-label");
    ELSIF Text.Equal(action, "shipglobal") THEN
      SetOnSuccess(Checkpoint.Attr.ShippedToGP);
    ELSIF Text.Equal(action, "shiplocal") THEN
      SetOnSuccess(Checkpoint.Attr.ShippedToLP);
    ELSIF Text.Equal(action, "shipproject") THEN
      SetOnSuccess(Checkpoint.Attr.ShippedToPP);
    ELSIF Text.Equal(action, "update") THEN
      SetDefaultUnbuilt();
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      ClearOnSuccess(Checkpoint.Attr.OutOfDate);
      ClearOnSuccess(Checkpoint.Attr.IsRelease);
      ClearOnSuccess(Checkpoint.Attr.NoRelease);
      ClearPackageTags();
      ClearPkgVCCache();
    ELSIF Text.Equal(action, "merge") OR Text.Equal(action, "merge2") THEN
      SetDefaultUnbuilt();
      ClearPackageTags();
      ClearPkgVCCache();
      Clear(Checkpoint.Attr.Modified);
      Clear(Checkpoint.Attr.Unmodified);
      Clear(Checkpoint.Attr.UpToDate);
      Clear(Checkpoint.Attr.NoConflicts);
    ELSIF Text.Equal(action, "uptodate") THEN
      SetOnSuccess(Checkpoint.Attr.UpToDate);
      SetOnFailure(Checkpoint.Attr.OutOfDate);
      rescan := FALSE;
    ELSIF Text.Equal(action, "any-user-cmd") THEN
    ELSIF Text.Equal(action, "need-mkdep-build-ship") THEN
      IF Checkpoint.Attr.Changed IN attrs THEN
        SetDefaultUnbuilt();
        rescan := FALSE;
      END;
    ELSIF Text.Equal(action, "need-build-ship") THEN
      SetDefaultRebuild();
      rescan := FALSE;
    ELSIF Text.Equal(action, "clear-mod-unmod") THEN
      Clear(Checkpoint.Attr.Modified);
      Clear(Checkpoint.Attr.Unmodified);
      ClearPkgVCCache();
      rescan := FALSE;
    ELSIF Text.Equal(action, "clear-utd-nocfl") THEN
      Clear(Checkpoint.Attr.UpToDate);
      Clear(Checkpoint.Attr.NoConflicts);
      ClearPkgVCCache();
      self.delVal(pkg, "current-label");
      rescan := FALSE;
    ELSIF Text.Equal(action, "clear-tags") THEN
      ClearOnSuccess(Checkpoint.Attr.IsRelease);
      ClearOnSuccess(Checkpoint.Attr.NoRelease);
      ClearPackageTags();
      ClearPkgVCCache();
      rescan := FALSE;
    ELSIF Text.Equal(action, "diff") THEN
      rescan := FALSE;
    ELSIF Text.Equal(action, "diff1") THEN
      rescan := FALSE;
    ELSIF Text.Equal(action, "diff2") THEN
      rescan := FALSE;
    END;
    Clear(Checkpoint.Attr.Changed);
    TRY
      dir := FSUtils.CanonicalPathname(dir);
      self.stateCache.setAttr(dir, attrs);
    EXCEPT
      Checkpoint.Error(e) =>
      RAISE Error("cannot set attributes for package " & pkg & ": " & e);
    | FSUtils.E(e) =>
      RAISE Error("cannot set attributes for package " & pkg & ": " & e);
    END;
    IF rescan THEN
      TRY
        IF self.verboseCache AND NOT Msg.vFlag THEN
          MsgX.T(self.msgif, "rescanning " & dir);
        END;
        self.stateCache.update(dir);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot checkpoint package " & pkg & ": " & e);
      END;
    END;
  END UpdateStateCache;
---------------------------------------------------------------------------
PROCEDURE ActionProbablyNeeded(self    : T;
                               pkg     : PkgBase.Name;
                               dir     : Pathname.T;
                               action  : PkgBase.Action;
                               VAR ret : INTEGER;
                               VAR res : TEXT) : BOOLEAN
  RAISES {Error} =
  VAR
    attrs  :  Checkpoint.AttrSet;
  BEGIN
    res := NIL;
    ret := 0;
    (* `ret' is the fake return value of the action. `0' means
       `okay', `yes', and `true', everything else `failure',
       `no', and `false'. *)
    IF NOT self.useCache THEN RETURN TRUE END;
    TRY
      dir := FSUtils.CanonicalPathname(dir);
    EXCEPT
      FSUtils.E(e) => RAISE Error(e);
    END;
    TRY
      attrs := self.stateCache.getAttr(dir);
    EXCEPT
      Checkpoint.Error(e) =>
      RAISE Error("cannot set attributes for package " & pkg & ": " & e);
    END;

    IF Text.Equal(action, "build") THEN
      RETURN Checkpoint.Attr.BuildFailed IN attrs OR
             NOT Checkpoint.Attr.BuildOk IN attrs;
    ELSIF Text.Equal(action, "buildlocal") THEN
      RETURN Checkpoint.Attr.BuildFailed IN attrs OR
             NOT Checkpoint.Attr.BuildOkL IN attrs;
    ELSIF Text.Equal(action, "checkconflicts") THEN
      IF Checkpoint.Attr.Conflicts IN attrs THEN
        ret := 0; RETURN TRUE;
      ELSIF Checkpoint.Attr.NoConflicts IN attrs THEN
        ret := 1; RETURN TRUE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "checkmodified") THEN
      IF Checkpoint.Attr.Modified IN attrs THEN
        ret := 0; RETURN TRUE;
      ELSIF Checkpoint.Attr.Unmodified IN attrs THEN
        ret := 1; RETURN FALSE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "checkout") THEN
    ELSIF Text.Equal(action, "checkrelease") THEN
      IF Checkpoint.Attr.IsRelease IN attrs THEN
        ret := 0; RETURN FALSE;
      ELSIF Checkpoint.Attr.NoRelease IN attrs THEN
        ret := 1; RETURN FALSE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "checkuptodate") THEN
      IF Checkpoint.Attr.UpToDate IN attrs THEN
        ret := 0; RETURN TRUE;
      ELSIF Checkpoint.Attr.OutOfDate IN attrs THEN
        ret := 1; RETURN TRUE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "clean") THEN
    ELSIF Text.Equal(action, "commitdevelmajor") THEN
    ELSIF Text.Equal(action, "commitdevelminor") THEN
    ELSIF Text.Equal(action, "commitdevelpatch") THEN
    ELSIF Text.Equal(action, "commitreleasemajor") THEN
    ELSIF Text.Equal(action, "commitreleaseminor") THEN
    ELSIF Text.Equal(action, "commitreleasepatch") THEN
    ELSIF Text.Equal(action, "conflicts") THEN
      IF Checkpoint.Attr.Conflicts IN attrs THEN
        ret := 0; RETURN FALSE;
      ELSIF Checkpoint.Attr.NoConflicts IN attrs THEN
        ret := 1; RETURN FALSE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "currentdeveltag") THEN
      res := self.getVal(pkg, "current-devel-tag");
      RETURN res = NIL;
    ELSIF Text.Equal(action, "currentlabel") THEN
      res := self.getVal(pkg, "current-label");
      RETURN res = NIL;
    ELSIF Text.Equal(action, "currentreleasetag") THEN
      res := self.getVal(pkg, "current-release-tag");
      RETURN res = NIL;
    ELSIF Text.Equal(action, "currenttag") THEN
      res := self.getVal(pkg, "current-tag");
      RETURN res = NIL;
    ELSIF Text.Equal(action, "externalshell") THEN
    ELSIF Text.Equal(action, "getlabel") THEN
    ELSIF Text.Equal(action, "isrelease") THEN
      res := self.getVal(pkg, "release-tag");
      IF Checkpoint.Attr.IsRelease IN attrs THEN
        ret := 0; RETURN FALSE;
      ELSIF Checkpoint.Attr.NoRelease IN attrs THEN
        ret := 1; RETURN FALSE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "listlabels") THEN
    ELSIF Text.Equal(action, "mkdep") THEN
      RETURN NOT Checkpoint.Attr.DepMade IN attrs;
    ELSIF Text.Equal(action, "modified") THEN
      IF Checkpoint.Attr.Modified IN attrs THEN
        ret := 0; RETURN FALSE;
      ELSIF Checkpoint.Attr.Unmodified IN attrs THEN
        ret := 1; RETURN FALSE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "realclean") THEN
    ELSIF Text.Equal(action, "setlabel") THEN
    ELSIF Text.Equal(action, "shipglobal") THEN
      RETURN NOT Checkpoint.Attr.ShippedToGP IN attrs;
    ELSIF Text.Equal(action, "shiplocal") THEN
      RETURN NOT Checkpoint.Attr.ShippedToLP IN attrs;
    ELSIF Text.Equal(action, "shipproject") THEN
      RETURN NOT Checkpoint.Attr.ShippedToPP IN attrs;
    ELSIF Text.Equal(action, "update") THEN
    ELSIF Text.Equal(action, "uptodate") THEN
      IF Checkpoint.Attr.UpToDate IN attrs THEN
        ret := 0; RETURN FALSE;
      ELSIF Checkpoint.Attr.OutOfDate IN attrs THEN
        ret := 1; RETURN FALSE;
      ELSE
        RETURN TRUE;
      END;
    ELSIF Text.Equal(action, "any-user-cmd") THEN
    ELSIF Text.Equal(action, "evaluate-changes") THEN
    END;

    RETURN TRUE;
  END ActionProbablyNeeded;
---------------------------------------------------------------------------
PROCEDURE ResultFromInternalVersionControl(
    self       : T;
    pkg        : PkgBase.Name;
    action     : PkgBase.Action;
    parameters : TextTextTbl.T;
    VAR res    : TEXT;
    VAR ret    : INTEGER) : BOOLEAN RAISES {Error} =

  PROCEDURE Ret(b : BOOLEAN) : INTEGER =
    BEGIN
      IF b THEN
        RETURN 0;
      ELSE
        RETURN 1;
      END;
    END Ret;

  PROCEDURE TagText(tag : Tag.T) : TEXT =
    BEGIN
      IF tag = NIL THEN
        RETURN "no appropriate tag found";
      END;
      RETURN tag.denotation();
    END TagText;

  CONST DoubleQuote = '\"';
  CONST Quote = '\'';

  PROCEDURE EvalMsgOptions() =
    VAR opts : TEXT;

    PROCEDURE EvalOpts() =
      VAR args, res : TEXT;

      PROCEDURE EvalOpt(name : TEXT; VAR res : TEXT) : BOOLEAN =
        VAR
          start, end : INTEGER;
          c : CHAR;

          PROCEDURE FindNext(c : CHAR) =
            BEGIN
              WHILE end < Text.Length(args) AND Text.GetChar(args, end) # c DO
                INC(end);
              END;
              IF end = Text.Length(args) THEN
                INC(end);
              END;
            END FindNext;

        BEGIN
          start := TextUtils.Pos(args, name);
          IF start > -1 THEN
            INC(start, Text.Length(name));
            WHILE start < Text.Length(args) AND
              Text.GetChar(args, start) IN ASCII.Spaces DO
              INC(start);
            END;
            IF start < Text.Length(args) THEN
              c := Text.GetChar(args, start);
              IF c = DoubleQuote THEN
                INC(start);
                end := start;
                FindNext(c);
              ELSIF c = Quote THEN
                INC(start);
                end := start;
                FindNext(c);
              ELSE
                end := start;
                FindNext(' ');
              END;
              res := Text.Sub(args, start, end - start);
              RETURN TRUE;
            END;
          END;
          RETURN FALSE;
        END EvalOpt;

      BEGIN
        TRY
          args := TextConv.Decode(opts, FALSE);
        EXCEPT ELSE
          MsgX.Error(self.msgif, "cannot un-escape options line: " & opts);
          args := opts;
        END;
        IF Msg.dFlag THEN
          MsgX.D(self.msgif, " args:  `" & args & "'");
        END;
        IF EvalOpt("-message", res) THEN
          IF Msg.dFlag THEN
            MsgX.D(self.msgif, " argument -message `" & res & "'");
          END;
          msg := res;
        END;
        IF msg = NIL AND EvalOpt("-msg", res) THEN
          IF Msg.dFlag THEN
            MsgX.D(self.msgif, " argument -msg `" & res & "'");
          END;
          msg := res;
        END;
        IF  msg = NIL AND EvalOpt("-m", res) THEN
          IF Msg.dFlag THEN
            MsgX.D(self.msgif, " argument -m `" & res & "'");
          END;
          msg := res;
        END;
        IF EvalOpt("-file", res) THEN
          IF Msg.dFlag THEN
            MsgX.D(self.msgif, " argument -file `" & res & "'");
          END;
          msgFile := APN.New(res);
        END;
        IF  msgFile = NIL AND EvalOpt("-f", res) THEN
          IF Msg.dFlag THEN
            MsgX.D(self.msgif, " argument -f `" & res & "'");
          END;
          msgFile := APN.New(res);
        END;
      END EvalOpts;

    BEGIN
      IF parameters.get("PKGVMOPT", opts) THEN
        EvalOpts();
      END;
      IF parameters.get("PKGMOPT", opts) THEN
        EvalOpts();
      END;
    END EvalMsgOptions;

  VAR
    done    := FALSE;
    tag     :  Tag.T;
    tagtext :  TEXT;
    vc, vcn :  PkgVC.T;
    msg     :  TEXT := NIL;
    msgFile :  APN.T := NIL;
  BEGIN
    TRY
      IF self.pkgvcAcc # NIL THEN
        vc := self.pkgVCIF(pkg);
        IF vc # NIL THEN
          IF    Text.Equal(action, "checkconflicts") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking package " & pkg &
                " for conflicts", level := 2);
            END;
            ret := Ret(vc.conflicts());
            res := vc.lastVCMsg;
            (* MsgX.T(self.msgif, res); *)
            done := TRUE;
          ELSIF Text.Equal(action, "checkmodified") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking package " & pkg &
                " for modifications", level := 2);
            END;
            ret := Ret(vc.modified());
            res := vc.lastVCMsg;
            (* MsgX.T(self.msgif, res); *)
            done := TRUE;
          ELSIF Text.Equal(action, "checkout") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking out package " & pkg, level := 2);
            END;
            VAR loc, tagtext, pkg : TEXT; pkgs : TextSeq.T; BEGIN
              IF parameters.get("TAG", tagtext) AND
                 parameters.get("LOCATION", loc) AND
                 parameters.get("PKG", pkg) THEN
                vcn := NEW(PkgVC.T).init(self.msgif);
                vcn.setEnvironment(vc.getEnvironment());
                TRY
                  res := "unknown checkout error";
                  pkgs := NEW(TextSeq.T).init();
                  pkgs.addhi(pkg);
                  VCUtils.CheckoutDirect(vcn, NIL, loc, tagtext, pkgs);
                  res := vcn.lastVCMsg;
                  (* MsgX.T(self.msgif, res); *)
                  ret := 0;
                EXCEPT
                  PkgVC.E(t) => ret := 1; res := vcn.lastVCMsg & "\n" & t;
                END;
                done := TRUE;
              ELSE
                RAISE Error("checkout missing one of TAG, LOCATION, PKG");
              END;
            END;
          ELSIF Text.Equal(action, "checkrelease") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking if package " & pkg &
                " is a release", level := 2);
            END;
            ret := Ret(vc.isRelease(tag));
            res := TagText(tag);
            done := TRUE;
          ELSIF Text.Equal(action, "checkuptodate") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking if package " & pkg &
                " is up-to-date", level := 2);
            END;
            ret := Ret(vc.upToDate());
            res := vc.lastVCMsg;
            (* MsgX.T(self.msgif, res); *)
            done := TRUE;
          ELSIF Text.Equal(action, "commitdevelmajor") THEN
            TRY
              EvalMsgOptions();
              vc.commitChanges(PkgVC.CommitType.Major, msg, msgFile);
              res := ""; (* commit will only leave the tag in lastVCMsg *)
              ret := 0;
            EXCEPT
              PkgVC.E(t) => res := vc.lastVCMsg & "\n" & t; ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "commitdevelminor") THEN
            TRY
              EvalMsgOptions();
              vc.commitChanges(PkgVC.CommitType.Minor, msg, msgFile);
              res := ""; (* commit will only leave the tag in lastVCMsg *)
              ret := 0;
            EXCEPT
              PkgVC.E(t) => res := vc.lastVCMsg & "\n" & t; ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "commitdevelpatch") THEN
            TRY
              EvalMsgOptions();
              vc.commitChanges(PkgVC.CommitType.Patch, msg, msgFile);
              res := ""; (* commit will only leave the tag in lastVCMsg *)
              ret := 0;
            EXCEPT
              PkgVC.E(t) => res := vc.lastVCMsg & "\n" & t; ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "commitreleasemajor") THEN
            TRY
              EvalMsgOptions();
              vc.commitRelease(PkgVC.CommitType.Major, msg, msgFile);
              res := ""; (* commit will only leave the tag in lastVCMsg *)
              ret := 0;
            EXCEPT
              PkgVC.E(t) => res := vc.lastVCMsg & "\n" & t; ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "commitreleaseminor") THEN
            TRY
              EvalMsgOptions();
              vc.commitRelease(PkgVC.CommitType.Minor, msg, msgFile);
              res := ""; (* commit will only leave the tag in lastVCMsg *)
              ret := 0;
            EXCEPT
              PkgVC.E(t) => res := vc.lastVCMsg & "\n" & t; ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "commitreleasepatch") THEN
            TRY
              EvalMsgOptions();
              vc.commitRelease(PkgVC.CommitType.Patch, msg, msgFile);
              res := ""; (* commit will only leave the tag in lastVCMsg *)
              ret := 0;
            EXCEPT
              PkgVC.E(t) => res := vc.lastVCMsg & "\n" & t; ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "conflicts") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking package " & pkg &
                " for conflicts", level := 2);
            END;
            ret := Ret(vc.conflicts());
            res := "";
            done := TRUE;
          ELSIF Text.Equal(action, "currentdeveltag") THEN
            tag := vc.currentDevelopmentTag(); ret := 0;
            res := TagText(tag);
            done := TRUE;
          ELSIF Text.Equal(action, "currentlabel") THEN
          ELSIF Text.Equal(action, "currentreleasetag") THEN
            tag := vc.currentReleaseTag(); ret := 0;
            res := TagText(tag);
            done := TRUE;
          ELSIF Text.Equal(action, "currenttag") THEN
            tag := vc.currentLocalTag(); ret := 0;
            res := TagText(tag);
            done := TRUE;
          ELSIF Text.Equal(action, "getlabel") THEN
          ELSIF Text.Equal(action, "isrelease") THEN
            ret := Ret(vc.isRelease(tag));
            res := "";
            done := TRUE;
          ELSIF Text.Equal(action, "listlabels") THEN
          ELSIF Text.Equal(action, "modified") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking package " & pkg &
                " for modifications", level := 2);
            END;
            ret := Ret(vc.modified());
            res := "";
            done := TRUE;
          ELSIF Text.Equal(action, "setlabel") THEN
          ELSIF Text.Equal(action, "update") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "updating package " & pkg, level := 2);
            END;
            IF parameters.get("TAG", tagtext) THEN
              tag := Tag.New(tagtext);
              IF VCUtils.TagExists(vc, tag) THEN
                TRY
                  vc.update(tag);
                  res := vc.lastVCMsg;
                  (* MsgX.T(self.msgif, res); *)
                  ret := 0;
                EXCEPT
                  PkgVC.E(t) => res := t; ret := 1;
                END;
              ELSE
                res := "tag does not exist: " & tag.originalText();
                ret := 1;
              END;
            ELSE
              res := "no tag defined for update";
              ret := 1;
            END;
            done := TRUE;
          ELSIF Text.Equal(action, "uptodate") THEN
            IF Msg.vFlag THEN
              MsgX.V(self.msgif, "checking if package " & pkg &
                " is up-to-date", level := 2);
            END;
            ret := Ret(vc.upToDate());
            res := "";
            done := TRUE;
          ELSIF Text.Equal(action, "diff") THEN
            (* FIXME: provide internal implementation for diff action *)
          ELSIF Text.Equal(action, "diff1") THEN
            (* FIXME: provide internal implementation for diff1 action *)
          ELSIF Text.Equal(action, "diff2") THEN
            (* FIXME: provide internal implementation for diff2 action *)
          END;
          IF done THEN
            UpdateStateCache(self, self.pkgPath(pkg), action, ret);
            RETURN TRUE;
          END;
        END;
      END;
    EXCEPT
      PkgVC.E(m) => RAISE Error("version control backend failed: " & m);
    END;
    RETURN FALSE;
  END ResultFromInternalVersionControl;
---------------------------------------------------------------------------
PROCEDURE ExecAction(self : T; pkg : PkgBase.Name; action : PkgBase.Action;
                     VAR res  :  TEXT;
                     externalShell : TEXT := NIL;
                     parameters : TextTextTbl.T := NIL) : INTEGER
  RAISES {Error} =
    (*
      Execute the action associated with `action' in the
      package root directory of `pkg' and return the exit code of
      the process (shell).
    *)
  VAR
    ret  :  INTEGER;
    cmd  :  PkgBase.CmdSeq;
    nwd  :  Pathname.T;
    kind :  PkgBase.Kind;
    errt :  TEXT := "";
    done := FALSE;
  BEGIN
    IF NOT self.exists(pkg) THEN
      RAISE Error("package " & pkg & " does not exist");
    END;

    nwd  := self.pkgPath(pkg);
    kind := self.pkgType(pkg);

    IF self.useCache AND
       NOT ActionProbablyNeeded(self, pkg, nwd, action, ret, res) THEN
      IF Msg.vFlag THEN
        MsgX.V(self.msgif, "omitting action " & action &
          " based on cached state", level := 2);
      END;
      RETURN ret;
    END;

    IF self.useCache AND
      (Text.Equal(action, "modified") OR Text.Equal(action, "conflicts") OR
       Text.Equal(action, "uptodate") OR Text.Equal(action, "release")) THEN
      TRY
        WITH res = self.getAndCacheVersionState(pkg) DO
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "--- status package " &
              TextUtils.Compress(res));
          END;
        END;
        IF Text.Equal(action, "modified") THEN
          IF self.attrIsSet(pkg, Checkpoint.Attr.Modified) THEN
            ret := 0;
          ELSE
            ret := 1;
          END;
        ELSIF Text.Equal(action, "conflicts") THEN
          IF self.attrIsSet(pkg, Checkpoint.Attr.Conflicts) THEN
            ret := 0;
          ELSE
            ret := 1;
          END;
        ELSIF Text.Equal(action, "uptodate") THEN
          IF self.attrIsSet(pkg, Checkpoint.Attr.UpToDate) THEN
            ret := 0;
          ELSE
            ret := 1;
          END;
        ELSIF Text.Equal(action, "release") THEN
          IF self.attrIsSet(pkg, Checkpoint.Attr.IsRelease) THEN
            ret := 0;
          ELSE
            ret := 1;
          END;
        END;
        done := TRUE;
      EXCEPT
      END;
    END;
    IF done THEN
      RETURN ret;
    END;
    IF ResultFromInternalVersionControl(self, pkg, action, parameters,
                                        res, ret) THEN
      RETURN ret;
    END;
    res := "external command failure";
    TRY
      TRY
	cmd := self.cfg.getAction(kind, action);
        IF cmd = NIL THEN
          RAISE Error("no commands for action " & action);
        END;
        TRY
          cmd := TextUtils.SubstituteVariables(cmd, parameters);
        EXCEPT
          TextUtils.Error(e) => RAISE Error("parameter error in " & cmd &
            ": " & e);
        END;
	IF externalShell = NIL THEN
          externalShell := self.cfg.getAction(kind, "externalshell");
        END;
        MsgX.T(self.msgif, "[" & nwd & "] " & cmd);
        IF NOT Text.Empty(cmd) AND NOT Text.Equal(cmd, "-") THEN
          IF externalShell = NIL THEN
            errt := " failed";
            ret := System.ExecuteList(cmd, msgif := self.msgif, wd := nwd);
          ELSE
            errt := " via shell " & externalShell & " failed";
            ret := System.ExecuteShell(cmd, externalShell, msgif := self.msgif,
                                       wd := nwd);
          END;
          (* caller is expected to print exit code when appropriate *)
          res := "";
        ELSE
          ret := 0;
        END;
        UpdateStateCache(self, nwd, action, ret);
      EXCEPT
	System.ExecuteError => RAISE Error("execution of " & cmd & errt);
      | Thread.Alerted      => RAISE Error("execution of " & cmd &
        "interrupted");
      | Error(e)            => RAISE Error(e);
      END;
    FINALLY
      (* skip *)
    END;
    RETURN ret;
  END ExecAction;
---------------------------------------------------------------------------
PROCEDURE ExecCmdList(self : T; pkg : PkgBase.Name; cmd : TEXT;
                      externalShell : TEXT := NIL) : INTEGER RAISES {Error} =
  VAR
    ret  :  INTEGER;
    nwd  :  Pathname.T;
    kind := self.pkgType(pkg);
  BEGIN
    IF NOT self.exists(pkg) THEN
      RAISE Error("package " & pkg & " does not exist");
    END;

    nwd := self.pkgPath(pkg);

    TRY
      TRY
	IF externalShell = NIL THEN
          externalShell := self.cfg.getAction(kind, "externalshell");
        END;
        MsgX.T(self.msgif, "[" & nwd & "] " & cmd);
	IF externalShell = NIL THEN
	  ret := System.ExecuteList(cmd, msgif := self.msgif, wd := nwd);
	ELSE
	  ret := System.ExecuteShell(cmd, externalShell, msgif := self.msgif,
                                     wd := nwd);
	END;
        UpdateStateCache(self, nwd, "any-user-cmd", ret);
      EXCEPT
	System.ExecuteError => RAISE Error("execution of " & cmd & " failed");
      | Thread.Alerted      => RAISE Error("execution of " & cmd &
	"interrupted");
      END;
    FINALLY
      (* skip *)
    END;
    RETURN ret;
  END ExecCmdList;
---------------------------------------------------------------------------
PROCEDURE GetCmdOutput(self : T; pkg : PkgBase.Name;
                       cmd : TEXT; VAR ret : INTEGER) : TEXT RAISES {Error} =
  VAR
    proc :  Process.T;
    res  :  TEXT := NIL;
    rd   :  Rd.T;
    nwd  :  Pathname.T;
  BEGIN
    IF NOT self.exists(pkg) THEN
      RAISE Error("package " & pkg & " does not exist");
    END;

    nwd := self.pkgPath(pkg);

    TRY
      TRY
        MsgX.T(self.msgif, "[" & nwd & "] " & cmd);
        proc := System.RdExecute(cmd, rd, nwd, msgif := self.msgif);
        res  := Rd.GetText(rd, LAST(INTEGER));
        ret := Process.Wait(proc);
        UpdateStateCache(self, nwd, "any-user-cmd", ret);
      EXCEPT
        Rd.Failure  => RAISE Error("error reading from command " & cmd);
      | Thread.Alerted  => RAISE Error("execution of " & cmd & "interrupted");
      | System.ExecuteError => RAISE Error("execution of " & cmd & " failed");
      END;
    FINALLY
      (* skip *)
    END;
    RETURN res;
  END GetCmdOutput;
---------------------------------------------------------------------------
PROCEDURE GetAndCacheVersionState(self : T; pkg : PkgBase.Name) : TEXT
  RAISES {Error} =
  VAR
    ret  :  INTEGER;
    res  :  TEXT;
    val  :  TEXT;
    cmd  :  TEXT;
    kind := self.pkgType(pkg);
    dir  := self.pkgPath(pkg);
    seq  :  TextSeq.T;
    vc   :  PkgVC.T;
    tag  :  Tag.T;
  BEGIN
    IF NOT self.useCache THEN RETURN NIL END;
    IF self.pkgvcAcc # NIL THEN
      vc := self.pkgVCIF(pkg);
      TRY
        IF vc # NIL THEN
          IF Msg.vFlag THEN
            MsgX.V(self.msgif, "getting short status for package " & pkg,
                   level := 2);
          END;
          IF vc.modified() THEN
            UpdateStateCache(self, dir, "modified", 0, FALSE);
            res := pkg & ": modified";
          ELSE
            UpdateStateCache(self, dir, "modified", 1, FALSE);
            res := pkg & ":";
          END;
          IF vc.upToDate() THEN
            UpdateStateCache(self, dir, "uptodate", 0, FALSE);
            res := res & " up-to-date";
          ELSE
            UpdateStateCache(self, dir, "uptodate", 1, FALSE);
          END;
          IF vc.conflicts() THEN
            UpdateStateCache(self, dir, "conflicts", 0, FALSE);
            res := res & " conflicts";
          ELSE
            UpdateStateCache(self, dir, "conflicts", 1, FALSE);
          END;
          TRY
            IF vc.isRelease(tag) THEN
              UpdateStateCache(self, dir, "isrelease", 0, FALSE);
              self.stateCache.setVal(dir, "release-tag", tag.denotation());
            ELSE
              UpdateStateCache(self, dir, "isrelease", 1, FALSE);
              self.stateCache.delVal(dir, "release-tag");
            END;
            IF vc.isSticky(tag) THEN
              self.stateCache.setVal(dir, "sticky-tag", tag.denotation());
            ELSE
              self.stateCache.delVal(dir, "sticky-tag");
            END;
            tag := vc.currentLocalTag();
            self.stateCache.setVal(dir, "current-tag", tag.denotation());
          EXCEPT
            Checkpoint.Error(e) =>
            RAISE Error("checkpoint error for " & pkg & ": " & e);
          END;
          RETURN res;
        END;
      EXCEPT
        PkgVC.E(m) => RAISE Error("version control backend failed: " & m);
      END;
    END;
    cmd := self.cfg.getAction(kind, "shortstatus");
    IF cmd = NIL THEN
      cmd := "pkgvm -sstat";
    END;
    res := GetCmdOutput(self, pkg, cmd, ret);
    IF ret # 0 THEN
      RAISE Error("command `" & cmd & "' failed in package " & pkg &
            " with status " & Fmt.Int(ret));
    END;
    seq := TextUtils.Split(res, " ");
    IF TextUtils.MemberOfTextSeq(seq, "modified") THEN
      UpdateStateCache(self, dir, "modified", 0, FALSE);
    ELSE
      UpdateStateCache(self, dir, "modified", 1, FALSE);
    END;
    IF TextUtils.MemberOfTextSeq(seq, "up-to-date") THEN
      UpdateStateCache(self, dir, "uptodate", 0, FALSE);
    ELSE
      UpdateStateCache(self, dir, "uptodate", 1, FALSE);
    END;
    IF TextUtils.MemberOfTextSeq(seq, "conflicts") THEN
      UpdateStateCache(self, dir, "conflicts", 0, FALSE);
    ELSE
      UpdateStateCache(self, dir, "conflicts", 1, FALSE);
    END;
    TRY
      IF MatchesTextSeq(seq, "^release:", val) THEN
        UpdateStateCache(self, dir, "isrelease", 0, FALSE);
        self.stateCache.setVal(dir, "release-tag", TextAfterChar(val, ':'));
      ELSE
        UpdateStateCache(self, dir, "isrelease", 1, FALSE);
        self.stateCache.delVal(dir, "release-tag");
      END;
      IF MatchesTextSeq(seq, "^current:", val) THEN
        self.stateCache.setVal(dir, "current-tag", TextAfterChar(val, ':'));
      ELSE
        self.stateCache.delVal(dir, "current-tag");
      END;
      IF MatchesTextSeq(seq, "^sticky:", val) THEN
        self.stateCache.setVal(dir, "sticky-tag", TextAfterChar(val, ':'));
      ELSE
        self.stateCache.delVal(dir, "sticky-tag");
      END;
    EXCEPT
      Checkpoint.Error(e) =>
      RAISE Error("checkpoint error for " & pkg & ": " & e);
    END;
    RETURN res;
  END GetAndCacheVersionState;
---------------------------------------------------------------------------
PROCEDURE MatchesTextSeq(ts : TextSeq.T; pattern : TEXT;
                         VAR res : TEXT) : BOOLEAN =
  VAR pat : RegEx.Pattern;
  BEGIN
    TRY pat := RegEx.Compile(pattern); EXCEPT ELSE END;
    FOR i := 0 TO ts.size() - 1 DO
      WITH elem = ts.get(i) DO
        IF RegEx.Execute(pat, elem) > -1 THEN
          res := elem;
          RETURN TRUE;
        END;
      END;
    END;
    RETURN FALSE;
  END MatchesTextSeq;
---------------------------------------------------------------------------
PROCEDURE TextAfterChar(t : TEXT; c : CHAR) : TEXT =
  VAR i := Text.FindChar(t, c);
  BEGIN
    IF i < 0 THEN
      RETURN "";
    END;
    RETURN Text.Sub(t, i + 1);
  END TextAfterChar;
---------------------------------------------------------------------------
PROCEDURE FileContents(self : T; pkg : PkgBase.Name; fn : TEXT) : TEXT
  RAISES {Error} =
  VAR
    path : Pathname.T;
    rd   : Rd.T;
    res  : TEXT;
  BEGIN
    IF NOT self.exists(pkg) THEN
      RAISE Error("package " & pkg & " does not exist");
    END;
    IF Pathname.Absolute(PathRepr.Native(fn)) THEN
      RAISE Error("pathname " & fn & " must not be absolute");
    END;
    path := Pathname.Join(self.pkgPath(pkg), PathRepr.Native(fn), NIL);
    TRY
      rd := FileRd.Open(path);
    EXCEPT ELSE
      RAISE Error("cannot open file " & fn);
    END;
    TRY
      TRY
        res := Rd.GetText(rd, LAST(CARDINAL));
      EXCEPT ELSE
        RAISE Error("cannot read file " & fn);
      END;
    FINALLY
      TRY Rd.Close(rd) EXCEPT ELSE END;
    END;
    RETURN res;
  END FileContents;
---------------------------------------------------------------------------
PROCEDURE Checkout(self : T; pkg : PkgBase.Name;
                   checkoutCmd : PkgBase.Action;
                   externalShell : TEXT := NIL;
                   rootDir : Pathname.T := NIL;
                   parameters : TextTextTbl.T := NIL) : INTEGER
  RAISES {Error} =
  VAR
    ret  :  INTEGER;
    loc  :  TEXT;
    cmd  :  PkgBase.CmdSeq;
    kind := self.pkgType(pkg);
  BEGIN
    IF self.exists(pkg) THEN
      RAISE Error("package " & pkg & " does already exist");
    END;

    TRY
      TRY
	cmd := self.cfg.getAction(self.pkgType(pkg), checkoutCmd);
	IF cmd = NIL THEN (* undefined package type *)
	  cmd := self.cfg.getAction("DEFAULT", checkoutCmd);
	END;
        TRY
          cmd := TextUtils.SubstituteVariables(cmd, parameters);
        EXCEPT
          TextUtils.Error(e) => RAISE Error("parameter error in " & cmd &
            ": " & e);
        END;
	IF externalShell = NIL THEN
          externalShell := self.cfg.getAction(kind, "externalshell");
        END;
        MsgX.T(self.msgif, "[" & rootDir & "] " & cmd);
	IF externalShell = NIL THEN
	  ret := System.ExecuteList(cmd, msgif := self.msgif, wd := rootDir);
	ELSE
	  ret := System.ExecuteShell(cmd, externalShell, msgif := self.msgif,
                                     wd := rootDir);
	END;
        IF parameters # NIL AND parameters.get("LOCATION", loc) THEN
          rootDir := Pathname.Join(rootDir, loc, NIL);
        ELSIF self.location.get(pkg, loc) THEN
          IF NOT Text.Equal(loc, Undefined) THEN
            rootDir := Pathname.Join(rootDir, loc, NIL);
          END;
        END;
        EVAL self.location.delete(pkg, loc);
        loc := Pathname.Join(rootDir, pkg, NIL);
        EVAL self.exists(pkg, rootDir);
        UpdateStateCache(self, loc, checkoutCmd, ret);
      EXCEPT
	System.ExecuteError => RAISE Error("execution of " & cmd & " failed");
      | Thread.Alerted      => RAISE Error("execution of " & cmd &
	"interrupted");
      END;
    FINALLY
      (* skip *)
    END;
    RETURN ret;
  END Checkout;
---------------------------------------------------------------------------
PROCEDURE GetFileCache(self : T) : FileInfo.T =
  BEGIN
    RETURN self.fileCache;
  END GetFileCache;
---------------------------------------------------------------------------
PROCEDURE NewCheckpoint(self : T; update := FALSE) : Checkpoint.T
  RAISES {Error} =
  VAR
    cp   := Checkpoint.New(self.fileCache, self.msgif);
    iter := self.location.iterate();
    pkg  :  TEXT;
    path :  TEXT;
  BEGIN
    TRY
      WHILE iter.next(pkg, path) DO
        IF NOT Text.Equal(path, Undefined) THEN
          cp.addDir(path);
        END;
      END;
      IF update THEN
        IF self.verboseCache AND NOT Msg.vFlag THEN
          MsgX.T(self.msgif, "scanning all packages (new checkpoint)...");
        END;
        cp.update();
      END;
    EXCEPT
      Checkpoint.Error(e) => RAISE Error("NewCheckpoint: " & e);
    END;
    RETURN cp;
  END NewCheckpoint;
---------------------------------------------------------------------------
PROCEDURE ReplaceStateCache(self : T; sc : Checkpoint.T) =
  BEGIN
    self.stateCache := sc;
  END ReplaceStateCache;
---------------------------------------------------------------------------
PROCEDURE CachedState(self : T) : Checkpoint.T =
  BEGIN
    RETURN self.stateCache;
  END CachedState;
---------------------------------------------------------------------------
PROCEDURE UpdateCache(self : T) RAISES {Error} =
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    TRY
      IF self.verboseCache AND NOT Msg.vFlag THEN
        MsgX.T(self.msgif, "scanning all packages (cache update)...");
      END;
      self.stateCache.update();
    EXCEPT
      Checkpoint.Error(e) => RAISE Error(e);
    END;
  END UpdateCache;
---------------------------------------------------------------------------
PROCEDURE DumpStateCache(self : T; header : TEXT) =
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    MsgX.T(self.msgif, header);
    TRY MsgX.T(self.msgif, self.stateCache.toText()); EXCEPT ELSE END;
  END DumpStateCache;
---------------------------------------------------------------------------
PROCEDURE SetAttr(self : T; pkg : PkgBase.Name; attr : Checkpoint.Attr)
  RAISES {Error} =
  VAR
    path  : TEXT;
    attrs : Checkpoint.AttrSet;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    IF self.location.get(pkg, path) THEN
      TRY
        attrs := self.stateCache.getAttr(path) + Checkpoint.AttrSet{attr};
        self.stateCache.setAttr(path, attrs);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot set attributes for package " & pkg & ": " & e);
      END;
    ELSE
      RAISE Error("SetAttr: no location for package " & pkg);
    END;
  END SetAttr;
---------------------------------------------------------------------------
PROCEDURE ClearAttr(self : T; pkg : PkgBase.Name; attr : Checkpoint.Attr)
  RAISES {Error} =
  VAR
    path  : TEXT;
    attrs : Checkpoint.AttrSet;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    IF self.location.get(pkg, path) THEN
      TRY
        attrs := self.stateCache.getAttr(path) - Checkpoint.AttrSet{attr};
        self.stateCache.setAttr(path, attrs);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot set attributes for package " & pkg & ": " & e);
      END;
    ELSE
      RAISE Error("SetAttr: no location for package " & pkg);
    END;
  END ClearAttr;
---------------------------------------------------------------------------
PROCEDURE AttrIsSet(self : T; pkg : PkgBase.Name; attr : Checkpoint.Attr)
  : BOOLEAN RAISES {Error} =
  VAR
    path  : TEXT;
    attrs : Checkpoint.AttrSet;
  BEGIN
    IF NOT self.useCache THEN RETURN FALSE END;
    IF self.location.get(pkg, path) THEN
      TRY
        attrs := self.stateCache.getAttr(path);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot get attributes for package " & pkg & ": " & e);
      END;
      RETURN attr IN attrs;
    ELSE
      RAISE Error("AttrIsSet: no location for package " & pkg);
    END;
  END AttrIsSet;
---------------------------------------------------------------------------
PROCEDURE SetVal(self : T; pkg : PkgBase.Name; name, val : TEXT)
  RAISES {Error} =
  VAR
    path  : TEXT;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    IF self.location.get(pkg, path) THEN
      TRY
        self.stateCache.setVal(path, name, val);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot set value for package " & pkg & ": " & e);
      END;
    ELSE
      RAISE Error("SetVal: no location for package " & pkg);
    END;
  END SetVal;
---------------------------------------------------------------------------
PROCEDURE GetVal(self : T; pkg : PkgBase.Name; name : TEXT) : TEXT
  RAISES {Error} =
  VAR
    path  : TEXT;
  BEGIN
    IF NOT self.useCache THEN RETURN NIL END;
    IF self.location.get(pkg, path) THEN
      TRY
        RETURN self.stateCache.getVal(path, name);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot get value for package " & pkg & ": " & e);
      END;
    ELSE
      RAISE Error("GetVal: no location for package " & pkg);
    END;
  END GetVal;
---------------------------------------------------------------------------
PROCEDURE DelVal(self : T; pkg : PkgBase.Name; name : TEXT) RAISES {Error} =
  VAR
    path  : TEXT;
  BEGIN
    IF NOT self.useCache THEN RETURN END;
    IF self.location.get(pkg, path) THEN
      TRY
        self.stateCache.delVal(path, name);
      EXCEPT
        Checkpoint.Error(e) =>
        RAISE Error("cannot delete value for package " & pkg & ": " & e);
      END;
    ELSE
      RAISE Error("GetVal: no location for package " & pkg);
    END;
  END DelVal;
---------------------------------------------------------------------------
BEGIN (* PoolSet MAIN *)
END PoolSet.

interface ASCII is in:


interface TextUtils is in:


interface FileInfo is in:


interface RegEx is in: