Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

Piszemy własny instalator w C

W tym wpisie opiszę to, jak stworzyłem instalator dla Libgreatttao. Instalator w zamierzeniu ma obsługiwać tylko x86 i x86_64, jako iż Libgreattao nie ma wsparcia dla Androida. Dzięki takiemu założeniu mogłem go napisać w C. Postanowiłem także użyć komponentów, które są dostępne raczej w każdym systemie Uniksowym, czyli biblioteki standardowej C i podstawowych wywołań/programów Uniksowych. Instalator korzysta z programów uname, tar, which, tput i tty.

Napisałem również skrypt tworzący instalator, jako iż przy wydaniu kolejnej wersji Libgreattao może zaistnieć potrzeba stworzenia nowego instalatora. Skrypt ten wykonuje w odpowiednich katalogach make clean, make, mkdir, tar, touch, make install. Niekiedy wykona xdg-terminal Skrypt ten sprawdza czy posiadamy architekturę x86_64, a jeśli nie, to generuje jedynie archiwum tar-a dla x86.

Makefile

Do utworzenia instalatora stworzyłem plik Makefile, które w sumie nie będzie potrzebny - polecenia z Makefile można umieścić w skrypcie tworzącym instalator. Skrypt tworzący instalator wykonuje make w katalogu ze źródłami instalatora. Oto, jak wygląda ten Makefile: tao_installer: main.c tarbal32 tarbal64 gcc main.c -o tao_installer -Wl,--format=binary -Wl,LICENSE -Wl,tarbal32 -Wl,tarbal64 -Wl,--format=default Przed poleceniem gcc jest umieszczony tabulator. Umieszczenie w tym miejscu tabulatora jest wymagane!
Pierwsza linijka określa nam, kiedy program make, w momencie wywołania w katalogu z naszym Makefil-em, ma wykonać drugą linijkę. Będzie to, jeżeli któryś z plików o jednej z nazw wymienionych po dwukropku będzie nowszy od pliku tao_installer.
Druga linijka to wywołanie kompilatora języka c. Kompilator wymaga pliku main.c, a przekazuje do linkera plik LICENSE, tarbal32, tarbal64. --format=binary oznaczy, że linker nie będzie zwracać uwagi na format pliku - po prostu doda go do wygenerowanego pliku wykonywalnego. Każdy kolejny plik przekazany do linkera będzie traktowany jako plik w formacie binarym. --format=default przełącza linkera w tryb dodawania plików w formacie domyślnym. Jest to konieczne, gdyż kompilator w wywołaniu linkera umieści na końcu plik obiektowy, który został wygenerowany z pliku main.c.

Uruchamianie programów konsolowych

W tym rozdziale nadmienię o dwóch niezwykle ważnych sztuczkach. Każdą z nich wykorzystaliśmy w instalatorze lub w skrypcie do wygenerowania instalatora. Obiema sztuczkami nie muszą się martwić programiści aplikacji pod Windows, bo Windows dba o te sprawy. Zaimplementowanie jednak tych rzeczy jest niezwykle proste. Poniżej zamieszczam spis tych sztuczek:
  1. Tworzenie okna terminala, gdy nie jest ono dostępne
  2. Przejście do katalogu z plikiem wykonywalnym uruchomionego programu
