Пишемо інсталятори в Linux

Мабуть, немає таких людей в середовищі Linux-користувачів, яким би не доводилося ставити закрите або комерційне програмне забезпечення. Не раз доводилось бачити “рідні” для цього виду програм інсталятори: графічні чи консольні, але завжди своєрідні. І поширюється програма разом з інсталятором в одному сценарії для командного інтерпретатора. А чи може хто-небудь написати власну програму-інсталятор, налаштувати її під себе? . Сьогодні, шановний читачу, ми навчимося писати інсталятори для UNIX.

Однак, перед тим, як продовжити, слід усвідомити наступне:

Цей приклад описаний виключно в навчальних цілях! Використовуйте його лиш у випадках крайньої необхідності!

Якщо Ви хочете поширювати власну програму для Linux — використовуйте стандартні способи поширення програмного забезпечення у відповідних дистрибутивах. Користувачі будуть Вам дуже вдячні.

Інсталятор зсередини

Важливо зрозуміти, яким чином працює більшість інсталяторів. Типова програма для встановлення певного програмного забезпечення складається з основних двох частин: архіву з файлами цього програмного забезпечення та сценаріїв установки та видалення. Відповідно, і процес установки теж складається з двох етапів — встановлення відповідних файлів та налаштування робочого середовища. Процес видалення програмного забезпечення виконується в зворотньому порядку. Для реалізації цих підходів необхідно розробити певну структуру каталогів, наприклад, як на малюнку:

Структура пакету

Структура директорій пакету

З малюнку можна зрозуміти, що файли, які несуть службову інформацію щодо пакету, виконують ініціалізацію та очистку середовища, знаходяться в директорії package. Всі інші директорії і є файлами самого програмного забезпечення. Надалі будемо опиратись на вище вказану структуру каталогів, і саме так буде виглядати свіжевстановлена нами програма.

Після того, як програма-інсталятор розпакувала свій вміст в задану директорію, вона запускає ініціалізаційний сценарій, який в нашому випадку зватиметься package/install.sh (тут і надалі всі імена файлів вказуватимуться відповідно до кореневої директорії встановленого проекту). В ході роботи встановлена нами програма може змінювати змінні середовища, створювати додаткові файли тощо. Всіма цими клопотами має займатись сценарій видалення package/uninstall.sh. А для того, щоб він знав, які файли були встановлені, на етапі установки буде створений список файлів package/list. Ось такий собі невеличкий огляд циклу життя програми: від установки і до видалення.

Перевівши погляд на інсталятори, які складаються з всього лиш одного сценарію командної оболонки, завжди дивуєшся, як можна покласти архів, який по суті є бінарним форматом, всередину текстового, яким є наш інсталятор. Все виявилося досить простим: достатньо використати base64- або UU-перетворення, і ось, наш архів записаний самими лише цифро-буквеними символами, або ж з доданням спеціальних символів. Не можна забувати і про зворотній бік цього перетворення – збільшення розміру файлу, так що пошук оптимального варіанту перетворення ще не закритий.

Від теорії до практики

Тепер, коли цілі і методи нам відомі, пора б зайнятись власне інсталятором. Перекодуванням файлів в base64, як і в UU-code, займається утиліта uuencode. ЇЇ аналог для зворотнього перетворення — uudecode. Отже, для того, щоб отримати перекодований варіант архіву, створеного з поточної директорії, достатньо виконати команду:

 $ tar -c . | bzip2 -z | uuencode - > decoded_archive

Тепер, коли ми знаємо, як записати архів всередині текстового файлу, достатньо обрамити його відповідними керуючими структурами, щоб отримати готовий інсталятор. Слід зауважити одну цікаву технічну особливість. Як відомо, в тій самій перекодованій формі base64 або UU-code пересилаються поштою файли в двійковому форматі. Коли декодер uudecode читає файл, він відкидає все, що не стосується закодованих даних. Отже, ми просто згодуємо весь наш інсталятор декодеру, уникнувши при цьому попереднє повне завантаження архіву в оперативну пам’ять. Але генерувати подібні сценарії “вручну” — справа не з приємних. Тому в якості подарунку тут буде запропонований варіант генератора подібних інсталяторів. Він здатен:

  1. Працювати в інтерактивному режимі
  2. Генерувати типові інсталяційні та деінсталяційні скрипти, або використовувати вже готові, якщо такі існують.

