[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[vidalia-svn] r3645: Add wixtool for manipulating Windows Installer XML files. (in vidalia/trunk/src/tools: . wixtool)



Author: coderman
Date: 2009-03-28 18:27:44 -0400 (Sat, 28 Mar 2009)
New Revision: 3645

Added:
   vidalia/trunk/src/tools/wixtool/
   vidalia/trunk/src/tools/wixtool/CMakeLists.txt
   vidalia/trunk/src/tools/wixtool/wixtool.cpp
Modified:
   vidalia/trunk/src/tools/CMakeLists.txt
Log:
Add wixtool for manipulating Windows Installer XML files.

Modified: vidalia/trunk/src/tools/CMakeLists.txt
===================================================================
--- vidalia/trunk/src/tools/CMakeLists.txt	2009-03-24 19:46:03 UTC (rev 3644)
+++ vidalia/trunk/src/tools/CMakeLists.txt	2009-03-28 22:27:44 UTC (rev 3645)
@@ -17,5 +17,6 @@
   add_subdirectory(po2nsh)
   add_subdirectory(nsh2po EXCLUDE_FROM_ALL)
   add_subdirectory(po2wxl)
+  add_subdirectory(wixtool)
 endif(WIN32)
 

Added: vidalia/trunk/src/tools/wixtool/CMakeLists.txt
===================================================================
--- vidalia/trunk/src/tools/wixtool/CMakeLists.txt	                        (rev 0)
+++ vidalia/trunk/src/tools/wixtool/CMakeLists.txt	2009-03-28 22:27:44 UTC (rev 3645)
@@ -0,0 +1,24 @@
+##
+##  $Id$
+## 
+##  Copyright (C) 2009  The Tor Project, Inc.
+##  See LICENSE file for terms; may be used according
+##  Vidalia or Tor license constraints. (dual license)
+##
+
+## wixtool source files
+set(wixtool_SRCS
+  wixtool.cpp
+)
+
+## Create the wixtool executable
+add_executable(wixtool ${wixtool_SRCS})
+
+## Link the executable with the appropriate Qt libraries
+target_link_libraries(wixtool ${QT_LIBRARIES})
+
+## Remember location so we can use it in custom commands
+get_target_property(WIXTOOL_EXECUTABLE wixtool LOCATION)
+set(VIDALIA_WIXTOOL_EXECUTABLE ${WIXTOOL_EXECUTABLE}
+    CACHE STRING "Location of Vidalia's wixtool program." FORCE)
+

Added: vidalia/trunk/src/tools/wixtool/wixtool.cpp
===================================================================
--- vidalia/trunk/src/tools/wixtool/wixtool.cpp	                        (rev 0)
+++ vidalia/trunk/src/tools/wixtool/wixtool.cpp	2009-03-28 22:27:44 UTC (rev 3645)
@@ -0,0 +1,710 @@
+/*
+**  $Id$
+**
+**  Copyright (C) 2009  The Tor Project, Inc.
+**  See LICENSE file for terms; may be used according
+**  Vidalia or Tor license constraints. (dual license)
+*/
+
+#include <QFile>
+#include <QDomDocument>
+#include <QTextStream>
+#include <QTextCodec>
+#include <QStringList>
+#include <stdlib.h>
+
+#define WIX_ATTR_ID  "Id"
+#define WIX_ATTR_DIRACTION "uninstall"
+#define WIX_ATTR_REGACTION "createAndRemoveOnUninstall"
+#define WIX_ATTR_VALUE "Value"
+#define WIX_ATTR_KEY "KeyPath"
+#define WIX_ATTR_GUID "Guid"
+#define WIX_ATTR_NAME "Name"
+#define WIX_ATTR_REG_TYPE "Type"
+#define WIX_ATTR_REG_NAME "Name"
+#define WIX_ATTR_REG_ROOT "Root"
+#define WIX_ATTR_REG_KEYPATH  "Key"
+#define WIX_ATTR_REG_ACTION  "Action"
+#define WIX_REG_KEY_TYPE "integer"
+#define WIX_TAG_FILE "File"
+#define WIX_TAG_DIR "Directory"
+#define WIX_TAG_FEATURE "Feature"
+#define WIX_TAG_COMPONENT "Component"
+#define WIX_TAG_COMPONENT_REF "ComponentRef"
+#define WIX_TAG_CREATEDIR "CreateFolder"
+#define WIX_TAG_REMOVEDIR "RemoveFolder"
+#define WIX_TAG_REGKEY "RegistryKey"
+#define WIX_TAG_REGVAL "RegistryValue"
+
+typedef void (*TraverseCallback)(void *cbdata, QDomElement e);
+
+/* Splice command takes an element or sub tree from one
+ * document and inserts it into another. This is useful for
+ * expanding placeholder elements with their desired content
+ * for example.
+ * If an element name is not unique the conventional WiX Id
+ * attribute can be used to identify the specific element.
+ */
+typedef struct s_SpliceData {
+  QString      dtag;
+  QString      did;
+  QDomElement  splice;
+} SpliceData;
+
+/* Replace operates on tags by name or Id like Splice but
+ * only makes modifications to individual elements. Replace
+ * can also remove elements. (replace with null)
+ */
+typedef struct s_ReplaceData {
+  QString  dtag;
+  QString  did;
+  QString  dprop;
+  QString  newtag;
+  QString  newprop;
+  QString  newpropval;
+} ReplaceData;
+
+/* Add operates on tags by name or Id as usual.
+ */
+typedef struct s_AddData {
+  QString  dtag;
+  QString  did;
+  QString  newtag;
+  QString  newprop;
+  QString  newpropval;
+} AddData;
+
+/* In order to support local per user installation some basic
+ * constrains must apply to every component included in a
+ * package. This includes using a key path for each component
+ * via registry keys and placing all application data under the
+ * local user profile folder.
+ * This utility will navigate the components and convert any
+ * keys to registry key paths and create folders in the deployment
+ * hierarchy as required.
+ */
+typedef struct s_UserLocalData {
+  QString      keypath;
+  QString      featureid;
+  QStringList  newcomps;
+} UserLocalData;
+
+
+/* Note that we must walk the tree ourselves as locate by ID
+ * nor suitable select by classification is available in the
+ * Qt API.
+ */
+bool
+do_walkdoc(QDomNode  n,
+  TraverseCallback  cb,
+  void *            cbdata,
+  QString *errorMessage)
+{
+  QTextStream error(stderr);
+  if ( !n.isNull() ) {
+    if ( n.isElement() ) {
+      QDomElement e = n.toElement();
+      (*cb)(cbdata, e);
+    }
+    if ( n.hasChildNodes() ) {
+      QDomNodeList subnodes = n.childNodes();
+      int i = 0;
+      while (i < subnodes.count()) {
+        do_walkdoc(subnodes.item(i++), cb, cbdata, errorMessage);
+      }
+    }
+  }
+  return true;
+}
+
+bool
+walkdoc(QDomDocument *doc,
+  TraverseCallback  cb,
+  void *            cbdata,
+  QString *errorMessage)
+{
+  QTextStream error(stderr);
+  QDomNode n = doc->documentElement();
+  do_walkdoc(n, cb, cbdata, errorMessage);
+  return true;
+}
+
+void 
+splicefunc(void *cbdata,
+ QDomElement e)
+{
+  SpliceData *d = reinterpret_cast<SpliceData *>(cbdata);
+  QString eid = e.attribute(WIX_ATTR_ID);
+
+  if (e.tagName().compare(d->dtag) == 0) {
+    /* if a specific Id is set, verify it too. */
+    if (d->did.isEmpty() ||
+        (eid.size() && !eid.compare(d->did)) ) {
+
+      /* expected behavior is to graft children of the splice under target.
+       * if we're only given a single element graft it instead.
+       */
+      if (d->splice.hasChildNodes()) {
+        QDomNodeList subnodes = d->splice.childNodes();
+        int i = 0;
+        while (i < subnodes.count()) {
+          e.appendChild(e.ownerDocument().importNode(subnodes.item(i++), true));
+        }
+      }
+      else {
+        e.appendChild(e.ownerDocument().importNode(d->splice, true));
+      }
+    }
+  }
+}
+
+/** Make modifications to requested documents.
+ * returns false on error and <b>errorMessage</b> will be set.
+ */
+bool
+docsplice(QDomDocument *doc,
+  QString arguments,
+  QString *errorMessage)
+{
+  Q_ASSERT(doc);
+  Q_ASSERT(errorMessage);
+  SpliceData  cbdata;
+
+  QStringList spliceinfo = arguments.split("=");
+  if (spliceinfo.count() != 2) {
+    *errorMessage = "Invalid argument for splice command: " + arguments;
+    return false;
+  }
+  if (spliceinfo[0].contains(':')) {
+    /* Id syntax */
+    QStringList destinfo = spliceinfo[0].split(":");
+    cbdata.dtag = destinfo[0];
+    cbdata.did  = destinfo[1];
+  }
+  else {
+    cbdata.dtag = spliceinfo[0];
+  }
+
+  QStringList  srcinfo = spliceinfo[1].split(":");
+  if (srcinfo.count() < 2) {
+    *errorMessage = "Invalid source argument for splice command: " + arguments;
+    return false;
+  }
+  QFile spliceFile(srcinfo[0]);
+  if (!spliceFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+    *errorMessage = QString("Unable to open '%1' for reading: %2\n")
+                     .arg(srcinfo[0]).arg(spliceFile.errorString());
+    return false;
+  }
+  QTextStream sfiletxt(&spliceFile);
+  QDomDocument sdoc;
+  QString parseError;
+  int  badline, badcol;
+  if (!sdoc.setContent (sfiletxt.readAll(), false, &parseError, &badline, &badcol)) {
+    *errorMessage = QString("Error parsing splice document '%1' at line %2 and column %3: %4")
+                    .arg(srcinfo[0]).arg(badline).arg(badcol).arg(parseError);
+    return false;
+  }
+
+  QDomNodeList elist = sdoc.elementsByTagName(srcinfo[1]);
+  if (elist.count() == 0) {
+    *errorMessage = QString("Unable to locate splice element '%1' in document.\n").arg(srcinfo[1]);
+    return false;
+  }
+  if (srcinfo.count() == 3) {
+    /* Id syntax for source elem */
+    for (int i=0; i < elist.count(); i++) {
+      QString eid = elist.item(i).toElement().attribute(WIX_ATTR_ID);
+      if (eid.size() && !eid.compare(srcinfo[2])) {
+        cbdata.splice = elist.item(i).toElement();
+      }
+    }
+  }
+  else {
+    /* without an Id the tag name should be unique. */
+    cbdata.splice = elist.item(0).toElement();
+  }
+  return walkdoc(doc, &splicefunc, &cbdata, errorMessage);
+}
+
+void 
+replacefunc(void *cbdata,
+ QDomElement e)
+{
+  ReplaceData *d = reinterpret_cast<ReplaceData *>(cbdata);
+  QString eid = e.attribute(WIX_ATTR_ID);
+
+  if (e.tagName().compare(d->dtag) == 0) {
+    /* if a specific Id is set, verify it too. */
+    if (d->did.isEmpty() ||
+        (eid.size() && !eid.compare(d->did)) ) {
+
+      /* no destination means remove node from tree */
+      if (d->newtag.isNull()) {
+        QDomNode parent = e.parentNode();
+        parent.removeChild(e);
+      }
+      else {
+        if (d->newtag.compare(e.tagName())) {
+          e.setTagName (d->newtag);
+        }
+        if (d->newprop.isNull()) {
+          /* clear all attributes (except Id if present) */
+          QDomNamedNodeMap attrs = e.attributes();
+          for (int i = 0; i < attrs.count(); i++) {
+            if (attrs.item(i).nodeName().compare(WIX_ATTR_ID)) {
+              e.removeAttribute(attrs.item(i).nodeName());
+            }
+          }
+        }
+        else {
+          /* only modify / clear a specific property */
+          QDomNode prop = e.attributeNode(d->newprop);
+          if (!prop.isNull()) {
+            e.setAttribute(d->newprop, d->newpropval);
+          }
+        }
+      }
+    }
+  }
+}
+
+/** Make modifications to requested documents.
+ * returns false on error and <b>errorMessage</b> will be set.
+ */
+bool
+docreplace(QDomDocument *doc,
+  QString arguments,
+  QString *errorMessage)
+{
+  Q_ASSERT(doc);
+  Q_ASSERT(errorMessage);
+  ReplaceData  cbdata;
+
+  QStringList replaceinfo = arguments.split("=");
+  if (replaceinfo.count() < 1) {
+    *errorMessage = "Invalid argument for replace command: " + arguments;
+    return false;
+  }
+  if (replaceinfo[0].contains(':')) {
+    /* Id syntax */
+    QStringList destinfo = replaceinfo[0].split(":");
+    cbdata.dtag = destinfo[0];
+    cbdata.did  = destinfo[1];
+    if (destinfo.count() >= 3) {
+      cbdata.dprop = destinfo[2];
+    }
+  }
+  else {
+    cbdata.dtag = replaceinfo[0];
+  }
+  if (replaceinfo.count() > 1) {
+    QStringList  srcinfo = replaceinfo[1].split(":");
+    if (srcinfo.count() < 1) {
+      *errorMessage = "Invalid target argument for replace command: " + arguments;
+      return false;
+    }
+    if (srcinfo.count() >= 1) {
+      if (srcinfo[0].length()) cbdata.newtag = srcinfo[0];
+    }
+    if (srcinfo.count() >= 2) {
+      if (srcinfo[1].length()) cbdata.newprop = srcinfo[1];
+    }
+    if (srcinfo.count() >= 3) {
+      if (srcinfo[2].length()) cbdata.newpropval = srcinfo[2];
+    }
+  }
+  return walkdoc(doc, &replacefunc, &cbdata, errorMessage);
+}
+
+void 
+addfunc(void *cbdata,
+ QDomElement e)
+{
+  AddData *d = reinterpret_cast<AddData *>(cbdata);
+  QString eid = e.attribute(WIX_ATTR_ID);
+
+  if (e.tagName().compare(d->dtag) == 0) {
+    /* if a specific Id is set, verify it too. */
+    if (d->did.isEmpty() ||
+        (eid.size() && !eid.compare(d->did)) ) {
+      if (d->newtag.compare(d->dtag)) {
+        QDomElement ne = e.ownerDocument().createElement(d->newtag);
+        if (!d->newprop.isNull()) {
+          ne.setAttribute(d->newprop, d->newpropval);
+        }
+        e.appendChild(ne);
+      }
+      else {
+        e.setAttribute(d->newprop, d->newpropval);
+      }
+    }
+  }
+}
+
+/** Make modifications to requested documents.
+ * returns false on error and <b>errorMessage</b> will be set.
+ */
+bool
+docadd(QDomDocument *doc,
+  QString arguments,
+  QString *errorMessage)
+{
+  Q_ASSERT(doc);
+  Q_ASSERT(errorMessage);
+  AddData  cbdata;
+
+  QStringList addinfo = arguments.split("=");
+  if (addinfo.count() < 1) {
+    *errorMessage = "Invalid argument for add command: " + arguments;
+    return false;
+  }
+  if (addinfo[0].contains(':')) {
+    /* Id syntax */
+    QStringList destinfo = addinfo[0].split(":");
+    cbdata.dtag = destinfo[0];
+    cbdata.did  = destinfo[1];
+  }
+  else {
+    cbdata.dtag = addinfo[0];
+  }
+  if (addinfo.count() > 1) {
+    QStringList  srcinfo = addinfo[1].split(":");
+    if (srcinfo.count() < 1) {
+      *errorMessage = "Invalid target argument for add command: " + arguments;
+      return false;
+    }
+    if (srcinfo.count() >= 1) {
+      if (srcinfo[0].length()) cbdata.newtag = srcinfo[0];
+    }
+    if (srcinfo.count() >= 2) {
+      if (srcinfo[1].length()) cbdata.newprop = srcinfo[1];
+    }
+    if (srcinfo.count() >= 3) {
+      if (srcinfo[2].length()) cbdata.newpropval = srcinfo[2];
+    }
+  }
+  return walkdoc(doc, &addfunc, &cbdata, errorMessage);
+}
+
+bool
+createRegLocalComponent(QDomElement e,
+ QString dirName,
+ QString keyPath)
+{ 
+  QDomElement nrk = e.ownerDocument().createElement(WIX_TAG_REGKEY);
+  QDomElement nrv = e.ownerDocument().createElement(WIX_TAG_REGVAL);
+  nrk.setAttribute(WIX_ATTR_REG_ROOT, "HKCU");
+  nrk.setAttribute(WIX_ATTR_REG_ACTION, "createAndRemoveOnUninstall");
+  nrk.setAttribute(WIX_ATTR_REG_KEYPATH, keyPath);
+  nrv.setAttribute(WIX_ATTR_REG_TYPE, WIX_REG_KEY_TYPE);
+  nrv.setAttribute(WIX_ATTR_REG_NAME, dirName);
+  nrv.setAttribute(WIX_ATTR_VALUE, "1");
+  nrv.setAttribute(WIX_ATTR_KEY, "yes");
+  nrk.appendChild(nrv);
+  e.appendChild(nrk);
+}
+
+bool
+createDirMgmtComponent(QDomElement e,
+ QString dirName)
+{
+  QDomElement nce;
+  /* An empty dir might produce a createdir, so only add if not present. */
+  if (e.elementsByTagName(WIX_TAG_CREATEDIR).count() == 0) {
+    nce = e.ownerDocument().createElement(WIX_TAG_CREATEDIR);
+    e.appendChild(nce);
+  }
+  nce = e.ownerDocument().createElement(WIX_TAG_REMOVEDIR);
+  nce.setAttribute("On", WIX_ATTR_DIRACTION);
+  nce.setAttribute(WIX_ATTR_ID, QString("Remove").append(dirName));
+  e.appendChild(nce);
+}
+
+void
+userlocalfunc(void *cbdata,
+ QDomElement e)
+{
+  UserLocalData *ulinfo = reinterpret_cast<UserLocalData *>(cbdata);
+  QString eid = e.attribute(WIX_ATTR_ID);
+
+  if (e.tagName().compare(WIX_TAG_FILE) == 0) {
+    e.removeAttribute(WIX_ATTR_KEY);
+  }
+  else if (e.tagName().compare(WIX_TAG_COMPONENT) == 0) {
+    /* If the WiX tools get confused we need to remove KeyPath attrs
+     * on any component elements after creation or merging.
+     * Empty directories with a CreateFolder and nothing else will do this.
+     */
+    e.removeAttribute(WIX_ATTR_KEY);
+  }
+  else if (e.tagName().compare(WIX_TAG_FEATURE) == 0) {
+    /* be sure to remove any default feature names; changed added above. */
+    QDomNodeList cnl = e.elementsByTagName(WIX_TAG_COMPONENT_REF);
+    for (int i = 0; i < cnl.count(); i++) {
+      QDomElement cre = cnl.item(i).toElement();
+      if (cre.attribute(WIX_ATTR_ID).compare(WIX_TAG_COMPONENT) == 0) {
+        e.removeChild(cre);
+      }
+    }
+    if (ulinfo->featureid.compare(e.attribute(WIX_ATTR_ID)) == 0) {
+      /* this is the target feature element for the new components, if any. */
+      QDomElement ne;
+      for (int i = 0; i < ulinfo->newcomps.count(); i++) {
+        QString currid = ulinfo->newcomps[i];
+        ne = e.ownerDocument().createElement(WIX_TAG_COMPONENT_REF);
+        ne.setAttribute(WIX_ATTR_ID, currid);
+        e.appendChild(ne);
+      }
+    }
+  }
+  else if (e.tagName().compare(WIX_TAG_DIR) == 0) {
+    QString dirName = e.attribute(WIX_ATTR_NAME);
+    QString dirId = e.attribute(WIX_ATTR_ID);
+    /* find all child components for this dir and see if it contains:
+     * create/remove folder elements, a registry element
+     */
+    if ( e.hasChildNodes() ) {
+      QDomElement fc;
+      bool  hasComponent = false;
+      bool  hasRegKey;
+      QDomNodeList subnodes = e.childNodes();
+      for (int i = 0; i < subnodes.count(); i++) {
+        hasRegKey = false;
+        if (subnodes.item(i).isElement()) {
+          QDomElement ce = subnodes.item(i).toElement();
+          if (ce.tagName().compare(WIX_TAG_COMPONENT) == 0) {
+            if (!hasComponent) {
+              hasComponent = true;
+              fc = ce;
+              if (ce.attribute(WIX_ATTR_ID).compare(WIX_TAG_COMPONENT) == 0) {
+                /* Fix default named components before adding registry elements. */
+                ce.setAttribute(WIX_ATTR_ID, QString("DCOMP").append(dirName));
+                ulinfo->newcomps.append(ce.attribute(WIX_ATTR_ID));
+              }
+              if (ce.elementsByTagName(WIX_TAG_REMOVEDIR).count() == 0) {
+                createDirMgmtComponent(ce, ce.attribute(WIX_ATTR_ID));
+              }
+            }
+            QDomNodeList compnodes = ce.childNodes();
+            for (int j = 0; j < compnodes.count(); j++) {
+              if (compnodes.item(j).isElement()) {
+                QDomElement compe = compnodes.item(j).toElement();
+                if (compe.tagName().compare(WIX_TAG_REGKEY) == 0) {
+                  hasRegKey = true;
+                }
+              }
+            }
+            if (!hasRegKey) {
+              createRegLocalComponent(ce, QString("RK").append(ce.attribute(WIX_ATTR_ID)), ulinfo->keypath);
+            }
+          }
+        }
+      }
+      if (!hasComponent) {
+        /* Certain system directories must be ignored; we don't manage them. */
+        if (dirId.compare("LocalAppDataFolder") &&
+            dirId.compare("AppDataFolder") &&
+            dirId.compare("CommonAppDataFolder") &&
+            dirId.compare("CommonFilesFolder") &&
+            dirId.compare("DesktopFolder") &&
+            dirId.compare("PersonalFolder") &&
+            dirId.compare("ProgramFilesFolder") &&
+            dirId.compare("ProgramMenuFolder") &&
+            dirId.compare("StartMenuFolder") &&
+            dirId.compare("StartupFolder") &&
+            dirId.compare("SystemFolder") &&
+            dirId.compare("TempFolder") &&
+            dirId.compare("WindowsFolder") ) {
+          /* if there is no component under this dir parent then we
+           * must create a component for the sole purpose of dir
+           * creation with the requisite registry key path.
+           */
+          QDomElement ne = e.ownerDocument().createElement(WIX_TAG_COMPONENT);
+          QString compId = QString("ULDirComp_").append(dirName);
+          ne.setAttribute(WIX_ATTR_GUID, "*");
+          ne.setAttribute(WIX_ATTR_ID, compId);
+          e.appendChild(ne);
+          createDirMgmtComponent(ne, dirName);
+          createRegLocalComponent(ne, QString("DRK").append(dirName), ulinfo->keypath);
+          ulinfo->newcomps.append(compId);
+        }
+      }
+    }
+  }
+}
+
+/** Make modifications to requested documents.
+ * returns false on error and <b>errorMessage</b> will be set.
+ */
+bool
+docuserlocal(QDomDocument *doc,
+  QString argument,
+  QString *errorMessage)
+{
+  Q_ASSERT(doc);
+  Q_ASSERT(errorMessage);
+  UserLocalData  cbdata;
+
+  QStringList ulinfo = argument.split(":");
+  if (ulinfo.count() < 2) {
+    *errorMessage = "Invalid argument for userlocal command: " + argument;
+    return false;
+  }
+  cbdata.keypath = ulinfo[0];
+  cbdata.featureid = ulinfo[1];
+  return walkdoc(doc, &userlocalfunc, &cbdata, errorMessage);
+}
+
+/** Display application usage and exit. */
+void
+print_usage_and_exit()
+{
+  QTextStream error(stderr);
+  error << "usage: wixtool <command> [-q] -i <infile> -o <outfile> <Arg0> [... <ArgN>]" << endl;
+  error << "  command one of: " << endl;
+  error << "    splice        Splice children from one document into another." << endl;
+  error << "    replace       Replace elements or attributes in a document." << endl;
+  error << "    add           Add elements or attributes into a document." << endl;
+  error << "    userlocal     Convert File elements into per-user local elements." << endl;
+  error << "  -i <infile>     Input or template file" << endl;
+  error << "  -o <outfile>    Output file" << endl;
+  error << endl;
+  error << "  splice args:    desttagname[:Id]=file:basetag[:Id]" << endl;
+  error << "    Splice children of basetag in file under desttagname" << endl;
+  error << endl;
+  error << "  replace args:   tagname[:Id]:property=newtagname[:Id]:property:value" << endl;
+  error << "    If newtagname is empty the element is deleted" << endl;
+  error << "    If newproperty is empty the property is deleted" << endl;
+  error << endl;
+  error << "  add args:       desttagname[:Id]=newtagname[:Id]:property:value" << endl;
+  error << "    Add properties or child elements to target" << endl;
+  error << "    If newtagname is empty only properties added to dest" << endl;
+  error << endl;
+  error << "  userlocal arg:  <registry key path>:<dest feature id>" << endl;
+  error << "    Convert KeyPath File elements into the per user local idiom" << endl;
+  error << "    with corresponding Create/RemoveDir and RegistryKey elements." << endl;
+  error << endl;
+  error << " NOTE: text content within an element is not accessible." << endl;
+  error << "       Use the Value= attribute syntax if necessary." << endl;
+  error << "       The optional :Id syntax restricts matching to elements with" << endl;
+  error << "       the Id attribute set to the value indicated." << endl;
+  error.flush();
+  exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+  QTextStream error(stderr);
+  QString command, errorMessage;
+  char *infile = 0, *outfile = 0;
+  QTextCodec *codec = QTextCodec::codecForName("utf-8");
+  bool quiet = false;
+  QStringList commandargs;
+
+  /* Check for the correct number of input parameters. */
+  if (argc < 6)
+    print_usage_and_exit();
+
+  /* Verify command is supported. */
+  command = argv[1];
+  if ( command.compare("splice", Qt::CaseInsensitive) &&
+       command.compare("replace", Qt::CaseInsensitive) &&
+       command.compare("add", Qt::CaseInsensitive) &&
+       command.compare("userlocal", Qt::CaseInsensitive) ) {
+    print_usage_and_exit();
+  }
+
+  /* Gather remaining arguments. */
+  for (int i = 2; i < argc; i++) {
+    QString arg(argv[i]);
+    if (!arg.compare("-q", Qt::CaseInsensitive))
+      quiet = true;
+    else if (!arg.compare("-i", Qt::CaseInsensitive) && ++i < argc)
+      infile = argv[i];
+    else if (!arg.compare("-o", Qt::CaseInsensitive) && ++i < argc)
+      outfile = argv[i];
+    else if (infile && outfile) {
+      commandargs.append(arg);
+    }
+  }
+  if ( !infile || !outfile || !commandargs.count() ) {
+    print_usage_and_exit();
+  }
+
+  /* Open the source document for reading. */
+  QFile srcFile(infile);
+  QTextStream sfiletxt(&srcFile);
+  sfiletxt.setCodec(codec);
+  if (!srcFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+    error << QString("Unable to open '%1' for reading: %2\n").arg(infile)
+                                                .arg(srcFile.errorString());
+    return 2;
+  }
+
+  /* Make sure the outfile does not exist before we use it. */
+  if (QFile::exists(outfile)) {
+    if (!QFile::remove(outfile)) {
+      error << QString("Unable to truncate outfile '%1'\n").arg(outfile);
+      return 2;
+    }
+  }
+
+  QDomDocument doc;
+  QString parseError;
+  int  badline, badcol;
+  if (!doc.setContent (sfiletxt.readAll(), false, &parseError, &badline, &badcol)) {
+    error << QString("Error parsing source document '%1' at line %2 and column %3: %4")
+                    .arg(infile).arg(badline).arg(badcol).arg(parseError);
+    return 3;
+  }
+
+  if (!command.compare("userlocal", Qt::CaseInsensitive)) {
+    if (!docuserlocal(&doc, commandargs[0], &errorMessage)) {
+      error << QString("Unable to convert document components to user local: %1\n")
+                          .arg(errorMessage);
+      return 4;
+    }
+  }
+  else {
+    for (int i = 0; i < commandargs.count(); i++) {
+      if (!command.compare("splice", Qt::CaseInsensitive)) {
+        if (!docsplice(&doc, commandargs[i], &errorMessage)) {
+          error << QString("Unable to process splice command '%1': %2\n")
+                          .arg(commandargs[i]).arg(errorMessage);
+          return 4;
+        }
+      }
+      else if (!command.compare("replace", Qt::CaseInsensitive)) {
+        if (!docreplace(&doc, commandargs[i], &errorMessage)) {
+          error << QString("Unable to process replace command '%1': %2\n")
+                          .arg(commandargs[i]).arg(errorMessage);
+          return 4;
+        }
+      }
+      else if (!command.compare("add", Qt::CaseInsensitive)) {
+        if (!docadd(&doc, commandargs[i], &errorMessage)) { 
+          error << QString("Unable to process add command '%1': %2\n")
+                          .arg(commandargs[i]).arg(errorMessage);
+          return 4;
+        }
+      }
+    }
+  }
+
+  /* Open the output file for writing. */
+  QFile docFile(outfile);
+  if (!docFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
+    error << QString("Unable to open '%1' for writing: %2\n").arg(outfile)
+                                                .arg(docFile.errorString());
+    return 5;
+  }
+
+  /* Write the .wxl output. */
+  QTextStream out(&docFile);
+  out << doc.toString(4);
+
+  return 0;
+}
+