2

Как я делал инсталлятор

Ура! Наконец и у меня появилось время о чем-то написать :) В течении всего лета было не до компьютера: лето, рыбалка, отпуск, то одна дача, то другая (только уже не дача, а сад) :))) А после отпуска еще и на работе работать заставили. :))))

Недавно по работе пришлось делать вещи, о которых я раньше имел весьма смутное представление:
1) Создание дистрибутива для моей java-программы.
2) Создание парсера (программы - синтаксического анализатора)

Про первое попытаюсь написать счас, а про второе - в ближайшее время.
Итак, создаем дистрибутив. С чего начать? Имхо, самому писать такую прогу в наше время - уже моветон :) Посему надо бы поискать что-нить наподобие Install Maker или Setup Creator, желательно бесплатное. Что я и сделал. Не скажу, что быстро нашел то, что меня устроило на 100%. Необходима была поддержка Win98, что в наше время уже редкость, тем более за бесплатно :). Конечно, скачать найти коммерческую программу, потом лекарство к ней, превращающее ее из коммерческой в бесплатную :) Но не люблю я бегать по аптекам :) Наконец нашел - бесплатную, с малюсеньким дистрибутивом и вполне адекватно работающую - Inno Setup. Самое главное - создаваемый им дистрибутив запускался и распаковывался на любой системе - от win 98 до XP.

Как выяснилось, Inno Setup представляет собой компилятор скриптов + мастер, облегчающий их написание. Если для вашего приложения не нужно проверять какие-то условия и в зависимости от них устанавливать какие-то дополнительные пакеты, то можно обойтись и мастером.



В моем случае пришлось несколько углубиться в изучение возможностей Inno Setup и дорабатывать напильником исходную болванку, созданную мастером. Причина этого в том, для что Java-программы мне надо:
-проверить наличие на компьютере JRE версии 1.5 или выше
-если нет таковой - распаковать и запустить установку JRE, заключенной в моем дистрибутиве
-создать ярлык, в котором будет прописан путь к установленной JRE.
Вот такой скрипт у меня в конце концов получился:
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{B0393573-FE63-436B-961A-B738915666EC}
AppName=Carbonatometr
AppVerName=Carbonatometr v.1.0
AppPublisher=OOO "TNG-Kazangeophysics"
DefaultDirName={pf}\Carbonatometr
DefaultGroupName=Carbonatometr
AllowNoIcons=yes
OutputDir=c:\java\Projects\carbometer\distr
SourceDir=c:\java\Projects\carbometer\distr
OutputBaseFilename=csetup
Compression=lzma
SolidCompression=yes
;HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment
;HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\CurrentVersion
;HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\{CurrentVersion}\JavaHome
;ключ запуска установки jre: /qb

[Languages]
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[Files]
Source: "c:\downloads\develop\java\j2re-1_5_0_13.exe"; DestDir:"{tmp}"; check: noJreCheck; BeforeInstall: JreInstallMessage; AfterInstall: JreInstall(ExpandConstant('{tmp}'), 'j2re-1_5_0_13.exe'); Flags: deleteafterinstall ignoreversion
Source: "rxtxSerial.dll"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "carbometer.pref"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "english.lng"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "jgoodies.jar"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "russian.lng"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "RXTXcomm.jar"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "carbometer.jar"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
Source: "carbometer.ico"; DestDir: "{app}"; check: JreCheck; Flags: ignoreversion
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Icons]
Name: "{group}\Carbonatometr"; Filename: "{code:GetJREPath}\bin\javaw"; parameters: "-jar carbometer.jar"; WorkingDir: "{app}"; IconFilename: "{app}\carbometer.ico"
Name: "{group}\{cm:UninstallProgram,Carbonatometr}"; Filename: "{uninstallexe}"
Name: "{commondesktop}\Carbonatometr"; Filename: "{code:GetJREPath}\bin\javaw"; parameters: "-jar carbometer.jar"; WorkingDir: "{app}"; Tasks: desktopicon; IconFilename: "{app}\carbometer.ico"
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\Carbonatometr"; Filename: "{code:GetJREPath}\bin\javaw"; parameters: "-jar carbometer.jar"; WorkingDir: "{app}"; Tasks: quicklaunchicon; IconFilename: "{app}\carbometer.ico"


