Qt Installer Framework 的解决方法不覆盖现有安装

Posted

技术标签:

【中文标题】Qt Installer Framework 的解决方法不覆盖现有安装【英文标题】:Workaround for Qt Installer Framework not overwriting existing installation 【发布时间】:2018-03-09 09:12:24 【问题描述】:

这个问题是关于 Qt Installer Framework 2.0 版的。

此时,使用 Qt 安装程序框架的人都知道,如果不进行自定义,您根本无法通过安装程序覆盖现有安装。这显然是为了解决在使用 Qt 框架时发生的一些问题。

但是,对于较小的、相对简单的项目,覆盖非常好,并且比必须事先手动运行维护工具方便得多。

我正在寻找一种涉及自定义 UI + 组件脚本的解决方案,该脚本将一个按钮添加到目标目录页面,允许用户选择任一

    如果指定目录存在,则删除它,或者 在该目录中运行维护工具。

最好能够在目标目录中运行维护工具,让它自动删除给定的包,但是我意识到这要求有点过分了。 p>

我已阅读本网站上有关解决同一问题的其他问题的答案,但没有一个解决方案能正常工作。我还想提一下,我已经启动并运行了一个组件脚本,但没有自定义 UI。

【问题讨论】:

【参考方案1】:

我做了一个黑客攻击。将其放在 installscript.qs 的末尾。

