Dotfiles, część 1: config-manage

Zarządzanie konfiguracją komputerów jest skomplikowane, a sprawa dodatkowo nabiera kolorytu wraz ze wzrostem liczby obsługiwanych komputerów osobistych, włączając w to telefony komórkowe. Każdy z nich trochę się różni: a to wersją systemu operacyjnego, a to ilością podpiętych monitorów, a to bebechami. System operacyjny wiele rzeczy przykrywa pod warstewką spójnego interfejsu, jednak część różnic konfiguracja użytkownika musi uwzględniać.

Prostota przystosowania konfiguracji do wielu urządzeń to nie jedyna z pożądanych cech systemu służącego do zarządzani zbiorem konfiguracji. Innymi cechami są łatwość instalacji/usunięcia pewnych części konfiguracji (np. usunięcia plików konfiguracyjnych dla programu, którego nie będziemy więcej używać), a także prostota definiowania zasad opisujących sam proces instalacji – skrypty sh tudzież basha, poza tymi najbardziej trywialnymi, mają to do siebie, że szybko stają się nieutrzymywalnymi koszmarkami: wodospadami wywołań komend przekazywanych do grepa, seda i awka wywoływanych z niezrozumiałymi wyrażeniami regularnymi, wyrytych na stałe w spaghetti if-elsów i swich-case’ów.

Nieco historii

Przez ostatnie 4 lata tworzyłem (nadal tworzę) system umożliwiający mi zarządzanie konfiguracjami w prosty i czytelny sposób. Celowo używam słowa “konfiguracjami”, a nie “plikami konfiguracjami”, gdyż konfiguracja jest pojęciem szerszym. Owszem, są to również pliki tekstowe zawierające opis najczęściej używanych opcji różnych programów, lecz oprócz tego są to na przykład reguły instalacji oprogramowania, komendy vima (vim -c), czy też komendy kompilujące i patchujące kod źródłowy. “Konfiguracja” to w tym kontekście ujęcie całościowe sposobu działania komputera dla danego użytkownika.

“Framework”, którego dziś używam, przez ostatnie lata ewoluował i w niczym nie przypomina kilku swoich poprzednich wersji. Część zmian zresztą nie ostała się nawet w historii gita; mam za sobą dość długi epizod tworzenia brancha dla każdej maszyny z osobna i cherry-pickowania między nimi części wspólnych (koszmar), który zakończył się usunięciem wzmiankowanych branchy.

W pierwszej wersji ów system był właśnie wspomnianym zestawem skryptów shellowych – każdy we własnym katalogu – zarządzanych przez skrypt centralny oraz “bibliotekę” wspólnych funkcji (np. do patchowania kodu, wyświetlania diffów, czy interaktywnego menu wyświetlającego się od wielkiego dzwonu). Każdy skrypt instalacyjny posiadał zatem obowiązkowe dwie pierwsze linijki: określenie swojej ścieżki bezwzględnej i source’owanie owej nieszczęsnej “biblioteki”:

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
. "$DIR/../libconfig"

Następnie następowała seria wywołań magicznych funkcji, które miały za zadanie “bezpiecznie” kopiować i usuwać pliki, zmieniać ich nazwy, tworzyć linki symboliczne itp. Ciekawe było, że część funkcji tworzyła linki symboliczne, podczas gdy inna część tworzyła zwykłe kopie plików, przez co zmiany nie były natychmiastowo odzwierciedlane w repozytorium.

System ten istniał sobie dość długo, bo blisko 3 lata. Przez ten czas ewoluowała głównie biblioteka. Funkcja usuwająca pliki stała się bezpieczniejsza po tym jak przez przypadek usunęła ich zbyt wiele. Funkcja tworząca linki symboliczne nie zawsze chciała je tworzyć. Dopisane zostało wykrywanie już zainstalowanych plików konfiguracyjnych i interaktywne potwierdzanie ich nadpisywania. Takie buty.

Potem zaś nadeszła rewolucja – spowodowana głównie niemożliwością utrzymania różnych konfiguracji dla różnych komputerów.

Operacja na otwartym sercu

Wszystkie skrypty, łącznie z łamagowatą biblioteką, zostały wysadzone w puch, a ich miejsce zajął tandem config-manage1 i GNU Stow2. Skrypt config-manage ma za zadanie wykonywać polecenia zapisane w pliku konfiguracyjnym za pomocą prostego do zrozumienia i edycji formatu ini. GNU Stow jest natomiast managerem dowiązań symbolicznych - to on wykonuje lwią część pracy polegającej na odpowiednim ulokowaniu plików konfiguracyjnych.

Repozytorium nadal jest podzielone na katalogi odpowiadające instalowalnym modułom. Na przykład może ono wyglądać następująco:

$HOME/config/
    home/
        .local/bin/
            script1
            script2
        .aliases
        .xsessionrc

    self/ -> .config-framework/self

    zsh/
        .zprofile
        .zshenv
        .zshrc

    .config-framework
        self/
            .local/bin/
                cmpver
                config-manage -> ../../../config-manage
            .zsh/completion/
                _config-manage
        config-manage

    config.ini

GNU Stow odpowiada za odwzorowanie wewnątrz katalogu domowego struktury plików i katalogów z każdego modułu. Zatem po zainstalowaniu modułów home, self i zsh, katalog domowy będzie miał następującą zawartość:

