Приветствую!
В этой статье я покажу вам свой вариант использования набора утилит WixToolSet для создания кастомных диалоговых окон с возможностью получения предустановленной информации (пароли, явки и прочие параметры). И приведу пример использования кастомных окон в простом кейсе.
Тем, кому лениво читать, предлагаю ознакомиться с проектом на github.
Проект состоит из двух частей:
Установщик MSI — установщик основного продукта, в задачи которого входит:
a. запросить пароли пользователя и рута;
b. скопировать скрипт в назначенную папку и запустить его с параметрами, по результату работы которого мы получаем файл с паролями;
c. открыть блокнотом итоговый файл.Установщик EXE — альтернативный установщик, который позволяет предварительно установить все необходимые компоненты для работы основного продукта, а после и сам продукт.
Его задачи и то, что мы делаем:
a. устанавливаем необходимые компоненты (в качестве примера представлен код тихой установки MSSQL Server 2016);
b. запрашиваем пароли пользователя и рута;
c. запускаем установщик основного продукта, где полученные пароли пользователя и рута передаем в качестве параметров (установщик основного продукта запускается в тихом режиме, без диалоговых окон);
d. установщик основного продукта выполняет все действия, указанные в п.1, за исключением п.1.а.
Я не буду заострять внимание на основах создания проектов windows installer и executable packages, а также описывать связи между ними, так как тема данной статьи — кастомизация. Считаю, что данного кейса для демонстрации возможностей оных вполне хватает. Кто столкнулся с набором утилит впервые, предлагаю рабочий проект на github и несколько полезных ссылок в конце статьи. Поехали!
Кастомизация пакетного установщика MSI
По сценарию работы основного продукта у пользователя необходимо запросить пароли пользователя и рута, в этом нам поможет кастомное диалоговое окно.
Для этого создаем в проекте файл PasswordDlg.wxs со следующим содержанием:
<!-- Перевод для ссылок вида !(loc.name) указываем в файле локализации Variables.wxl
Стили формата {\WixUI_Font_Name} описываются в файле сценария MyWixUI_InstallDir.wxs -->
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<UI>
<!-- Задаем баннер, файл которого расположен в корне проекта. Требования к размерам картинки можно посмотреть тут -->
<Binary Id="BannerBmp" SourceFile="Banner.bmp" />
<!-- Задаем имя своего окна Id="PasswordDlg" и указываем название в поле Title -->
<Dialog Id="PasswordDlg" Width="370" Height="270" Title="!(loc.PasswordDialogTitle)">
<!-- Задаем оглавление окна, используем координаты X и Y, где центр оси левый верхний угол, и размер блока W,H -->
<Control Id="Title" Type="Text" X="15" Y="6" Width="160" Height="15" Transparent="yes" NoPrefix="yes" Text="{\WixUI_Font_Title}!(loc.PasswordTitle)" />
<!-- Задаем описание окна -->
<Control Id="Description" Type="Text" X="25" Y="23" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.PasswordTitleDescription)" NoWrap="no"/>
<!-- Задаем banner, где в поле Text указывается Bynary Id, объявленный выше -->
<Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="BannerBmp" />
<Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
<!-- Выводим информацию-пояснение к диалоговому окну -->
<Control Id="DialogInfo" Type="Text" X="20" Y="60" Width="228" Height="45" Text="!(loc.PasswordDialogInfo)" TabSkip="yes" Transparent="yes" />
<!-- Задаем поле для ввода пароля пользователя и привязываем к полю параметр ProperPasswordUser -->
<Control Id="LabelPwdUser" Type="Text" X="20" Y="100" Height="17" Width="95" Transparent="yes" Text="!(loc.LabelPasswordUser)" />
<Control Id="EditPwdUser" Type="Edit" X="100" Y="97" Height="17" Width="150" Property="ProperPasswordUser" />
<!-- Задаем поле для ввода пароля рута и привязываем к полю параметр ProperPasswordRoot -->
<Control Id="LabelPwdRoot" Type="Text" X="20" Y="120" Height="17" Width="95" Transparent="yes" Text="!(loc.LabelPasswordRoot)" />
<Control Id="EditPwdRoot" Type="Edit" X="100" Y="117" Height="17" Width="150" Property="ProperPasswordRoot" />
<!-- Выводим пояснение о требованиях к сложности пароля для пользователя -->
<Control Id="DialogDefaultPwdInfo" Type="Text" X="20" Y="150" Width="300" Height="40" Text="!(loc.PasswordDefaultPwdDescription)" TabSkip="yes" Transparent="yes" Disabled="yes" />
<!-- Рисуем разделительную линию и функциональные кнопки Назад, Далее и Отмена -->
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.PasswordButtonNext)" />
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.PasswordButtonBack)" />
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.PasswordButtonCancel)">
<Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
</Control>
</Dialog>
</UI>
</Fragment>
</Wix>
В качестве локализации у нас будет выступать файл Variables.wxl со следующим содержанием:
<!-- Тут, я думаю, понятно все и без комментариев -->
<?xml version="1.0" encoding="utf-8"?>
<WixLocalization Culture="ru-RU" xmlns="http://schemas.microsoft.com/wix/2006/localization">
<String Id="PasswordTitle">Настройка сервера</String>
<String Id="PasswordTitleDescription">Укажите информацию и нажмите кнопку "Далее"</String>
<String Id="PasswordDialogTitle">Установка [ProductName]</String>
<String Id="PasswordDialogInfo">Укажите пароли пользователей базы данных:</String>
<String Id="PasswordDefaultPwdDescription">Для обеспечения инфоромационной безопасности и сохранности корпоротивной информации, необходимо заменить пароли на более сложные</String>
<String Id="PasswordEnter">Введите пароль:</String>
<String Id="PasswordButtonNext">Далее</String>
<String Id="PasswordButtonBack">Назад</String>
<String Id="PasswordButtonCancel">Отмена</String>
<String Id="LabelUserName">Пользователь:</String>
<String Id="LabelPasswordUser">Пароль для пользователя:</String>
<String Id="LabelPasswordRoot">Пароль для рута:</String>
</WixLocalization>
Далее, нам необходимо создать свой сценарий диалоговых окон (далее - сценарий), куда будет добавлено новое окно для запроса пароля пользователя и рута. В нашем проекте мы используем сценарий WixUI_InstallDir, файл которого расположен по пути: C:\Program Files (x86)\WiX Toolset v3.11\SDK\wixui\WixUI_InstallDir.wxs.
Копируем этот файл в наш проект, предварительно переименовав в MyWixUI_InstallDir.wxs.
Набор утилит WixToolSet предоставляет несколько стандартных сценариев диалоговых окон. Список всех доступных сценариев можно посмотреть тут.
Как вы поняли из названия, сценарий представляет собой цепочку из диалоговых окон с описанием действий при нажатии на кнопки: «Далее», «Назад», «Отмена» и т.д. Нам остается всего лишь вставить свое окно в цепочку и указать ссылки на предыдущее и следующее окна.
Полный список доступных диалоговых окон можно посмотреть тут.
Содержание файла MyWixUI_InstallDir.wxs у нас следующее:
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<!-- Даем новое имя нашему сценарию -->
<UI Id="MyWixUI_InstallDir">
<!-- Определяем стили текста, WixUI_Font_Title также используется в PasswordDlg.wxs -->WixUI_Font_Title используется в PasswordDlg.wxs
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<Property Id="WixUI_Mode" Value="InstallDir" />
<!-- Перечень ссылок на вспомогательные диалоговые окна -->
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<!-- Далее, мы публикуем диалоговые окна в определенном порядке, параметры публикации описаны тут, а условия тут -->
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
<!-- Начало сценария, окно приветствия -->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">NOT Installed</Publish>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
<!-- Для окна лицензионного соглашения изменяем переход на PasswordDlg -->
<Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="PasswordDlg">LicenseAccepted = "1"</Publish>
<!-- Добавляем наше диалоговое окно, при этом меняем значение Value для перехода на InstallDirDlg и возврата к LicenseAgreementDlg →
<Publish Dialog="PasswordDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>
<Publish Dialog="PasswordDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">1</Publish>
<!-- Для окна выбора пути установки изменяем возврат к PasswordDlg -->
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="PasswordDlg">1</Publish>
<!-- Дальше можно ничего не трогать -->
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
<Property Id="ARPNOMODIFY" Value="1" />
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
</Wix>
Теперь осталось добавить наш новый сценарий, картинки баннера и хедера в проект, для этого в файле Product.wxs указываем:
<!-- Тут указываем свою цепочку диалоговых окон -->
<UIRef Id="MyWixUI_InstallDir"/>
<!-- Тут указываем картинку для баннера -->
<WixVariable Id="WixUIBannerBmp" Value="Banner.bmp" />
<!-- Тут указываем картинку для хедера диалогового окна -->
<WixVariable Id="WixUIDialogBmp" Value="Dialog.bmp" />
На этом вся работы выполнена, можно запускать проект и наслаждаться результатом.
Кастомизация исполняемого установщика EXE
Основная цель использования альтернативного установщика EXE - инсталляция дополнительных компонентов для работы основного продукта. Также для удобства мы запросим пароли пользователя и рута, передадим их в качестве параметров в установщик MSI, который запускается в тихом режим, без демонстрации диалоговых окон.
Для наших целей нам подойдет кастомизация окна опций установщика MSI, где в качестве шаблона установщика мы используем RtfLargeTheme.xml и файл локализации RtfTheme.wxl.
Копируем эти файлы в наш проект, предварительно переименовав в MyRtfLargeTheme.xml и MyRtfTheme.wxl, соответственно. Файлы расположены тут: C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes.
Чтобы попасть в опции необходимо нажать кнопку “Опции” в окне приветствия, после запуска установщика EXE, далее нам откроется окно запроса паролей пользователя и рута, как показано на рисунке 3.
Для получения такого окна необходимо изменить файл шаблона MyRtfLargeTheme.xml, разделы <Page Name=“Install“> и <Page Name=“Options“>, содержание которого выглядит так:
<!--Перевод для ссылок вида #(loc.name) указываем в файле локализации, обратите внимание на то, что в проекте MSI ссылки были вида !(loc.name)-->
<!--Описываем окно Приветствия, добавляем версию проекта и кнопку Опции-->
<Page Name="Install">
<Text X="11" Y="80" Width="-11" Height="-70" TabStop="no" FontId="2" HexStyle="0x800000" DisablePrefix="yes" />
<Richedit Name="EulaRichedit" X="12" Y="81" Width="-12" Height="-71" TabStop="yes" FontId="0" />
<Text Name="InstallVersion" X="11" Y="-41" Width="210" Height="17" FontId="3" DisablePrefix="yes" HideWhenDisabled="yes">#(loc.InstallVersion)</Text>
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
<!--Добавляем кнопку Опции, где и буду запрашиваться наши параметры-->
<Button Name="OptionsButton" X="-191" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
<Button Name="InstallButton" X="-91" Y="-11" Width="95" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
</Page>
<!--Описываем окно Опции, добавляем поля для ввода паролей пользователя и рута-->
<Page Name="Options">
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
<!--Создаем поле для ввода пароля пользователя-->
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsUserPwd)</Text>
<Editbox Name="UserPwdEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes">DefaultUserPwd</Editbox>
<!--Создаем поле для ввода пароля рута-->
<Text X="11" Y="171" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsRootPwd)</Text>
<Editbox Name="RootPwdEditbox" X="11" Y="193" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes">DefaultRootPwd</Editbox>
<!--Кнопки ОК и Отмена-->
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#!(loc.!(loc.(loc.OptionsCancelButton)</Button>
</Page>
Содержание файла локализации MyRtfTheme.wxl я показывать не буду, думаю, что с ним вы разберетесь самостоятельно.
Нам осталось указать получившиеся шаблон и локализацию в проект, для этого нам нужно внести изменения в файл Bundle.wxs, добавив следующее:
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<bal:WixStandardBootstrapperApplication
<!-- Укажем файл лицензионного соглашения, который расположен в корне проекта-->
LicenseFile="EULA-RU.rtf"
<!-- Укажем иконку для проекта, которая расположена в корне проекта-->
LogoFile="logo.ico"
<!-- Укажем возможность починки проекта через инсталлятор-->
SuppressRepair="no"
<!-- Укажем созданные шаблон и файл локализации, которые расположены в корне проекта-->
ThemeFile="MyRtfLargeTheme.xml"
LocalizationFile="MyRtfTheme.wxl"
<!-- Показываем версию проекта в окне установщика-->
ShowVersion="yes"/>
</BootstrapperApplicationRef>
<!-- Создадим две переменные для хранения значений паролей, полученных в окне опций. Использовать переменные не обязательно, но в данном случае нас интересует параметр Hidden=”yes”, который позволяет скрыть значение паролей при передаче в качестве параметров.-->
<!-- В Value записываем значение, полученное из контрола EditBox [UserPwdEditbox], определенного в опциях -->
<Variable Name="UserPwdVariable" Type="string" bal:Overridable="yes" Value="[UserPwdEditbox]" Hidden="yes" />
<!-- В Value записываем значение, полученное из контрола EditBox [RootPwdEditbox], определенного в опциях -->
<Variable Name="RootPwdVariable" Type="string" bal:Overridable="yes" Value="[RootPwdEditbox]" Hidden="yes" />
<!-- В цепочке мы указываем наш установщик и передаем пароли как параметры -->
<Chain>
<!--Установка дополнительной компоненты-->
<ExePackage Id="InstallPackage" …
</ExePackage>
<!--Запускаем основной проект и параметрами передаем пароли-->
<MsiPackage Id="InstallMSI" SourceFile= "$(var.WixToolSet_MSI.TargetDir)WixToolSet_MSI.msi"
DisplayName="Установка MSI"
Visible="yes"
Vital="yes">
<MsiProperty Name="APPLICATIONFOLDER" Value="[SourceFolder]"/>
<!--Передаем значения полученных паролей в качестве параметров-->
<MsiProperty Name="PROPPWDROOT" Value="[RootPwdVariable]" />
<MsiProperty Name="PROPPWDUSER" Value="[UserPwdVariable]" />
</MsiPackage>
</Chain>
В итоге у нас получилась некая обертка, которая позволяет установить все необходимые зависимости или компоненты для вашего продукта, а после сам продукт, предварительно запросив всю необходимую информацию.
Как мы видим, сложности в получении необходимой информации для полной и качественной установки вашего приложения нет, все будет зависеть только от вашей фантазии и разумности.
Надеюсь, сильно ругать не будете, это моя первая статья. Проба пера!
Всем спасибо и до скорого!
Автор статьи: Сокол Даниил
П.С. Полезные ссылки для изучения набора утилит WixToolSet.
Статьи от @Terror, где описаны основы работы с wixtoolset:
Создание инсталлятора с помощью WiX.
Создание инсталлятора с помощью WiX. Часть 2.
Создание инсталлятора с помощью WiX. Часть 3.
Статья от автора @Revolution, где описан прекрасный способ сборки пакета msi со сторонними файлами:
Автоматическое добавление файлов в WiX инсталлятор.
tomasloh
А можно ли выполнить функцию над полученной строкой? Скажем проверить, что два пароля совпадают или получить хеш?
reb00s
В теории да, но я такого не делал. Если я правильно понял кейс, то необходимо после нажатия кнопки "Далее" выполнить определенные действия, по результату чего перейти на следующее окно, либо вывести сообщение об ошибке, так?
Вообще, я не вижу смысла усложнять инсталлятор и добавлять какие либо проверки, но если очень хочется... :)
Предлагаю смотреть в сторону следующих статей: https://stackoverflow.com/questions/33690724/wix-custom-dialog-when-previous-version-exists, https://stackoverflow.com/questions/16336684/inserting-custom-action-between-dialogs-installuisequence-in-wix