NSIS: продолжение установки после перезагрузки.

Преамбула

Например, у нас есть софтина, которая в процессе своей установки требует перезагрузку, после чего доустанавливается. Например, софт устанавливает свой драйвер, обновление, или же регистрирует системный сервис. Как реализовать инсталлятор для такой программы?

Изначально я хотел обойтись параметрами командной строки, добавляемыми при запуске установщика, но с ними произошла жопа, поэтому будем действовать более надежными методами.

Алгоритм

0. Проверяем наличие уникального ключа Реестра, сигнализирующего о том, что надо выполнить вторую часть установки (после перезагрузки):

DetailPrint "Read After Reboot flag..."
ReadRegStr $R0 HKCU "Software\Microsoft\Windows\CurrentVersion\"
"${APPNAME}_afterreboot"
StrCmp "$R0" "1" AfterReboot

1. Производим все действия, которые необходимо произвести до перезагрузки (устанавливаем драйвера, регистрируем DLL и т.д.)
2. Записываем в однократную автозагрузку (ключ Реестра RunOnce) наш инсталлятор, путь к exe файлу инсталлятора, который можно взять из переменной $EXEPATH.
Автозагрузку прописываем для текущего пользователя (пользователя, запустившего установщик), т.е. в ключ HKEY_CURRENT_USER.

WriteRegStr HKCU
"Software\Microsoft\Windows\CurrentVersion\RunOnce"
"${APPNAME}" "$EXEPATH"

Примечание: В RunOnce могут записаться программы, которые требуется выполнить однократно при загрузке системы. После выполнения программ, ключ RunOnce очищается автоматически, что отличает его от ключа Run, где данные не очищаются, и его можно использовать, как постоянную автозагрузку.

3. Устанавливаем уникальный флаг, по которому будем определять, что инсталлятор был запущен после перезагрузки, например, таким образом:

DetailPrint "Write After Reboot flag..."
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\"
"${APPNAME}_afterreboot" "1"

4. Выдаем запрос на немедленную перезагрузку:

DetailPrint "Reboot request."
MessageBox MB_YESNO|MB_ICONINFORMATION
"Installation will be cobntinue after reboot. Press OK to reboot now." IDYES RebootID

Если пользователь ответил «Yes«, сразу перезагружаемся:

	RebootID:
		DetailPrint "Rebooting..."
		Reboot

Если нет, ждем перезагрузки вручную:

DetailPrint "Installation continue if user restart system."
Goto SecEnd

5. После перезагрузки удаляем флаг (проверив его наличие, см выше):
DetailPrint "Delete After Reboot flag..."
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\"
"${APPNAME}_afterreboot"

6. И продолжаем установку…
DetailPrint "Continue install after reboot..."

Скриншоты



Исходник примера

На GitHub

Глюк в NSIS: проверка параметров командной строки инсталлятора не работает

Вот серьезно, бьюсь (головой об стену) уже третий день, ну не работает функция GetOptions, так, как заявлено в NSIS Wiki, уже и по колесам стучал, и фары протирал, в смысле просто тупо копировал код из примера в тестовый инсталлятор, и еще десяток написал, перепробовав все что можно и нельзя, ну не выходит каменный цветок, хоть тресни. Функция в любом случае выдает, либо что никаких ошибок не случилось, для следующего кода, соответственно, всегда получается found:

Section
	${GetOptions} "/S /T" "/T"  $R0
 
	IfErrors 0 +2
	MessageBox MB_OK "Not found" IDOK +2
	MessageBox MB_OK "Found"
SectionEnd

Либо, для следующего кода всегда остается значение по умолчанию, даже если в командной строке явно задано иное:

Section
	${GetOptions} "/AR=N" "/AR=" $R0
	DetailPrint "R0: $R0"
	
SectionEnd

Например, выполняешь TestInstaller.exe /AR=Y, все равно в переменной $R0 оказывается N

Такие дела.

NSIS: Совсем коротко про секции

Преамбула