[Code]
//importing a Windows API function
function MessageBox(hWnd: Integer; lpText, lpCaption: AnsiString; uType: Cardinal): Integer;
external 'MessageBoxA@user32.dll stdcall';
const
JRE_ROOT_KEY = 'SOFTWARE\JavaSoft\Java Runtime Environment';
VERSION_KEY = 'CurrentVersion';
JRE_VERSION = '1.5';
JAVA_HOME = 'JavaHome';
{Получение пути к JRE}
function GetCurrentJREpath( var path : string) : boolean;
var version : string;
begin
result := RegQueryStringValue(HKEY_LOCAL_MACHINE, JRE_ROOT_KEY, VERSION_KEY, version);
{Проверка - есть ли данный ключ в реестре}
if result and (CompareStr(version, JRE_VERSION) >= 0) then
result := RegQueryStringValue(HKEY_LOCAL_MACHINE, JRE_ROOT_KEY + '\' + version, JAVA_HOME, path)
else
result := false;
end;
{Функция проверки наличия JRE}
function JreCheck : boolean;
var version: String;
begin
result := GetCurrentJREpath(version);
end;
{Функция проверки отсутствия JRE}
function noJreCheck : boolean;
var version: String;
begin
result := not GetCurrentJREpath(version);
end;

{Функция отображения сообщения об установке JRE}
procedure JreInstallMessage;
begin
MessageBox(StrToInt(ExpandConstant('{wizardhwnd}')),
'Для работы программы Carbonatometr необходимо установить JRE версии 1.5 или старше.' +
'Нажмите OK для установки JRE 1.5 update 13',
'Carbonatometr setup',
MB_OK);
end;
{Процедура установки JRE}
procedure JreInstall(jre_location, jre_file : String);
var
path: String;
code: integer;
begin
code := -1;
if not JreCheck then
begin {ключа нет или версия не подходит}
path := jre_location + '\' + jre_file;
if not exec(path, '/qb', '', SW_SHOW, ewWaitUntilTerminated, code) then
begin
MessageBox(StrToInt(ExpandConstant('{wizardhwnd}')),
'Не удалось запустить установку Java Runtime Environment. Программа Carbonatometr не установлена.' +
' Строка запуска = ' + path,
'Carbonatometr setup',
MB_OK);
SysErrorMessage(code);
end
else if code <> 0 then
MessageBox(StrToInt(ExpandConstant('{wizardhwnd}')),
'Не удалось установить Java Runtime Environment. Программа Carbonatometr не установлена. Код ошибки: ' +
IntToStr(code),
'Carbonatometr setup',
MB_OK);
end;
end;

function GetJREPath(param : String) : String;
var path: string;
begin
path := 'C:\';
GetCurrentJREpath(path);
result := path;
end;

Видно, что скрипт представляет собой подобие ini-файла. Попробую прокомментировать некоторые его разделы.
В разделе Setup располагаются различные опции установщика, там же задается название нашего приложения. По-моему весь этот раздел создан у меня мастером, и я его даже не редактировал.
В разделе [Tasks] записываются различные задания для установщика, такие как создание групп, создание иконок на рабочем столе и в панели быстрого запуска. При запуске установщик спросит, хочет ли пользователь создавать группы, иконки и т.п. Впоследствии эти задания привязываются к конкретным ярлыкам в разделе [icons]. То есть в моем случае программа установки предложит пользователю создать иконки на рабочем столе и панели быстрого запуска. Какие конкретно иконки положить в эти места, мы укажем в разделе Icons.
Дальше идет основной раздел - Files. Тут перечислены файлы, которые будут помещены в дистрибутив. Я в свой дистрибутив запихнул дистрибутив JRE, который будет распаковываться и устанавливаться (в случае необходимости) перед остальными файлами.
Выражение
DestDir:"{tmp}";

говорит, куда файл должен быть помещен при установке. В данном случае файл будет установлен в "{tmp}" - временнную директорию, создаваемую установщиком в директории windows\temp.
Далее идет выражение
check: noJreCheck;

Это выражение проверяет, можно ли устанавливать файл, к которому оно относится или нет. После ключевого слова check должно идти двоеточие и произвольное имя функции, возвращающей булево значение. Если функция возвращает true, то файл будет установлен, в противном случае - не будет. Тело этой функции должно быть определено в разделе Code, который я прокомментирую ниже. В моем случае эта функция называется noJreCheck и возвращает true, если JRE нужной версии не обнаружена.
Следующее выражение:
BeforeInstall: JreInstallMessage;

означает, что перед установкой данного файла будет выполнена функция JreInstallMessage. Название функции произвольно, она (естественно) должна размещаться в разделе code. У этой функции может быть несколько аргументов целого, булевского или строковых типов. Эта функция не должна возвращать значение (т.е. это должна быть процедура). В моем случае эта функция просто выводит сообщение о том, что счас будет произведена установка JRE. После этого установщик "установит" файл (речь до сих пор идет о файле-дистрибутиве JRE) в папку {tmp}. Теперь JRE надо установить уже "по-нормальному", то есть запустить сам дистрибутив. Для этого мы укажем функцию, которая будет выполняться "после установки":
AfterInstall: JreInstall(ExpandConstant('{tmp}'), 'j2re-1_5_0_13.exe');

Процедура JreInstall, так же описанная в разделе [Code], получает в качестве параметров значение константы {tmp} и имя исполняемого файла. Дополнительный вызов ExpandConstant('{tmp}') необходим, чтобы передать в JreInstall значение константы '{tmp}', а не ее имя.
В самом конце строки, относящейся к первому файлу нашего дистрибутива стоит выражение
Flags: deleteafterinstall ignoreversion

Первый флаг (deleteafterinstall) означает, что после завершения установки данный файл будет удален. Имхо, пользователю не нужен файл размером 17 МБ, ботающийся где-то в недрах папки Windows и никак не использующийся. Второй (ignoreversion) говорит, что нужно просто заменять существующие файлы на новые, не сравнивая их версии.
Остальные файлы в моем дистрибутиве не требуют столь хитрого обращения, как дистрибутив JRE и просто копируются в папку '{app}', то есть туда, куда пользователь захочет установить эту программу.

Следующий раздел - [Icons] я уже упоминал выше.
Этот раздел фактически управляет созданием ярыков. Мне нужно было создать ярлык, позволяющий запускать мою java-прогу и обеспечивающий ее нормальную работу. Командная строка запуска у меня должна быть следующей:
{JRE_HOME}\bin\javaw -jar {app}\carbometer.jar

Рабочую папку ('{app}') тоже надо указать.
Я опишу третью строку [Icons], поскольку в ней больше всего параметров:
Name: "{commondesktop}\Carbonatometr"; Filename: "{code:GetJREPath}\bin\javaw"; parameters: "-jar carbometer.jar"; WorkingDir: "{app}"; Tasks: desktopicon; IconFilename: "{app}\carbometer.ico"

Она описывает ярлык с названием Carbonatometr, который надо разместить на рабочем столе; Строка запуска определяется двумя параметрами - Filename и Parameters. Первый - это имя исполняемого файла, а второй - параметры командной строки. Для того, что бы выяснить, куда же установилась у нас JRE, я вызываю функцию GetJREPath, которую сам же и напишу в разделе [Code]. Параметр Tasks: desktopicon; связывает данный ярлык с разделом [Tasks]. Таким образом, если при установке пользователь поставит галочку у checkbox-а "Создать значок на Рабочем столе?"(а такой checkbox будет, поскольку в разделе [tasks] я попросил его создать), то ярлык, описанной в этой строке раздела [icons] появится на рабочем столе. Параметры WorkingDir и IconFilename означают соответственно рабочую директорию и имя файла значка для ярлыка.
Наконец-то я добрался до раздела [code]!
Этот раздел оказался самым легким в написании :) Пишется он на языке Pascal, который если не всем известен, то во всяком случае, его азы (а бльшего и не надо) легко освоить за пару часов.
Замечу, что InnoSetup поддерживает огромное число внутренних функций, и вряд ли придется что-то экспортировать извне, однако при желании это можно сделать. Я залез в один пример, и подглядел, как можно импортировать функцию API MessageBox:
function MessageBox(hWnd: Integer; lpText, lpCaption: AnsiString; uType: Cardinal): Integer;
external 'MessageBoxA@user32.dll stdcall';
Мои отрывочные знания языка Pascal не позволяют утверждать, насколько сильно он отличается от внутреннего языка InnoSetup. Так, я не смог объявить внутри процедуры локальную константу - пришлось объявить ее глобально. Еще одно отличие этого языка от того Pascal, что я учил в школе - это способ, которым функции возвращают значение. В том Pascal-е надо было присвоить возвращаемое значение имени функции. Здесь это вызывает ошибку. Возвращаемое значение должно присваиваться специальной переменной result.
Главная, для чего мне был нужен раздел [code] - это проверить наличие установленной JRE и, при ее отсутствии - установить ее.
Проверку наличия JRE осуществляет функция GetJREPath:
{Получение пути к JRE}
function GetCurrentJREpath( var path : string) : boolean;
var version : string;
begin
result := RegQueryStringValue(HKEY_LOCAL_MACHINE, JRE_ROOT_KEY, VERSION_KEY, version);
{Проверка - есть ли данный ключ в реестре}
if result and (CompareStr(version, JRE_VERSION) >= 0) then
result := RegQueryStringValue(HKEY_LOCAL_MACHINE, JRE_ROOT_KEY + '\' + version, JAVA_HOME, path)
else
result := false;
end;

Здесь RegQueryStringValue - встроенная функция Innosetup. Она запрашивает указанный ключ реестра, имеющий строковое значение, и если таковой существует, сохраняет его в переменной version и возвращает true. Если указанный ключ не существует, то переменная version не изменит своего значения, а RegQueryStringValue возвратит false.
В данном случае для проверки наличия нужной JRE, я запрашиваю значение ключа HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Java Runtime Environment\CurrentVersion.
Процедура, выполняющая запуск установки JRE:
{Процедура установки JRE}
procedure JreInstall(jre_location, jre_file : String);
var
path: String;
code: integer;
begin
code := -1;
if not JreCheck then
begin {ключа нет или версия не подходит}
path := jre_location + '\' + jre_file;
if not exec(path, '/qb', '', SW_SHOW, ewWaitUntilTerminated, code) then
begin
MessageBox(StrToInt(ExpandConstant('{wizardhwnd}')),
'Не удалось запустить установку Java Runtime Environment. Программа Carbonatometr не установлена.' +
' Строка запуска = ' + path,
'Carbonatometr setup',
MB_OK);
SysErrorMessage(code);
end
else if code <> 0 then
MessageBox(StrToInt(ExpandConstant('{wizardhwnd}')),
'Не удалось установить Java Runtime Environment. Программа Carbonatometr не установлена. Код ошибки: ' +
IntToStr(code),
'Carbonatometr setup',
MB_OK);
end;
end;

Большая часть тела этой процедуры нужна для обработки ошибок. Функция exec, вызываемая для запуска установщика JRE, является внутренней ф-ей InnoSetup, ее не надо импортировать. Первый параметр у нее - запускаемый файл, второй - параметры командной строки, третий - рабочая директория. Четвертый говорит о том, что надо отображать окно вызываемой программы. Пятый параметр - константа ewWaitUntilTerminated указывает, что функция будет ожидать завершения запущенной программы, а в последнем параметре возвратит код завершения процесса...
Короче, если все ОК, то code будет равно 0, а exec возвратит true.
Еще, кто заметил - я через командную строку передал инсталлятору JRE параметр '/qb'. Инсталлятор JRE, запущенный с этим параметром не будет ничего спрашивать, а тихо поставит JRE в директорию, предусмотренную по умолчанию. Кому интересно - про опции командной строки Microsoft Windows Installer можно почитать тут.

2 коммент.:

Анонимный комментирует...

Спасибо за статью! Все понятно и просто, очень пригодилась!

Анонимный комментирует...

Большое спасибо! )

Отправить комментарий