2013-06-21 12:05:11 UTC

Замечания

Начиная с Windows 7 и Windows Server 2008 R2, содержащих Windows Installer 5.0, появилась замечательная возможность сделать один установочный пакет, корректно устанавливающий приложения как для текущего пользователя, так и для всех пользователей, учитывая все нюансы UAC. Однако, это делается не так очевидно, как кажется на первый взгляд. Вот этим и хочется поделиться.

Начнем пожалуй с пруфлинка на MSDN. Там говорится что, для того, чтобы сделать такой пакет, вам нужно установить начальные значения свойств ALLUSERS и MSIINSTALLPERUSER в 2 (два) и 1 (один) соответственно. После того, как это будет сделано, настройкой по умолчанию будет установка только для одного (текущего) пользователя, но с возможностью изменить это (т.е. установить для всех) в процессе установки.

Это была теория, а теперь перейдем к практике с использованием WiX toolset. В WiX UI расширении есть возможность выбрать последовательность WixUI_Advanced в которой можно выбрать как вы будете ставить приложение — только для текущего пользователя или для всех. Замечательная вещь, но, как это часто бывает, без обработки напильником это не работает как ожидается. Корень проблемы лежит в том, что WixUI_Advanced ничего не знает про свойство MSIINSTALLPERUSER и соответственно не делает того, что надо делать для корректной работы такого пакета установки. Для того, чтобы это исправить, вам понадобятся исходники WiX, Взять можно с CodePlex.

Действия

Первое, что надо сделать, это в вашем главном файле wxs (Обычно Product.wxs), установить вышеупомянутые свойства в нужные значения:

<Property Id="ALLUSERS" Secure="yes" Value="2" />
<Property Id="MSIINSTALLPERUSER" Secure="yes" Value="1" />

Далее, в каталоге src\ext\UIExtension\wixlib найти файл WixUI_Advanced.wxs, скопировать его к себе и включить в свой проект. Файл можно переименовывать, а можно и оставить как есть, не критично. Однако, чтобы не было пересечения с библиотечной последовательностью, необходимо переименовать id для модифицированной UI, т.е. надо найти в файле строчку:

<UI Id="WixUI_Advanced">

И переименовать значение Id в например WixUI_MySetup, т.е. теперь это будет выглядеть так:

<UI Id="WixUI_MySetup">

И ссылаться теперь нужно будет на этот Id т.е вместо:

<UIRef Id="WixUI_Advanced" />

надо:

<UIRef Id="WixUI_MySetup" />

Обычно это делается в Product.wxs.

Затем, найти определения custom actions WixSetDefaultPerUserFolder, WixSetDefaultPerMachineFolder, WixSetPerUserFolder, WixSetPerMachineFolder, по умолчанию они выглядят так:

<CustomAction
	Id="WixSetDefaultPerUserFolder" 
	Property="WixPerUserFolder" 
	Value="[LocalAppDataFolder]Apps\[ApplicationFolderName]" 
	Execute="immediate" />
<CustomAction 
	Id="WixSetDefaultPerMachineFolder"
	Property="WixPerMachineFolder"
	Value="[ProgramFilesFolder][ApplicationFolderName]" 
	Execute="immediate" />
<CustomAction 
	Id="WixSetPerUserFolder" 
	Property="APPLICATIONFOLDER" 
	Value="[WixPerUserFolder]" 
	Execute="immediate" />
<CustomAction 
	Id="WixSetPerMachineFolder" 
	Property="APPLICATIONFOLDER" 
	Value="[WixPerMachineFolder]" 
	Execute="immediate" />

и переименовать их Id, чтобы не было пересечения с библиотечными, например так:

<CustomAction
	Id="MyWixSetDefaultPerUserFolder"
	Property="WixPerUserFolder"
	Value="[LocalAppDataFolder]Apps\[ApplicationFolderName]"
	Execute="immediate" />