Секции — это удобный механизм, позволяющий обеспечить пользователю выбор компонентов программы, если такие присутствуют. В каждой секции можно создавать свои ярлыки, распаковывать файлы, и производить любые другие действия, практически независимо от других секций. Естественно, значения переменных, измененные в одной секции, в других тоже будут изменены, потому что в NSIS все переменные глобальные.

Отображение секций и выбор компонентов

Для самого простого выбора компонентов в NSIS, необходимо вставить в скрипт, перед, собственно, описанием секции, следующий код:

ComponentText "Какой-то текст", например:

ComponentText "Choose which features of ${APPNAMEANDVERSION} you want to install."

Естественно, ${APPNAMEANDVERSION} должна быть заранее определена:

!define APPNAME "SectionsExample"
!define APPNAMEANDVERSION " SectionsExample 0.1"


Окно инсталлятора с выбором компонентов

Простая секция

В любом NSIS-скрипте должна быть создана хотя бы одна секция, иначе компилятор будет ругаться, и файл установщика не создаст. В самом простом случае, когда не требуется выбор компонентов, создается единственная секция без названия:

Section
	DetailPrint "Hello, world!"	
SectionEnd


Примечание: После конструкции Section идет имя секции, заключенное в кавычки. Если кавычки пропустить, то имя секции будет некорректным, и неверно отобразится в окне выбора компонентов. Попробуйте вставить следующий код в пример, и посмотрите, как он будет работать. Ссылка на пример в конце заметки.

Section Component 4

	; Wrong section description
	SetOverwrite on
	
	DetailPrint "Component #4 installing..."
	
SectionEnd

Основные компоненты. Блокируем возможность снятия checkbox’а (галочки) пользователем

Для чего? Например, у нас есть программа, у которой, в свою очередь есть основные компоненты, например экзешник и DLL, а также дополнительные, которые можно устанавливать, а можно и не устанавливать. И если уж пользователь хочет поставить наше приложение, то он должен установить хотя бы основные компоненты.

Для того, чтобы заблокировать чекбокс, прописываем в начале секции конструкцию:

SectionIn RO

Section "Main Program and components"

	; Checkbox select and disable user changes
	SectionIn RO
	SetOverwrite on
	
	DetailPrint "Main program installing..."
	
SectionEnd

Чекбокс (галочка) выбранный по умолчанию

Для этого ничего делать не надо, только определить секцию с именем в кавычках и дефольными настройками. Пользователь может деактивировать checkbox (снять галочку) в окне выбора компонентов.

Section "Component #2"

	; Checkbox enabled by default & user will change it state
	SetOverwrite on
	
	DetailPrint "Component #2 installing..."
	
SectionEnd

Сделать, чтобы галочка была снята по умолчанию

Наверное, самый частый вопрос, задаваемый, что в рунете, что в Интернете вообще, по поводу NSIS. Так вот, ларчик открывается просто. Достаточно после Section поставить специальный флаг /o.

Пользователь также сможет активировать checkbox (галочку), если ему нужно будет установить данный компонент.

Section /o "Component #3"

	; Checkbox disabled by default & user will change it state
	SetOverwrite on
	
	DetailPrint "Component #3 installing..."
	
SectionEnd

Скрытые секции

Такие секции не будут отображены в окне выбора компонентов. Их названия пишутся без кавычек и без пробела, а перед названием ставится знак «-«. Код в таких секциях будет выполнен в любом случае, независимо от желания пользователя:

Section -HiddenSection
	DetailPrint "This is a hidden section"
	DetailPrint "This code running anyway."
SectionEnd

Специальные секции

К ним, наверное, относится одна — Uninstall, ну и еще не совсем секция, а встроенная функция, которую можно переопределить, дописав свой код, т.е. что-то типа обработчика событий в «больших» языках, например в C#

О секции Uninstall и рассказывать особо не надо, кроме ремарки, что в секции Uninstall «забываются» все значения глобальных переменных и значение переменной $INSTDIR

Исходник примера с секциями

На GitHub

Глюк (недоработка) uninstaller’а NSIS.

Или как поместить uninstaller не в директории программы.

Преамбула