Отже, почнемо його розбір. Як і кожен сценарій, наш починається з визначення інтерпретатора, або так званого “sha-bang” рядка та невеличкої документації щодо використання самого скрипта:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
#
# package.sh - tool for creating self-extracting and self-installing packages.
#
# SYNOPSIS
#
# package.sh DIR [NAME]
#       creates a package from directory DIR with name NAME
#
# package.sh uses tar, uuencode and gzip to generate a package. It generates
# installing and uninstalling scripts in DIR/package directory to provide
# additional flexibility for users. These files (install.sh and uninstall.sh)
# are not generated if exist.

Для зручності кожна окрема дія буде виділена в функцію. Оскільки всі службові сценарії лежатимуть в наперед відомій директорії package, то з них і почнемо:

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
generate_install_script() {
cat < < EOF
#!/bin/bash
# This is a simple installer script generated by package.sh - simple self-extracting
# and self-installing package creator
 
if [ ! -d "\$PWD/package" ]; then
        echo "This script must be run from the directory where package is installed"
        exit 1
fi
 
# TODO: Write your installation operations here.
 
EOF
}

Сценарій видалення є дещо цікавішим. Оскільки список встановлених файлів знаходиться в package/list, то необхідно пройти весь список, вилучаючи вказані файли з системи. Щоб мати можливість вилучити не лише файли, а й директорії, при цьому залишаючи ті, які містять файли, що не входять до пакету, слід пройти цей список знизу вверх, використовуючи команду rmdir для директорій.

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
generate_uninstall_script() {
cat < < EOF
#!/bin/bash
# This is a simple uninstaller script generated by package.sh - simple self-extracting
# and self-installing package creator
 
if [ ! -d "\$PWD/package" ]; then
        echo "This script must be run from the directory where package is installed"
        exit 1
fi
 
tac package/list | while read FILE
        do
        if [ -d "\$FILE" ] ; then
                rmdir \$FILE 2>/dev/null
        else
                rm \$FILE
        fi
        done
 
rmdir package
 
exit 0;
EOF
}

