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

No Gravatar

Мабуть, немає таких людей в середовищі 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” рядка та невеличкої документації щодо використання самого скрипта:

#!/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;

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