Конечно, возникает вопрос «зачем?». Ну, например, мы устанавливаем некое системное приложение, драйвер, или хотим что-то пропатчить. В качестве каталога по умолчанию для такого приложения может служить, например, директория $WINDIR (обычно C:\Windows). Если мы хотим безболезненно удалить такое приложение, то нам нужно создать анинсталлер, но, совсем не обязательно чтоб он хранился в том же каталоге, что и установленное приложение. Зачем, например, загаживать каталог C:\Windows файлами uninstall.exe. Да в том то все и дело, что незачем, и вот тут мы сталкиваемся с глюками анинсталлера.

Баг uninstaller’а

В анинстайлере и инсталляторе переменная $INSTDIR определяется по-разному. В секции установки $INSTDIR можно задать, в т.ч. и автоматически, а в uninstaller’е она определяется, как директория, из которой запустился анинсталлер. Это довольно легко проиллюстрировать простым кодом, который на самом деле ничего не устанавливает, а просто предлагает пользователю выбрать каталог для установки, а сам выводит пути к каталогам и создает тестовый анинсталлер во временном каталоге. Анинсталлер тоже ничего не удаляет, просто выводит значения переменных, заданных в основном скрипте.

Любые пользовательские переменные при работе uninstall так же «забываются», вне зависимости от того, были ли они определены в секции, или в скрипте вообще.

Фрагмент кода из установщика:

; явно задаем директорию для установки
InstallDir "$PROGRAMFILES\TestUninst" 
;создаем тестовую переменную
Var TESTVAR
;[..]
;выводим окно для выбора директории для установки
DirText "Choose the folder in which to install ${APPNAME}."

Section "Inst"

	; Set Section properties
	SetOverwrite on
	
	StrCpy $TESTVAR "Test variable value"

	DetailPrint "INSTDIR (install): $INSTDIR"
	DetailPrint "TESTVAR (install): $TESTVAR"
	WriteUninstaller "$TEMP\TestUninst.exe"

SectionEnd

Секция установки:

1. Заполняем $TESTVAR
2. Выходим значение $TESTVAR
3. Выводим значение $INSTDIR
4. Создаем Uninstall’ер, причем не в $INSTDIR


В секции удаления выводим значения $INSTDIR и $TESTVAR, анинсталлер запускаем из временного каталога.

Section Uninstall
	
	SetDetailsView show
	DetailPrint "INSTDIR (uninstall): $INSTDIR"
	DetailPrint "TESTVAR (uninstall): $TESTVAR"
	
SectionEnd

Значение $INSTDIR изменилось на временный каталог, а значение $TESTVAR было потеряно. Если переместить анинсталлер в другой каталог, значение $INSTDIR опять поменяется.

Исходник, иллюстрирующий баг

Размещение uninstall в не в $INSTDIR

Из вышеизложенного напрашивается вывод, что значения $INSTDIR и нужных переменных надо сохранять на этапе установки, например в Реестр или INI-файл, а на этапе удаления восстанавливать.

В следующем примере я буду сохранять нужные значения в INI-файл, который будет располагаться в директории с uninstall.exe, а сама программа будет устанавливаться в другой каталог. Сгенерирую простой скрипт мастером скриптов и немного подправлю.

1. Меняем InstallDir на "$TEMP\TestApp"
InstallDir "$TEMP\TestApp"

2. Заводим переменную $UNINSTDIR
Var UNINSTDIR

3. В секции установки задаем ее значение, скажем "$PROGRAMFILES\TestApp"
StrCpy $UNINSTDIR "$PROGRAMFILES\TestApp"

4. Создаем каталог $UNINSTDIR
CreateDirectory "$UNINSTDIR"

5. Меняем где надо пути в создаваемых ярлыках
CreateShortCut "$SMPROGRAMS\TestApp\Uninstall.lnk" "$UNINSTDIR\uninstall.exe"

6. …путь для записи uninstall.exe
WriteUninstaller "$UNINSTDIR\uninstall.exe"

7. …и запись в Реестре для «Программ и компонентов»
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" "UninstallString" "$UNINSTDIR\uninstall.exe"