Тепер черга за основним скриптом інсталятора. Саме на нього покладається основна задача видобування файлів з архіву, генерації списку файлів та первинної ініціалізації нашого пакету, через це його будова містить додаткові перевірки вхідних аргументів, виведення помилок в роботі до stderr тощо. окрім того, в разі відсутності будь-яких вхідних аргументів сценарій дозволяє в інтерактивному режимі вказати цільову директорію. Нижче наведений код для тіла інсталятора:

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package_installer() {
cat < < EOF
#!/bin/bash
#
# Package installation script.
if [ -n "\$1" ]
then
        if [ -d "\$1" ]
        then
                TARGETDIR="\$1"
        else
                echo "Directory does not exist" > /dev/stderr
                exit 1;
        fi
else
        echo -n "Please, specify target directory[default is \$PWD]: "
        read TARGETDIR
 
        if [ -n "." ]
        then
                TARGETDIR="."
        else
                while [ ! -d "\$TARGETDR" ]
                do
                        echo "Directory does not exist." > /dev/stderr
                        echo -n "Please, specify target directory"
                        read TARGETDIR
                done
        fi
fi
 
FILELIST=\`mktemp\`
echo -n "Extracting archive contents. This may take a while.."
cat \$0 | uudecode | bzip2 -dc | tar -xvkC \$TARGETDIR > \$FILELIST
 
echo "\$TARGETDIR/package/list" > \$TARGETDIR/package/list
cat \$FILELIST >> \$TARGETDIR/package/list
rm \$FILELIST
 
echo -ne "Done.\\nRunning installation script.."
cd \$TARGETDIR
package/install.sh
 
echo -ne "Done.\nDo you want to remove the installer?(y/N) "
read ANSWER
 
if [ "\$ANSWER" == "y" ]; then
        rm \$0;
fi
 
echo "Done. Enjoy!"
exit 0;
 
EOF
}

Ну ось, базові три частини, необхідні для інсталяційного пакета, готові. Тепер черга за генератором. Він в свою чергу повинен створити інсталяційні та деінсталяційні скрипти, якщо необхідно, заархівувати цільову директорію разом зі службовими скриптами і об’єднати код інсталятора з закодованим тілом архіву. При цьому слід передбачити необхідні дії у випадку виключних ситуацій, як, наприклад створення файлу пакету з вже існуючим іменем. В результаті тіло генератора матиме вигляд:

109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# Generator main part begins here
DIR=$PWD
 
if [ -n "$1" ]
then
        if [ -d "$1" ]
        then
                TARGETDIR="$1"
        else
                echo "Directory does not exist" > /dev/stderr
                exit 1;
        fi
else
        echo -n "Please, specify target directory[default is $PWD]: "
        read TARGETDIR
 
        if [ ! -n "$TARGETDIR" ]
        then
                TARGETDIR="."
        else
                while [ ! -d "$TARGETDR" ]
                do
                        echo "Directory does not exist." > /dev/stderr
                        echo -n "Please, specify target directory"
                        read TARGETDIR
                done
        fi
fi
 
# Creating scripts directory if not exist
if [ ! -d "$TARGETDIR/package" ]; then
        mkdir $TARGETDIR/package
fi
 
 
# Generating installation script if not specified by user.
INSTALLSCRIPT="$TARGETDIR/package/install.sh"
if [ ! -f "$TARGETDIR/package/install.sh" ]; then
        echo -n "Generating installer script.."
        generate_install_script > $INSTALLSCRIPT
        chmod a+x $INSTALLSCRIPT
        echo "Done."
fi
 
 
# Generating uninstallation script if not specified by user.
UNINSTALLSCRIPT="$TARGETDIR/package/uninstall.sh"
if [ ! -f "$UNINSTALLSCRIPT" ]; then
        echo -n "Generating uninstaller script.."
        generate_uninstall_script > $UNINSTALLSCRIPT
        chmod a+x $UNINSTALLSCRIPT
        echo "Done."
fi
 
echo -n "Compressing data. This may take a while.."
DATA=`mktemp`
cd $TARGETDIR
tar -cf -  . | bzip2 -9z | uuencode - > $DATA
cd $DIR
 
#Generating the final package and naming it as user desires.
echo -ne "Done.\nGenerating package.."
PACKAGE=`mktemp -p . package.sh.XXXXXXX`
package_installer > $PACKAGE
cat $DATA >> $PACKAGE
 
echo -ne "Done.\nRemoving temporary files.."
rm $DATA
 
PACKAGENAME=""
# Setting up the package name
if [ -n "$2" ]; then
        PACKAGENAME=$2
else
        PACKAGENAME="`basename $TARGETDIR`.sh"
fi
 
# If the same file is aready exist - ask user
if [ -f "$PACKAGENAME" ]; then
        echo -ne "\nFile $PACKAGENAME already exists. Overwrite?(y/N) "
        read ANSWER
        if [ "$ANSWER" != "y" ]; then
                exit 1;
        fi
else
        mv "$PACKAGE" "$PACKAGENAME"
        PACKAGE=$PACKAGENAME
fi
 
chmod a+x $PACKAGE
 
echo -e "Done.\nEnjoy!"
exit 0;

Ось генератор і готовий. І хоча він досить примітивний, однак на живому прикладі дозволяє побачити деякі з можливостей всім відомого командного інтерпретатора. Наостанок лиш хочу всіх поздоровити з минулими святами, і нехай цей рік принесе радість пізнання невідомого та успіх!