<CustomAction 
	Id="MyWixSetDefaultPerMachineFolder" 
	Property="WixPerMachineFolder" 
	Value="[ProgramFilesFolder][ApplicationFolderName]" 
	Execute="immediate" />
<CustomAction
	Id="MyWixSetPerUserFolder" 
	Property="APPLICATIONFOLDER" 
	Value="[WixPerUserFolder]" 
	Execute="immediate" />
<CustomAction 
	Id="MyWixSetPerMachineFolder" 
	Property="APPLICATIONFOLDER" 
	Value="[WixPerMachineFolder]" 
	Execute="immediate" />

Соответственным образом нужно модифицировать InstallExecuteSequence и InstallUISequence, для того чтобы работали ваши, а не библиотечные custom actions. Исправленный вариант:

<InstallExecuteSequence>
    <Custom Action="MyWixSetDefaultPerUserFolder" Before="CostFinalize" />
    <Custom Action="MyWixSetDefaultPerMachineFolder" After="MyWixSetDefaultPerUserFolder" />
    <Custom Action="MyWixSetPerUserFolder" After="MyWixSetDefaultPerMachineFolder">
        ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS="" OR (ALLUSERS=2 AND (NOT Privileged)))
    </Custom>
    <Custom Action="MyWixSetPerMachineFolder" After="MyWixSetPerUserFolder">
        ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS=1 OR (ALLUSERS=2 AND Privileged))
    </Custom>
</InstallExecuteSequence>
<InstallUISequence>
    <Custom Action="MyWixSetDefaultPerUserFolder" Before="CostFinalize" />
    <Custom Action="MyWixSetDefaultPerMachineFolder" After="MyWixSetDefaultPerUserFolder" />
    <Custom Action="MyWixSetPerUserFolder" After="MyWixSetDefaultPerMachineFolder">
        ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS="" OR (ALLUSERS=2 AND (NOT Privileged)))
    </Custom>
    <Custom Action="MyWixSetPerMachineFolder" After="MyWixSetPerUserFolder">
        ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS=1 OR (ALLUSERS=2 AND Privileged))
    </Custom>
</InstallUISequence>

После этого, в последовательности диалогов, нужно найти следующие строчки:

<Publish Dialog="InstallScopeDlg" Control="Next" Property="ALLUSERS" Value="{}" Order="2">
    WixAppFolder = "WixPerUserFolder"
</Publish>
<Publish Dialog="InstallScopeDlg" Control="Next" Property="ALLUSERS" Value="1" Order="3">
    WixAppFolder = "WixPerMachineFolder"
</Publish>

И заменить их на нужные нам:

<Publish Dialog="InstallScopeDlg" Control="Next" Property="MSIINSTALLPERUSER" Value="1" Order="3">
    WixAppFolder = "WixPerUserFolder"
</Publish>
<Publish Dialog="InstallScopeDlg" Control="Next" Property="MSIINSTALLPERUSER" Value="{}" Order="2">
    WixAppFolder = "WixPerMachineFolder"
</Publish>

Т.е. нам не нужно менять свойство ALLUSERS, т.к. это все испортит, а нужно менять свойство MSIINSTALLPERUSER, как написано в требованиях к такого типа инсталляторам.

После этих двух строчек, надо добавить ещё две:

<Publish 
    Dialog="InstallScopeDlg"
    Control="Next"
    Event="DoAction"
    Value="MyWixSetDefaultPerMachineFolder"
    Order="3">WixAppFolder = "WixPerMachineFolder"</Publish>
<Publish
    Dialog="InstallScopeDlg"
    Control="Next"
    Event="DoAction"
    Value="MyWixSetDefaultPerUserFolder"
    Order="3">WixAppFolder = "WixPerUserFolder"</Publish>

Это запуск custom actions, устанавливающих целевые папки по умолчанию в нужное нам значение. Без этого, при варианте установки для всех пользователей, целевая папка будет установлена неверно, т.к. custom actions в InstallUISequence и InstallExecuteSequence отрабатывают ещё при свойстве MSIINSTALLPERUSER = 1, и соответственно [ProgramFilesFolder] будет указывать в локальную для пользователя папку, вместо C:\Program Files

