Мабуть, немає таких людей в середовищі 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 читає файл, він відкидає все, що не стосується закодованих даних. Отже, ми просто згодуємо весь наш інсталятор декодеру, уникнувши при цьому попереднє повне завантаження архіву в оперативну пам’ять. Але генерувати подібні сценарії “вручну” — справа не з приємних. Тому в якості подарунку тут буде запропонований варіант генератора подібних інсталяторів. Він здатен:
- Працювати в інтерактивному режимі
- Генерувати типові інсталяційні та деінсталяційні скрипти, або використовувати вже готові, якщо такі існують.
Отже, почнемо його розбір. Як і кожен сценарій, наш починається з визначення інтерпретатора, або так званого “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; |
Ось генератор і готовий. І хоча він досить примітивний, однак на живому прикладі дозволяє побачити деякі з можливостей всім відомого командного інтерпретатора. Наостанок лиш хочу всіх поздоровити з минулими святами, і нехай цей рік принесе радість пізнання невідомого та успіх!