component.addOperation("AppendFile", "@TargetDir@/cleanup.bat",
   "ping 127.0.0.1 -n 4\r\ndel /F /Q maintenancetool.exe \ 
    && for /F %%L in ('reg query HKEY_USERS /v /f \"@TargetDir@\\maintenancetool.exe\" /d /t REG_SZ /s /e') do \ 
        reg query %%L /v DisplayName \
            && reg delete %%L /f\r\ndel /F /Q cleanup.bat \
            && exit\r\n")
component.addOperation("Execute", "workingdirectory=@TargetDir@",
   "cmd", "/C", "start", "/B", 
   "Cleaning up", "cmd /C ping 127.0.0.1 -n 2 > nul && cleanup.bat > nul")

这将删除waiting 3 seconds 之后的maintenancetool.exe,导致安装程序仅警告目标文件夹不为空,而不是拒绝安装。它还删除了用于卸载程序的注册表项,因此它不会在添加/删除程序中累积。显然,在维护工具被删除后,您将无法再将其用于卸载或更新等操作,但我只能通过再次运行安装程序来支持它。维护工具仅在安装完成后编写,cmd start cmdhackery 是为了让安装程序不会注意到还有一个步骤仍在运行。如果您有多个可选组件,您可能需要增加延迟或使其更健壮以检查是否仍在运行。

理论上,不必编写批处理文件然后执行它。您应该能够直接执行该命令。在实践中,我还没有找到正确转义引号以使正确的 cmd 实例评估正确部分的方法。

【讨论】:

有机会我会试试这个。 通过 Windows 中的添加/删除程序 UI 检查已安装的程序。当你这样做时,你会得到已安装程序的副本吗? @rationalcoder 我确实这样做了,这很糟糕。我会看看我是否也能找到一个黑客。 @rationalcoder 最后发现了更多可以删除添加/删除程序条目的黑客。好的部分是复制/粘贴有 2 行。不好的地方是,当你觉得它很有趣时,它可能会坏掉。【参考方案2】:

我找到了一种在没有嵌入式控制器脚本的情况下使用 rationalcoder's solution 的方法!

您所要做的就是使用purge 命令启动维护工具并将yes 发送到其标准输入。 因此,将组件脚本中的这一行 installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs"); 替换为这一行 installer.execute(dir + "/maintenancetool.exe", ["purge"], "yes");

这样,安装被替换,Windows 中的添加/删除程序 UI 不包含任何重复项。

对于 Windows 用户,请确保原始安装目录不是由终端打开的。如果是,则不会删除该目录并且安装将失败。该目录将保持在错误状态,在您重新启动会话之前,您无法删除或访问它。

【讨论】:

这是一个不错的发现。有时间我一定要试一试。【参考方案3】:

好的,这个答案基于最新的安装程序框架(3.2.2),我不确定它是否适用于旧版本。

要覆盖目标目录,您只需在配置文件中将 RemoveTargetDir 设置为 false:

<RemoveTargetDir>false</RemoveTargetDir>

成功了。

官方文档对此元素的解释如下:

如果卸载时不应删除目标目录,则设置为false。

如果你不知道它是如何使用的,那就有点混乱了:

bool TargetDirectoryPage::validatePage()

    m_textChangeTimer.stop();

    if (!isComplete())
        return false;

    if (!isVisible())
        return true;

    ///
    /// NOTICE HERE:
    /// If you set RemoveTargetDir to false, function return true here.
    const QString remove = packageManagerCore()->value(QLatin1String("RemoveTargetDir"));
    if (!QVariant(remove).toBool())
        return true;

    const QString targetDir = this->targetDir();
    const QDir dir(targetDir);
    // the directory exists and is empty...
    if (dir.exists() && dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty())
        return true;

    const QFileInfo fi(targetDir);
    if (fi.isDir()) 
        QString fileName = packageManagerCore()->settings().maintenanceToolName();
#if defined(Q_OS_MACOS)
        if (QInstaller::isInBundle(QCoreApplication::applicationDirPath()))
            fileName += QLatin1String(".app/Contents/MacOS/") + fileName;
#elif defined(Q_OS_WIN)
        fileName += QLatin1String(".exe");
#endif

        QFileInfo fi2(targetDir + QDir::separator() + fileName);
        ///
        /// AND NOTICE HERE:
        /// Do exists check here.
        if (fi2.exists()) 
            return failWithError(QLatin1String("TargetDirectoryInUse"), tr("The directory you selected already "
                "exists and contains an installation. Choose a different target for installation."));
        

        return askQuestion(QLatin1String("OverwriteTargetDirectory"),
            tr("You have selected an existing, non-empty directory for installation.\nNote that it will be "
            "completely wiped on uninstallation of this application.\nIt is not advisable to install into "
            "this directory as installation might fail.\nDo you want to continue?"));
     else if (fi.isFile() || fi.isSymLink()) 
        return failWithError(QLatin1String("WrongTargetDirectory"), tr("You have selected an existing file "
            "or symlink, please choose a different target for installation."));
    
    return true;

Notice cmets "NOTICE HERE:"。

【讨论】:

据我测试,这种方法不会删除以前的安装。因此,新旧安装都将在 Windows 程序中。不是首选行为。 @user3405291是的,你是对的,最后我不得不将我的安装程序重构为在线以避免这种情况。【参考方案4】:

我终于找到了一个可行的解决方案。

你需要三件事来完成这个:

    一个组件脚本, 目标目录页面的自定义 UI,以及 自动点击卸载程序的控制器脚本。

我现在将逐字列出对我有用的内容(以及我的项目特定内容)。我的组件叫Atlas4500 Tuner

config.xml

<?xml version="1.0" encoding="UTF-8"?>
<Installer>
    <Name>Atlas4500 Tuner</Name>
    <Version>1.0.0</Version>
    <Title>Atlas4500 Tuner Installer</Title>
    <Publisher>EF Johnson Technologies</Publisher>
    <StartMenuDir>EF Johnson</StartMenuDir>
    <TargetDir>C:\Program Files (x86)\EF Johnson\Atlas4500 Tuner</TargetDir>
</Installer>

packages/Atlas4500 Tuner/meta/package.xml

<?xml version="1.0" encoding="UTF-8"?>
<Package>
    <DisplayName>Atlas4500Tuner</DisplayName>
    <Description>Install the Atlas4500 Tuner</Description>
    <Version>1.0.0</Version>
    <ReleaseDate></ReleaseDate>
    <Default>true</Default>
    <Required>true</Required>
    <Script>installscript.qs</Script>
    <UserInterfaces>
        <UserInterface>targetwidget.ui</UserInterface>
    </UserInterfaces>
</Package>

自定义组件脚本packages/Atlas4500 Tuner/meta/installscript.qs

var targetDirectoryPage = null;

function Component() 

    installer.gainAdminRights();
    component.loaded.connect(this, this.installerLoaded);


Component.prototype.createOperations = function() 

    // Add the desktop and start menu shortcuts.
    component.createOperations();
    component.addOperation("CreateShortcut",
                           "@TargetDir@/Atlas4500Tuner.exe",
                           "@DesktopDir@/Atlas4500 Tuner.lnk",
                           "workingDirectory=@TargetDir@");

    component.addOperation("CreateShortcut",
                           "@TargetDir@/Atlas4500Tuner.exe",
                           "@StartMenuDir@/Atlas4500 Tuner.lnk",
                           "workingDirectory=@TargetDir@");


Component.prototype.installerLoaded = function()

    installer.setDefaultPageVisible(QInstaller.TargetDirectory, false);
    installer.addWizardPage(component, "TargetWidget", QInstaller.TargetDirectory);

    targetDirectoryPage = gui.pageWidgetByObjectName("DynamicTargetWidget");
    targetDirectoryPage.windowTitle = "Choose Installation Directory";
    targetDirectoryPage.description.setText("Please select where the Atlas4500 Tuner will be installed:");
    targetDirectoryPage.targetDirectory.textChanged.connect(this, this.targetDirectoryChanged);
    targetDirectoryPage.targetDirectory.setText(installer.value("TargetDir"));
    targetDirectoryPage.targetChooser.released.connect(this, this.targetChooserClicked);

    gui.pageById(QInstaller.ComponentSelection).entered.connect(this, this.componentSelectionPageEntered);


Component.prototype.targetChooserClicked = function()

    var dir = QFileDialog.getExistingDirectory("", targetDirectoryPage.targetDirectory.text);
    targetDirectoryPage.targetDirectory.setText(dir);


Component.prototype.targetDirectoryChanged = function()

    var dir = targetDirectoryPage.targetDirectory.text;
    if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) 
        targetDirectoryPage.warning.setText("<p style=\"color: red\">Existing installation detected and will be overwritten.</p>");
    
    else if (installer.fileExists(dir)) 
        targetDirectoryPage.warning.setText("<p style=\"color: red\">Installing in existing directory. It will be wiped on uninstallation.</p>");
    
    else 
        targetDirectoryPage.warning.setText("");
    
    installer.setValue("TargetDir", dir);