8. После создания uninstall.exe в каталоге $UNINSTDIR необходимо создать INI-файл, в который сохранить пути к каталогам $UNINSTDIR и $INSTDIR

WriteINIStr "$UNINSTDIR\dirs.ini" "dirs" "I" "$INSTDIR"
WriteINIStr "$UNINSTDIR\dirs.ini" "dirs" "U" "$UNINSTDIR"

На этом с секцией установки закончили.

В секции удаления:

1. Читаем значения из INI-файла (который для uninstall.exe будет располагаться в каталоге из переменной $INSTDIR) в перезаписываемые переменные $0 и $1. Если чтение не удалось, необходимо будет обработать ошибку (см. описания функций ReadINIStr и конструкции IfErrors в справочнике копия).

ReadINIStr $0 "$INSTDIR\dirs.ini" "dirs" "I"
IfErrors Dupa 0
ReadINIStr $1 "$INSTDIR\dirs.ini" "dirs" "U"
IfErrors Dupa 0

2. Восстанавливаем значения $INSTDIR и $UNINSTDIR:

StrCpy $INSTDIR $0
StrCpy $UNINSTDIR $1

3. Удаляем uninstall.exe, INI-файл и каталог анинсталлера:

Delete "$UNINSTDIR\uninstall.exe"
Delete "$UNINSTDIR\dirs.ini"
RMDir "$UNINSTDIR\"

4. Удаляем компоненты программы и ярлыки.

5. В конце секции заводим метку OK:

6. После удаления программы переходим в конец секции
Goto OK

7. Перед меткой OK: заводим метку Dupa:, куда попадем если произошла ошибка чтения INI-файла. После метки Dupa: будет обработчик ошибки. В данном случае выводим пользователю сообщение о том, что INI-файл некорректный, и произвести автоматическое удаление программы нельзя.

Dupa:
	MessageBox MB_ICONSTOP|MB_OK "Error reading
        $INSTDIR\dirs.ini! Uninstall aborted!"

Исходники

1. Исходник, иллюстрирующий баг uninstaller’а
2. Исходник примера

Справочник по NSIS с OSZone

Не сильно хороший, но пусть будет.

Примечание: Если открываешь CHM-файл а страницы пустые, нажимаем на файле правой кнопкой мыши, заходим в свойства и разблокируем его (там кнопка будет)

Скачать с Mega.nz
На OSZone

NSIS: определение версии и архитектуры Windows

Одна из частых задач при создании инсталлятора — определить версию и архитектуру ОС, например, чтобы в зависимости от этого подкладывать нужную версию экзешника, или устанавливать нужный драйвер.

Отображение версии Windows

Для отображения версии Windows можно воспользоваться готовым кодом из NSIS wiki. На выбор предоставлены два скрипта: только для клиентских ОС и для клиентских и серверных. Для своих целей я пользовался вторым, правда в нем есть небольшой баг — клиентская Windows XP x64 определяется как Windows Server 2003:

Создаем отдельный пустой NSIS-скрипт, копируем туда код из wiki, и сохраняем в тот же каталог, где находится основной скрипт инсталлятора под любым удобным названием, например gvv.nsi.

В основном скрипте подключаем этот файл:

!include gvv.nsi

Далее вызываем GetWindowsVersion, записываем результат в переменную (например, в предопределенную $R0) и выводим результат:

${GetWindowsVersion} $R0
DetailPrint "Windows Version: $R0"

Выполнение кода, если обнаружена определенная версия Windows

Так сказать, «точное» определение версии. Применяется, когда надо выполнить определенные действия, например, в XP и только в XP, или в семерке и только в семерке.

Для этого в комплекте есть модуль WinVer.nsh, сначала подключим его:

!include WinVer.nsh

Далее используем if и один из макросов IsWin?, например IsWinXP, IsWin7, IsWin10:

${If} ${IsWinXP}
	DetailPrint "Is Windows XP"
${EndIf}

Определенная версия и выше

Применяется, когда нужно выполнять код, если версия ОС выше нужной или нужная.