$HOME/
    .local/bin
        cmpver -> ../../config/self/.local/bin/cmpver
        config-manage -> ../../config/self/.local/bin/config-manage
        script1 -> ../../config/home/.local/bin/script1
        script2 -> ../../config/home/.local/bin/script2

    .zsh/completion/ -> ../../config/self/.zsh/completion

    .aliases -> ../../config/home/.aliases
    .xsessionrc -> ../../config/home/.xsessionrc
    .zprofile -> ../../config/zsh/.zprofile
    .zshenv -> ../../config/zsh/.zshenv
    .zshrc -> ../../config/zsh/.zshrc

Szczególnie warto zwrócić uwagę na .zsh/completion/ – w tej chwili jest to link symboliczny do katalogu, jednak gdyby więcej modułów zdefiniowało skrypty autouzupełniania dla zsh, wówczas GNU Stow utworzyłby w jego miejscu katalog, a w nim odpowiednie dowiązania.

config-manage

Skryptem, który zajmuje się wprawianiem maszynerii w ruch jest config-manage. Jego zadaniem jest zmiana katalogu roboczego, ustawienie kilku zmiennych dostępnych w pliku konfiguracyjnym i koniec końców odczyt i wykonanie zadań zapisanych w pliku config.ini - pierwszym znalezionym w bieżącym katalogu lub dowolnym katalogu nadrzędnym. Nie będę przytaczał tutaj kodu źródłowego samego skryptu, gdyż jest on dostępny w repozytorium, które specjalnie na tę okazję stworzyłem.

Repozytorium to jest stworzone z myślą o uwzględnieniu go w repozytorium konfiguracji jako submoduł gita. Domyślnie posiada moduł o nazwie self, który zainstaluje skrypt config-manage oraz dodatkowe skrypty pomocnicze w katalogu .local/bin

config-manage posiada dwie podkomendy służace do instalacji i deinstalacji modułów: install i uninstall. Obie po prostu przyjmują listę nazw modułów3. Domyślne zasady (komendy) dlań wywoływane znajdują się w sekcji [DEFAULT] pliku config.ini. Sekcja [DEFAULT] zawiera zresztą domyślne pola dla każdej sekcji pliku konfiguracyjnego, a nawet jeśli dany moduł nie posiada odpowiadającej mu sekcji, to zostanie w zamian użyta sekcja domyślna. Oznacza to, że jeśli zadowolimy się ustawieniami domyślnimi (czyli jedynie instalacją plików konfiguracyjnych tu i ówdzie), to w ogóle nie trzeba się przejmować istnieniem pliku config.ini. Dodatkowo dla wygody w sekcji domyślnej utworzone są dwie dodatkowe zmienne, które mogą zostać użyte gdziekolwiek: ${stow_install} i ${stow_uninstall}. Ułatwiają one pisanie zasad instalacji modułów, w których wywołanie programu Stow jest zledwie jednym z kilku kroków.

Spójrzmy jak może wyglądać przykładowy plik config.ini:

[DEFAULT]
stow_install = stow -R ${targetname}
stow_uninstall = stow -D ${targetname}
install_cmd = ${stow_install}
uninstall_cmd = ${stow_uninstall}

[self]
# disable uninstall
uninstall_cmd =

[home]
install_cmd = ${stow_install}
              script1 ${env:PWD}
              ls ${var:home}/.local/bin

Jego treść mówi raczej sama za siebie, jednak dla jasności opiszę co się dzieje w jego poszczególnych fragmentach.

Najpierw zostały zdefiniowane domyśle zasady instalowania i odinstalowywania modułów. Następnie, w sekcji [self], określamy, że chcemy moduł o takiej nazwie jedynie instalować, uniemożliwiając jego usunięcie.

W sekcji [home] pozostawiamy domyślne zasady deinstalacji programów, jednak przy instalacji chcemy, oprócz utworzenia odpowiednich linków symbolicznych, wykonać uruchomienie testowe jednego z instalowanych skryptów, a następnie wylistowanie katalogu, w którym został zainstalowany. Wobec tego przekazujemy konkretne polecenia do pola install_cmd, każde w nowej linii. Teraz config-manage będzie wiedział, że przy instalacji musi wykonać więcej niż jedną komendę, jednak niepowodzenie dowolnej z nich (sygnalizowane przez zwrócenie statusu innego niż 0) zakończy wykonywanie całego “skryptu”.

Warto jeszcze zatrzymać się nad zmiennymi dostępnymi w pliku konfiguracyjnym, jest ich bowiem kilka:

  • ${targetname} - specjalna zmienna dodana przez skrypt config-manage, dostępna wewnątrz danej sekcji i zawierająca jej nazwę.
  • ${var:...} - kilka zmiennych pomocniczych dodanych przez config-manage. Należą do nich: ${var:home} (ścieżka bezwzględna do katalogu domowego) oraz ${var:workdir} (ścieżka bezwzględna do katalogu, w którym rezyduje skrypt config-manage). Dostępne są w każdej sekcji.
  • ${env:...} - wszystkie zmienne środowiskowe ustawione w momencie wywoływania config-manage. Dostępne w każdej sekcji.

W następnej części

To właściwie podsumowuje podstawowe założenia filozofii, którą się kierowałem przy tworzeniu mojego autorskiego frameworku do zarządzania konfiguracjami. Ponieważ artykuł mocno już napęczniał, to w tym miejscu się zatrzymamy – informacji do przetrawienia jest aż nadto. W następnej części opiszę sposób przekazywania argumentów do zasad instalacyjnych oraz podam przykłady modułów, w których może się to przydać.

Ciąg dalszy nastąpi…


  1. W tej części opiszę jedynie najprostszy przypadek - zarządzanie plikami konfiguracyjnymi. Dodatkowe informacje na temat frameworku znajdują się w części 2

  2. Opis programu GNU Stow znajduje się w części 3
  3. To nie do końca prawda, ale opowiem o tym następnym razem.