Component.prototype.componentSelectionPageEntered = function()

    var dir = installer.value("TargetDir");
    if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) 
        installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/scripts/auto_uninstall.qs");
    

自定义目标目录小部件packages/Atlas4500 Tuner/meta/targetwidget.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>TargetWidget</class>
 <widget class="QWidget" name="TargetWidget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>491</width>
    <height>190</height>
   </rect>
  </property>
  <property name="sizePolicy">
   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
    <horstretch>0</horstretch>
    <verstretch>0</verstretch>
   </sizepolicy>
  </property>
  <property name="minimumSize">
   <size>
    <width>491</width>
    <height>190</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QLabel" name="description">
     <property name="text">
      <string/>
     </property>
    </widget>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout">
     <item>
      <widget class="QLineEdit" name="targetDirectory">
       <property name="readOnly">
        <bool>true</bool>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QToolButton" name="targetChooser">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="minimumSize">
        <size>
         <width>0</width>
         <height>0</height>
        </size>
       </property>
       <property name="text">
        <string>...</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
   <item>
    <layout class="QHBoxLayout" name="horizontalLayout_2">
     <property name="topMargin">
      <number>0</number>
     </property>
     <item>
      <widget class="QLabel" name="warning">
       <property name="enabled">
        <bool>true</bool>
       </property>
       <property name="text">
        <string>TextLabel</string>
       </property>
      </widget>
     </item>
     <item>
      <spacer name="horizontalSpacer">
       <property name="orientation">
        <enum>Qt::Horizontal</enum>
       </property>
       <property name="sizeHint" stdset="0">
        <size>
         <width>40</width>
         <height>20</height>
        </size>
       </property>
      </spacer>
     </item>
    </layout>
   </item>
   <item>
    <spacer name="verticalSpacer">
     <property name="orientation">
      <enum>Qt::Vertical</enum>
     </property>
     <property name="sizeHint" stdset="0">
      <size>
       <width>20</width>
       <height>122</height>
      </size>
     </property>
    </spacer>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