Obie sztuczki przedstawię, jako kawałek kodu powłoki bash. Oto pierwsza ze sztuczek: tty > /dev/null if [ ! $? -eq 0 ]; then xdg-terminal "env SPAWN_TERMINAL=TRUE \"$0\"" exit 0 fi Program tty zwraca 1, jeśli standardowe wejście nie jest terminalem, a 0 w przypadku, gdy nim jest. W logice bash-a 0 oznacza prawdę, a wszystko inne fałsz. Jest to spowodowane tym, że kody powrotu sygnalizują typ błędu lub jego brak. Druga linijka sprawdza kod powrotu( $? ). Sprawdza czy nie jest (!) równy (-eq) 0.. Jeżeli nie jest równy 0, to wykonuje xdg-terminal. Xdg-terminal uruchamia graficzny emulator terminala naszego środowiska graficznego, uruchamiając w nim polecenie przekazane jako pierwszy parametr. Nawiasy podwójne pozwalają na podstawianie w nich zmiennych. My wykorzystujemy argument przekazany naszemu skryptowi na pozycji 0. W systemach Uniksowych przyjęto, że pierwszy argument, to ścieżka do uruchomionego pliku, a powłoki chyba zawsze przekazują w parametrze 0 tą ścieżkę. Umieściliśmy ten argument między ciągami \", jako iż ścieżka może zawierać spacje. Backslash w tych ciągach jest konieczny, jako iż znak cudzysłowia zamknąłby ciąg przekazywany za ciągiem xdg-terminal. env SPAWN_TERMINAL=TRUE ustawia dla nowo tworzonego procesu(przez xdg-terminal) zmienną środowiskową SPAWN_TERMINAL na ciąg TRUE. Umożliwia na to powstrzymanie zamknięcia okna terminala, wykonując polecenie read, o tak: if [ "$SPAWN_TERMINAL" == "TRUE" ]; then echo Naciśnij dowolny klawisz, by zamknąć te okno # Napis ten przetłumaczyłem z języka angielskiego - w moim skrypcie jest on po angielsku read -t 20 pause fi exit 0 powoduje wyjście, by dalsze instrukcje się nie wykonały. Podsumowując: proces uruchamia swój program w oknie konsoli, jeśli nie został uruchomiony w konsoli.
Druga sztuczka polega na przejściu do bieżącego katalogu. Ta sztuczka jest wykorzystywana tylko przez generator instalatora. Jest to konieczne, gdyż w skrypcie odwołujemy się do ścieżek relatywnych, a skrypt mógł zostać uruchomiony z dowolnego katalogu(środowiska graficzne uruchamiają wszystko z katalogu / ). Oto jak prosta jest to sztuczka: cd "`dirname "$0"`" cd zmienia obecny katalog, a dirname "$0" zwraca ścieżkę do katalogu naszego programu. Ścieżka ta będzie absolutna lub relatywna, ale niezależnie od tego zadziała, gdyż ścieżka relatywna zostanie doklejona na koniec obecnego katalogu. Jeśli uruchomiliśmy skrypt a.sh, w taki sposób katalog/do/a/a.sh w katalogu /home/nintyfan, to dirname zwróci katalog/do/a/, a cd wykona operację sklejenia, która zwróci /home/nintyfan/katalog/do/a/. Jeśli jednak podaliśmy ścieżkę /home/nintyfan/katalog/do/a/a.sh z katalogu /home/ktosik, to dirname zwróci /home/nintyfan/katalog/do/a, a cd nie wykona sklejenia i przejdzie do /home/nintyfan/katalog/do/a.

Ważniejsze elementy instalatora