В WinVer.nsh есть другой комплект макросов — AtLeastWin? (AtLeastWin7, AtLeastWin8, AtLeastWin10) которые также нужно использовать вместе с if. Например, следующий код сработает на «семерке» и всех последующих версиях Windows. В деталях установки отобразится соответствующее сообщение:

${If} ${AtLeastWin7}
	DetailPrint "Is Windows 7 or higher"
${EndIf}

Архитектура ОС

Для определения архитектуры ОС необходимо подключить модуль x64.nsh (также поставляется вместе с компилятором NSIS):

!include x64.nsh

Поддерживается определение архитектуры x86 (IsNativeIA32), PC (AMD) x64 (IsNativeAMD64) и ARM64 (IsNativeARM64) пример кода есть в самом файле x64.nsh 🙂 Мне ARM была не нужна, поэтому пример кода на три строки короче:

${If} ${IsNativeAMD64}
    DetailPrint "System Architecture: x64"
${ElseIf} ${IsNativeIA32}
    DetailPrint "System Architecture: x86"
${Else}
    DetailPrint "Unsupported CPU architecture!"
${EndIf}

Скриншоты

Под катом

Пример целиком

На GitHub

Источники

1. Get Windows version
2. Stackoverflow

NSIS: Hello, world.

Изучение любого языка начинается с Hello, world, вот и я не буду отступать от этой традиции. Напишем простой инсталлятор, который даже не инсталлятор, потому что ничего не устанавливает, а просто пишет сообщение пользователю.

Name "HelloWorld"
OutFile "helloworld.exe"

ShowInstDetails show
RequestExecutionLevel User

Section
	DetailPrint "Hello, world!"	
SectionEnd

Объяснение кода

Сначала пишем обязательную конструкцию Name и придумываем имя установщика, тут, естественно, "HelloWorld". Имя может содержать пробелы, оно будет отображаться в заголовках окон и окнах инсталлятора, а также попадет в список программ в «Программы и компоненты», в случае реального установщика.

Далее идет обязательная конструкция OutFile — файл установщика. Если его не прописать, то компилятор не будет знать под каким именем собрать программу установки и пошлет нас.

Следующим шагом (ShowInstDetails show) развернем окно с логом установки, как это было описано в предыдущей копия заметке.

Теперь запрашиваем запуск из под пользователя с ограниченными правами (RequestExecutionLevel User). Инсталляторы обычно запускаются с правами администратора, но в данном случае мы ничего не устанавливаем, и нажимать лишний раз кнопку в окошке UAC не нужно.

Далее необходимо завести хотя бы одну секцию. Если в скрипте не будет хотя бы одной секции — компилятор прервет работу. Организуем единственную секцию без названия.

Section
	[ТУТ БУДЕТ КОД]
SectionEnd

На самом деле секции нужны для того, чтобы сделать возможным выбор компонентов устанавливаемой программы, полную или выборочную установку. Но об этом как-нибудь в другой раз.

Внутри секции пишем сообщение, выводящее заданный текст в окошко с логом (деталями установки):

DetailPrint "Hello, world!"

О редакторах кода для NSIS

На самом деле выбора не так, чтобы много, но какой-никакой есть. На официальном сайте предлагают пользоваться Eclipse и плагином к нему, но у меня на Клипсу, wim и TeX стойкая аллергия еще со студенческих времен.
Есть плагин для Visual Studio, но написан он китайцами для китайцев, так что нафиг. Впрочем, может есть и нормальные плагины, но я не нашел. Сам пользуюсь пожилой средой Venis IX, там есть мастер для генерации простых установщиков, которого вполне хватает, чтоб постоянно не писать одно и то же, создавая новый инсталлятор.

Исходник

На GitHub

NSIS: Открыть окошко с логом установки (Show details) по умолчанию

Если ничего не делать, детали установки по умолчанию скрыты:

Чтобы лог установки сразу был виден пользователю, необходимо применить один из следующих методов:

1. В коде скрипта, до секций прописываем команду:

ShowInstDetails show

Окошко с логом (деталями установки) будет открыто

