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