Instalator wykorzystuje tę samą sztuczkę, co skrypt generujący instalator, jednak tutaj trzeba wykorzystać funkcje języka C i Uniksowe funkcje, jak system, fork, execlp, a także makra WIFEXIT, WEXITSTATUS. Funkcja system uruchamia domyślną powłokę użytkownika, przekazując jej ciąg znaków, jako polecenia do wykonania. Zwraca ona informacje o tym, czy wykonanie polecenia się powiodło, i z jakim kodem powrotu. Makro WIFEXIT sprawdza czy przekazany jej argument zawiera informację o tym, że polecenie zostało uruchomione i zakończyło się normalnie. WEXITSTATUS zwraca kod powrotu z tej samej informacji, jeśli wywołanie makra, które opisałem przed chwilą, zwraca prawdę. Jeżeli chcemy sprawdzić czy domyślne wejście nie jest terminalem, to poniższe wystarczy: int is_spawned_in_console = system("tty &> /dev/null"); if (WIFEXITED(is_spawned_in_console) && WEXITSTATUS(is_spawned_in_console) == 1) { // nie terminal } Do bloku kodu instrukcji if możemy wkleić następującą treść: execlp("xdg-terminal", "xdg-terminal", argv[0], NULL); fprintf(stderr, "xdg-terminal program not installed! Aborting!\n"); exit(1); Execlp przyjmuje nazwę programu do uruchomienia, a także listę parametrów do przekazania, zakończoną wartością NULL. argv[0], to pierwszy argument przekazany do naszego programu, czyli sposób, w jakim został wywołany(panuje tutaj analogia do skryptów bash). Execlp przy braku błędu nie powraca.
Kolejną rzeczą jest sprawdzenie efektywnego identyfikatora użytkownika. W systemach Uniksowych każdy proces ma trzy identyfikatory użytkownika, jednak ja nie będę ich opisywać. Zwrócę tylko uwagę, że efektywny UID zawiera informacje o tym, na prawach jakiego użytkownika działa program. Sprawdzenie efektywnego identyfikatora użytkownika wykonuje się funkcja geteuid. Musimy sprawdzić czy nie jest równy zero, i jeżeli to prawda, to uruchomić sudo w ten sam sposób, co uruchomiliśmy xdg-terminal. Trzeba zachować tą samą kolejność sprawdzania tych warunków, co w tym artykule.

Obsługa plików zasobów

Pamiętasz, jak doklejaliśmy do wynikowego pliku z programu linker, własne pliki? Dla każdego takiego pliku są tworzone co najmniej dwa symbole "_binary_ŚCIEŻKA_DO_PLIKU_start" i "_binary_ŚCIEŻKA_DO_PLIKU_end", przy czym znaki niedozwolone w etykietach assemblerowych, jak kropka czy slash, są zamieniane na znaki podłóg. By utworzyć zmienną wskazującą na początek tekstu licencji, to należy się posłużyć takim kodem: extern char license_text_start[] asm("_binary_LICENSE_start"); Extern mówi kompilatorowi, żeby nie szukał tego symbolu w bieżącym pliku.

By wypakować plik zasobów, to należy posłużyć się mkstemp i write. Nie można także zapomnieć o wywołaniu close. Do write przekazujemy, jako drugi argument wskaźnik na początek pliku w zasobach, a jako trzeci długość tego pliku. By obliczyć długość pliku, to należy rzutować oba wskaźniki na koniec i początek naszego pliku w zasobach na typ (intptr_t), który jest w pliku nagłówkowych unistd.h. Do mkstemp należy przekazać tablicę znaków(powtarzam: tablicę znaków, a nie wskaźnik na ciąg znaków, jako iż literały znakowe są umieszczane w pamięci bez praw do zapisu), której ostatnie sześć znaków to X.
Oto przykład: char license_text_path[] = {'/', 't', 'm', 'p', '/', 'l', 'i', 'b', 'g', 'r', 'e', 'a', 't', 't', 'a', 'o', '-', 'l', 'i', 'c', 'e', 'n', 's', 'e', '-', 'X', 'X', 'X', 'X', 'X', 'X'}; fd = mkstemp(license_text_path); write(fd, license_text_start, (intptr_t)license_text_end - (intptr_t)license_text_start); close(fd);

Sprawdzanie architektury

Do sprawdzenia architektury trzeba się posłużyć poleceniem uname -m. Jest też oczywiście funkcja dostępna w C, pod UNIKSEM, które zwraca te informacje. Jednak, by nie mieszać za bardzo, postanowiłem użyć uname -m. Dla prostoty wybrałem popen języka C i fgets(również języka C), zamiast korzystać z uniksowych fork, pipe, dup2, read, execlp. Są sytuacje, w których lepiej posłużyć się drugą metodą, bo np. przekazujemy parametr wywołania naszego procesu do wywoływanego programu, jednak w tym wypadku lepiej posłużyć się popen i fgets. Trzeba jeszcze usunąć znak nowego wiersza z końca. W źródłach instalatora jest funkcja get_command_output_without_last_newline, która załatwia wszystko.

Po otrzymaniu architektury, to należy sprawdzić, czy początek archiwum tar dla tej architektury nie jest jej końcem(czyli czy archiwum nie było tak naprawdę pustym plikiem). Powód jest taki, że być może nie będziemy posiadać kompilatora obsługującego jedną z architektur, więc skrypt generujący archiwum tar, wygeneruje zamiast tego pusty plik. Jeżeli archiwum było plikiem pustym, to trzeba wybrać inną obsługiwaną architekturę przez ten procesor i ponowić próbę. W przeciwnym wypadku zapisujemy wskaźnik na koniec i początek pliku do specjalnych zmiennych. Oto przykład: if (strcmp(architecture, "x86") == 0) { if ((intptr_t) tarbal32_end -(intptr_t) tarbal32_start == 0) { system("tput setaf 1"); fprintf(stderr, "Unsupported architecture type\n"); system("tput sgr0"); exit(1); } tarbal_start = tarbal32_start; tarbal_end = tarbal32_end; } else if (strcmp(architecture, "x86_64") == 0) { if ((intptr_t) tarbal64_end -(intptr_t) tarbal64_start == 0) { system("tput setaf 1"); fprintf(stderr, "x86_64 processors are not supported. Switching to x86\n"); if ((intptr_t) tarbal32_end -(intptr_t) tarbal32_start == 0) { fprintf(stderr, "Unsupported architecture type\n"); system("tput sgr0"); exit(1); } system("tput sgr0"); tarbal_start = tarbal32_start; tarbal_end = tarbal32_end; } else { tarbal_start = tarbal64_start; tarbal_end = tarbal64_end; } } else { system("tput setaf 1"); fprintf(stderr, "Unkown architecture. Exiting\n"); system("tput sgr0"); exit(1); } Trochę przydługi przykład, prawda? No, cóż - nie chciało mi się refaktoryzować, gdyż obsługujemy tylko dwie architektury.; Dodanie obsługi np. arm-ów wymagałoby skorzystanie z Javy lub z FatELF, jednak Java nie musi być zainstalowana na każdym systemie, a FatELF nie zostało włączone do vanilii.
Wykorzystujemy tutaj tput. Służy to do koloryzowania komunikatów. Informacje o tput znajdują się na anglojęzycznej Wikipedii.

Wyświetlanie licencji

Do wyświetlania licencji należy się posłużyć się jakimś pagerem. Pager powinien być wskazywany przez zmienną środowiskową PAGER. Jeżeli nie, to należy za pomocą popen("which less", "r") i popen("which more", "r") sprawdzić obecność pagerów less i more(wynik popen należy przekazać do fgets, a następnie usunąć z bufora, do którego zapisał w fgets znak nowej linii, by w ten sposób uzyskać ścieżkę do pagera). Następnie wybieramy jeden z dostępnych. Po wypakowaniu licencji z zasobów, możemy ją wyświetlić, przekazując do execlp ścieżkę do pagera lub zawartość zmiennej PAGER, jako pierwszy i drugi argument, a jako trzeci należy przekazać ścieżkę do pliku z licencją.. Musimy się także posłużyć funkcją wait. Oto przykład: if (answer == 'r') { if (fork() == 0) { char buffer[PATH_MAX]; pagerprog = getenv("PAGER"); if (pagerprog) { execlp(pagerprog, pagerprog, license_text_path, NULL); } get_command_output_without_last_newline("which less", buffer, PATH_MAX); execlp(buffer, buffer, license_text_path, NULL); get_command_output_without_last_newline("which more", buffer, PATH_MAX); execlp(buffer, buffer, license_text_path, NULL); fprintf(stderr, "Unable to open any pager\n"); exit(1); } if (wait(&status) == -1) { perror("ERROR IN WAIT "); } if (WIFEXITED(status)) { if (WEXITSTATUS(status) == 1) { exit(1); } } else { fprintf(stderr, "Error while executing program to show license text"); } } Funkcja get_command_output_without_last_newline pobiera wynik polecenia i usuwa nową linię z końca. Możesz przeczytać  zawartość tej funkcji ze źródeł libgreattao, podkatalogu Installer, pliku main.c. Zmienna answer, w przypadku mojego programu, przyjmuje wartość 'r', gdy użytkownik wybrał przeczytanie licencji.

Właściwa instalacji

Dla właściwej instalacji, należy zmienić bieżący katalog na /usr/local, bo tam powinny być trzymane wszelkie programy nie dostarczone z dystrybucją, a następnie wykonać: execlp("tar", "tar", "--extract", "-f", tarbal_path, NULL); tarbal_path powinno przechowywać ścieżkę do archiwum z programem do instalacji. Archiwum te powinno być wcześniej wypakowane pod tą ścieżkę z zasobów.

Skrypt tworzący instalator

Najpierw należy po sobie wyczyścić. Następnie powinnyśmy utworzyć dowiązanie symboliczne pliku z licencją do katalogu ze skryptem tworzącym instalator. Następnie tworzymy dwa puste pliki: tarbal32 i tarbal64. Pierwszy jest dla plików do wypakowania dla architektury x86, a drugi dla plików do wypakowania dla architektury x86_64. Jest to konieczne, gdyż nie będziemy przeprowadzać generowania instalatora dla architektury x86_64 na procesorze x86 - gcc chyba tego nie wspiera, choć muszę sprawdzić. Następnie należy sprawdzić architekturę, i jeżeli jest to x86_64, wykonujemy make clean -C katalog_ze_źródłami_programu, a następnie make -C katlog_ze_źródłami_programu, po czym make install -C katalog_ze_źródłami_programu. Należy pamiętać tak, by skonfigurować make tak, by instalował program do katalogu, z którego wygenerujemy archiwum dla x86_64. Zawsze(niezależnie czy architekturą jest x86_64, czy też x86,) należy wygenerować program dla x86. Robimy to podobnie, jak dla x86_64, lecz przed każdą komendą należy dopisać CFLAGS="-m32" CXXFLAGS="-m32" linux32 Należy też pamiętać, by przed przystąpieniem do instalacji programu dla x86, skonfigurować make tak, by instalował do katalogu, z którego wygenerujemy tarbal dla architektury x86.

Tworzenie tarbali

Do utworzenia tarbala, należy wykorzystać przełącznik C, wskazując na katalog z binariami, czyli tak: tar -cf ./plik_wynikowy -C katalog_z_binariami/ . Przełącznik -C wskazuje na katalog, do którego ma przejść tar przed wygenerowaniem tarbala. Nie należy generować tarbala w taki oto sposób: tar -cf ./plik_wynikowy katalog_z_binariami/* Bo wtedy katalog główny archiwum będzie zawierać katalog katalog_z_binariami, a w nim poszczególne pliki. Spowoduje to, że program będzie nam się źle instalować.
Należy również dodać kompresję gzip. Przykład poprawnego wywołania polecenia tar przedstawiam poniżej: tar -czf ./tarbal32 -C tarbal-files-32/ .  

linux programowanie

Komentarze

0 nowych
nintyfan   11 #1 11.06.2015 11:46

Jeżeli ktoś by się uparł, i chciałby mieć instalatory dla wszystkich architektur systemów GNU/Linux, to może wygenerować skrypt bash-a, w miejsce programu w C. Wtedy dodawane pliki binarne(tarbale) należy zakodować w BASE64 i dodać je na koniec archiwum. Instalator powinien wyłuskać takie pliki binarne przez head -n numer_linii_w_którym_jest_zakodowany_plik_binarny | tail -n 1 | base64 -d > /ścieżka_do_tarbala. Podobnie możemy postąpić z licencją.

marcin1510   8 #2 11.06.2015 15:44

Dzięki ! Jeszcze przydałoby się wiedzieć czy na Windows też takie coś przedstawione tu by działało.

nintyfan   11 #3 11.06.2015 15:55

@marcin1510: Windows ma coś takiego, jak pliki zasobów, które można dodać do pliku wykonywalnego. Nie mam pojęcia jednak, jak wypakować plik zasobu. Kiedy programowałem w Assemblerze/C na Windows, to korzystałem ze specjalnego API, by ustawiać ikony w oknie na te z zasobów pliku. Myślę jednak, że w każdym systemie, nie tylko GNU/Linux, można odwzorować pamięć na uchwyt/deskryptor/wskaźnik pliku, więc powinno się dać.

Co do generowania instalatora, to możesz zainstalować msys i make , a także pewnie i tar na Windows.

Autor edytował komentarz.
duffee   11 #4 12.06.2015 00:10

sa darmowe instalatory, z ktorych mozna korzystac

TestamenT   12 #5 12.06.2015 02:47

Ja ograniczam korzystanie z C do minimum. Bo jak trzeba operować na pamięć to C jest bezlitosne i nie wybacza błędów.

Dobrze byłoby użyć dialog i zenity jako graficzne interfejsy.

nintyfan   11 #6 12.06.2015 07:43

@TestamenT: Zastanawiam się nad użyciem libgreattao i otwarcia pamięci, jako plik, by zapewnić graficzny interfejs :-D.

Poza tym, to czemu nie wystarczyłby dialog/ncurses? Zenity jest zbędne - po co komplikować rzeczy?
Inną kwestią jest to, że ten instalator jedynie wyświetla licencję i pyta się o akceptację instalacji - to jedyne zadanie.

  #7 12.06.2015 12:10

Nie mam pojęcia czym jest libgreattao ale nie wygodniej dla ludzi wygenerować paczki deb i rpm zamiast pisać instalatory?

nintyfan   11 #8 12.06.2015 14:07

@sbv (niezalogowany): Jest takie coś, jak OBS(OpenSuSE Build Service), które generuje paczki dla wielu dystrybucji. Jednak się na tym nie znam, jak również nie znam się na generowaniu paczek. Nauczę się korzystać z OBS, i może wygeneruję paczki.

Generowanie paczek, to nie programowanie, a na paczkowaniu się nie znam.

Autor edytował komentarz.
WODZU   17 #9 12.06.2015 14:36

Zwrócił moją uwagę pewien fragment kodu, który powinien wyglądać raczej tak:

if (WIFEXITED == 1) {
"drink beer && watch sport"
}
else {
"wash dishes"
}

;)

Autor edytował komentarz.
nintyfan   11 #10 12.06.2015 15:27

@WODZU: Muszę Cię zmartwić, ale wg. dokumentacji WIFEXITED przyjmuje status, jako parametr, a dodatkowo zwraca prawdę lub fałsz.

WODZU   17 #11 12.06.2015 15:29

@nintyfan: Czyli to nie żona, która opuściła mieszkanie?

nintyfan   11 #12 12.06.2015 20:13

@WODZU: Nie znam przesłania twojej wypowiedzi.

WODZU   17 #13 12.06.2015 20:18

@nintyfan: Chyba moja wyobraźnia popłynęła niezrozumiale daleko, ale gdy zobaczyłem ten kod, a nie jestem programistą, to w oczy rzuciło mi się właśnie WIFEXITED, które odczytałem jako WIFE EXITED, czyli w wolnym tłumaczeniu "żona wyszła" ;)

nintyfan   11 #14 12.06.2015 21:35

@WODZU: No tak. Muszę być przemęczony.

awangardowy   7 #15 13.06.2015 00:46

ja korzystam z basha/makefile/czy tam prostego zip/unzip .
co do instalowania na servery polecam ansible :
http://www.ansible.com/blog/free-ansible-book