2. Прописать внутри секции команду:

SetDetailsView show

Это может быть полезно, например, если вы хотите, чтобы детали показывались и при удалении. В таком случае, вышеуказанная команда добавляется в начало секции, описывающей анинсталлер:

Section Uninstall
	
	SetDetailsView show
	
	[..]
	[..]

SectionEnd

Переделал автоматический инсталлятор для Devcon. Или куча ворчания.

Делал, чтоб клиенту меньше ручками работать. Теперь не надо самому определять версию винды, и вытаскивать нужный devcon из архива соответственно.

Предыдущий я сделал на Inno Setup, но Inno Setup оказался той еще заразой, нихрена он версию Винды толком определить не мог, и тихой сапой подкладывал во все винды один и тот же devcon, который подходит для Windows 7 и выше. От чего у клиента с виндой XP x64 произошел адский баттхерт, так, что забрызгало пол-Петербурга, аж до моей деревни брызги долетели. Видимо, клиент меня проклял, потому что мне на ногу упал топор (слава Ктулху обухом), у меня упали форточки, точнее форточка, но стеклянная и на пол, ко мне какого-то черта приперлась лесная охрана и стала [РОСКОМНАДЗОР], и я после этого [РОСКОМНАДЗОР] в результате, теперь страдаю [МИНЗДРАВ].

Новый сделал на NSIS, но как оказалось — NSIS тоже [РОСКОМНАДЗОР], точнее, не сам NSIS, он мне как раз понравился, за исключением одного странного недокументированного глюка, о котором как-нибудь расскажу, и скрипт-язык, вполне себе такая помесь Бейсика с Башем. А вот документация, это, товарищи, лютый [РОСКОМНАДЗОР] в последней стадии.

Русский мануал к популярному инсталл-мэйкеру писали сто лет назад какие-то [ДАННЫЕ УДАЛЕНЫ] с OSZone.net, еще и назвали свое творение «справочником», хотя что-что а справочники так не делают. Просто плохо структурированные статьи, которые на [РОСКОМНАДЗОР] запихнули в chm-файл. [РОСКОМНАДЗОРКА], поучитесь у того же Исзелиона с wasm.ru, как нужно делать справочники и мануалы. На туториале Исзелиона сотня-другая студентов если не аспирантами стали, так свой зачет по ассемблеру точно получили. Аффтар еще и жалуется на форуме, типа, «ну на[РОСКОМНАДЗОР] обновлять мануал, если его никто не читает, и очевидные вопросы задает». Так [РОСКОМНАДЗОРКА], [РОСКОМНАДЗОРКА], ты так написал этот мануал, что кошка [РОСКОМНАДЗОРОМ] по клавиатуре лучше напишет. Но ладно, я много захотел, русского мануала. Да, согласен. Но теперь «поговорим о влажности в номере», точнее об оригинальной вики от разработчиков.

Оригинальная вики тоже [РОСКОМНАДЗОРНЫЙ], [РОСКОМНАДЗОР] твою мать, [РОСКОМНАДЗОР]. Причем, я вполне себе уверен, что там есть все описания функций языка NSIS, вот только отдельной категории для списка функций нет. Функции потом ищутся гуглем, который выводит на страницу-сироту с вполне нормальным описанием, но [РОСКОМНАДЗОР] знает, почему на эту страницу нельзя попасть из самой вики. В навигации [РОСКОМНАДЗОР] не валялся, в общем без стакана не разобраться. А еще пишут, что «хорошо документированный продукт». Документирован он может и хорошо, но найти в документации что-то конкретное, это как в ГОРФ пойти к чиновнику: «вы мне справку ЕПРСТ-1488 принесли, а надо было ЖПЧШЦ-666», и [РОСКОМНАДЗОР] знает где это написано.

Даже есть мысль как-нибудь выкачать эту вики себе на комп, сделать нормально, попытаться перевести и выпустить в виде справочника.

Впрочем, инсталлер сделал:

Скачать с Mega.nz
С GitHub
Исходник (файлы devcon_all.nsi и gvv.nsi)