packages/Atlas4500 Tuner/data/scripts/auto_uninstall.qs

// Controller script to pass to the uninstaller to get it to run automatically.
// It's passed to the maintenance tool during installation if there is already an
// installation present with: <target dir>/maintenancetool.exe --script=<target dir>/scripts/auto_uninstall.qs.
// This is required so that the user doesn't have to see/deal with the uninstaller in the middle of
// an installation.

function Controller()

    gui.clickButton(buttons.NextButton);
    gui.clickButton(buttons.NextButton);

    installer.uninstallationFinished.connect(this, this.uninstallationFinished);


Controller.prototype.uninstallationFinished = function()

    gui.clickButton(buttons.NextButton);


Controller.prototype.FinishedPageCallback = function()

    gui.clickButton(buttons.FinishButton);

这里的想法是检测当前目录中是否有安装,如果有,则使用只需单击它的控制器脚本运行该目录中的维护工具。

请注意,我将控制器脚本放在一个脚本目录中,该目录是实际组件数据的一部分。如果您有多个组件,您可能会做一些更清洁的事情,但这就是我正在使用的。

您应该可以自己复制这些文件,只需调整字符串即可。

【讨论】:

它可以工作,但原始安装必须有这个 auto_uninstall 脚本才能工作。必须从项目的先前版本中获取此文件。 这真的很棒而且很有用,谢谢。我认为有意义的一项更改是将卸载程序的运行转移到 QInstaller.PerformInstallation 中,因为我们已经完成了向用户询问的问题,并且他们此时不会保释。 @FredRoss-Perry 我想了解更多相关信息。这是否消除了对第二个管理员权限检查的需要?你到底做了什么改变? @rationalcoder - 我没有进行管理员检查(我可能应该)。我改变的是添加这一行: Component.prototype.installerLoaded = function() ... gui.pageById(QInstaller.PerformInstallation).entered.connect(this, this.componentPerformInstallationPageEntered); 然后添加这个函数: Component.prototype.componentPerformInstallationPageEntered = function () // 在我们安装之前,我们会静默运行卸载程序(未显示) 谢谢,它按预期工作。我尝试在像@FredRoss-Perry 这样进入 PerformInstallation 页面时移动卸载,但是之后的安装是部分的,一些文件丢失了(比如我的应用程序二进制文件:-()。我试图添加一个计时器但没有成功,它看起来像同步问题,但installer.execute 应该是同步的。你知道吗?【参考方案5】:

您需要做的事情很少:

要获取 TargetDirectoryPage 的过去,您可以通过添加此代码来实现 installer.setValue("RemoveTargetDir", false)

允许您运行此代码的自定义 UI(或消息框)。此 UI 应插入 TargetDirectoryPage 之后。 // you need to append .exe on the maintenance for windows installation installer.execute(installer.findPath(installer.value("MaintenanceToolName"), installer.value("TargetDir")));

【讨论】:

这让我走上了正确的道路,因为运行维护工具的工作方式是这样的。然而,“RemoveTargerDir”并没有做任何事情。请参阅我提出的解决方案的答案。

以上是关于Qt Installer Framework 的解决方法不覆盖现有安装的主要内容,如果未能解决你的问题,请参考以下文章

在 Qt Installer Framework (QtIFW) 安装程序中安装 VC++ Redistributables?

Qt Installer Framework - 防止安装在非空文件夹中

使用Qt installer framework制作安装包(不知道是否适合Mac和Linux?)

Qt Installer Framework - 在开始菜单中为所有用户创建快捷方式

重要网址小记

重要网址小记