В итоге, у нас должно получиться примерно следующее (новый файл WixUI_Advanced.wxs):

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <WixVariable Id="WixUISupportPerUser" Value="1" Overridable="yes" />
        <WixVariable Id="WixUISupportPerMachine" Value="1" Overridable="yes" />

        <PropertyRef Id="ApplicationFolderName" />

        <CustomAction 
            Id="MyWixSetDefaultPerUserFolder"
            Property="WixPerUserFolder"
            Value="[LocalAppDataFolder]Apps\[ApplicationFolderName]"
            Execute="immediate" />
        <CustomAction 
            Id="MyWixSetDefaultPerMachineFolder" 
            Property="WixPerMachineFolder" 
            Value="[ProgramFilesFolder][ApplicationFolderName]" 
            Execute="immediate" />
        <CustomAction 
            Id="MyWixSetPerUserFolder"
            Property="APPLICATIONFOLDER"
            Value="[WixPerUserFolder]"
            Execute="immediate" />
        <CustomAction 
            Id="MyWixSetPerMachineFolder"
            Property="APPLICATIONFOLDER"
            Value="[WixPerMachineFolder]" 
            Execute="immediate" />

        <InstallExecuteSequence>
            <Custom Action="MyWixSetDefaultPerUserFolder" Before="CostFinalize" />
            <Custom Action="MyWixSetDefaultPerMachineFolder" After="MyWixSetDefaultPerUserFolder" />
            <Custom Action="MyWixSetPerUserFolder" After="MyWixSetDefaultPerMachineFolder">
                ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS="" OR (ALLUSERS=2 AND (NOT Privileged)))
            </Custom>
            <Custom Action="MyWixSetPerMachineFolder" After="MyWixSetPerUserFolder">
                ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS=1 OR (ALLUSERS=2 AND Privileged))
            </Custom>
        </InstallExecuteSequence>
        <InstallUISequence>
            <Custom Action="MyWixSetDefaultPerUserFolder" Before="CostFinalize" />
            <Custom Action="MyWixSetDefaultPerMachineFolder" After="MyWixSetDefaultPerUserFolder" />
            <Custom Action="MyWixSetPerUserFolder" After="MyWixSetDefaultPerMachineFolder">
                ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS="" OR (ALLUSERS=2 AND (NOT Privileged)))
            </Custom>
            <Custom Action="MyWixSetPerMachineFolder" After="MyWixSetPerUserFolder">
                ACTION="INSTALL" AND APPLICATIONFOLDER="" AND (ALLUSERS=1 OR (ALLUSERS=2 AND Privileged))
            </Custom>
        </InstallUISequence>

        <UI Id="WixUI_MySetup">
            <TextStyle 
				Id="WixUI_Font_Normal" 
				FaceName="!(loc.Advanced_Font_FaceName)" 
				Size="!(loc.Advanced_Font_Normal_Size)" />
            <TextStyle 
				Id="WixUI_Font_Bigger" 
				FaceName="!(loc.Advanced_Font_FaceName)" 
				Size="!(loc.Advanced_Font_Bigger_Size)" />
            <TextStyle 
				Id="WixUI_Font_Title" 
				FaceName="!(loc.Advanced_Font_FaceName)" 
				Size="!(loc.Advanced_Font_Title_Size)" Bold="yes" />
            <TextStyle 
                Id="WixUI_Font_Emphasized" 
                FaceName="!(loc.Advanced_Font_FaceName)" 
                Size="!(loc.Advanced_Font_Emphasized_Size)" 
                Bold="yes" />

            <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
            <Property Id="WixUI_Mode" Value="Advanced" />

            <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" />
            <DialogRef Id="WelcomeDlg"/>

            <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
           
            <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="1">1</Publish>
            <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="2">
                WIXUI_INSTALLDIR_VALID&lt;&gt;"1"
            </Publish>

            <Publish Dialog="AdvancedWelcomeEulaDlg" Control="Advanced" Event="NewDialog" Value="InstallScopeDlg" Order="1">
                !(wix.WixUISupportPerMachine) AND !(wix.WixUISupportPerUser)
            </Publish>
            <Publish Dialog="AdvancedWelcomeEulaDlg" Control="Advanced" Event="NewDialog" Value="FeaturesDlg" Order="2">
                NOT !(wix.WixUISupportPerMachine)
            </Publish>
            <Publish Dialog="AdvancedWelcomeEulaDlg" Control="Advanced" Event="NewDialog" Value="InstallDirDlg" Order="3">
                !(wix.WixUISupportPerMachine) AND NOT !(wix.WixUISupportPerUser)
             </Publish>

            <Publish Dialog="InstallScopeDlg" Control="Back" Event="NewDialog" Value="AdvancedWelcomeEulaDlg">1</Publish>
            <!-- override default WixAppFolder of WixPerMachineFolder as standard user won't be shown the radio group to set WixAppFolder -->
            <Publish Dialog="InstallScopeDlg" Control="Next" Property="WixAppFolder" Value="WixPerUserFolder" Order="1">
                !(wix.WixUISupportPerUser) AND NOT Privileged
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Property="MSIINSTALLPERUSER" Value="1" Order="3">
                WixAppFolder = "WixPerUserFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Property="MSIINSTALLPERUSER" Value="{}" Order="2">
                WixAppFolder = "WixPerMachineFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="MyWixSetDefaultPerMachineFolder" Order="3">
                WixAppFolder = "WixPerMachineFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Event="DoAction" Value="MyWixSetDefaultPerUserFolder" Order="3">
                WixAppFolder = "WixPerUserFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Property="APPLICATIONFOLDER" Value="[WixPerUserFolder]" Order="4">
                WixAppFolder = "WixPerUserFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Property="APPLICATIONFOLDER" Value="[WixPerMachineFolder]" Order="5">
                WixAppFolder = "WixPerMachineFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Event="NewDialog" Value="FeaturesDlg" Order="6">
                WixAppFolder = "WixPerUserFolder"
            </Publish>
            <Publish Dialog="InstallScopeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="7">
                WixAppFolder = "WixPerMachineFolder"
            </Publish>

            <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="InstallScopeDlg">
                !(wix.WixUISupportPerUser)
            </Publish>
            <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="AdvancedWelcomeEulaDlg">
                NOT !(wix.WixUISupportPerUser)
            </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">
                NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;"1"
            </Publish>
            <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="FeaturesDlg" 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="FeaturesDlg" Control="Back" Event="NewDialog" Value="InstallScopeDlg">
                NOT Installed AND WixAppFolder = "WixPerUserFolder"
            </Publish>
            <Publish Dialog="FeaturesDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg">
                NOT Installed AND WixAppFolder = "WixPerMachineFolder"
            </Publish>
            <Publish 
                Dialog="FeaturesDlg" 
                Control="Back" 
                Event="NewDialog" Value="MaintenanceTypeDlg">Installed</Publish>

            <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>

            <Publish Dialog="MaintenanceTypeDlg" Control="ChangeButton" Event="NewDialog" Value="FeaturesDlg">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>

            <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="3">
                Installed AND PATCH
            </Publish>

            <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">
                Installed AND PATCH
            </Publish>
        </UI>

        <InstallUISequence>
            <Show Dialog="WelcomeDlg" Before="AdvancedWelcomeEulaDlg" >Installed AND PATCH</Show>
        </InstallUISequence>

        <Property Id="WIXUI_INSTALLDIR" Value="APPLICATIONFOLDER" />
        <UIRef Id="WixUI_Common" />
    </Fragment>
</Wix>

Вот и все.

2013-06-21 12:05:11 UTC microsoft msi noweb programming setup snippet windows