cz.1| Jak to jest być deweloperem aplikacji wieloplatformowej - Deweloper vs Windows
Witam.
Długo nie pisałem - bo ostatni cały tydzień spędziłem na głowieniu się dlaczego to, dlaczego tamto i sto innych rzeczy nie działa pod systemem Windows. Wybaczcie za wstęp, ale straciłem na prawdę sporo czasu na przenoszeniu swojej jednej małej aplikacji napisanej w Pythonie na system Windows.
Spis treści: 1. Proces przenoszenia kodu Pythona 2. Ogólny proces paczkowania przez wirtualną maszynę, Tworzenie "exe" z plików Pythona 3. Pakowanie projektu w instalator NSIS 4. Podsumowanie i krótki komentarz
No dobrze, no to zacznę według kolejności.
1. Proces przenoszenia kodu Pythona
Jako, że Python jako interpreter działa natywnie pod systemami opartymi o jądro Linux jak i pod systemami z rodziny Windows (NT) tak więc można wnioskować - "a co tam takiego zależnego od platformy jest" a jednak troszkę jest i to troszkę to za dużo.
Zdaję sobie sprawę, że w C, C++ czy innym kompilowanym języku na pewno jest jeszcze więcej problemów przy przenoszeniu aplikacji ale ja opisuję w tym wypadku Pythona i nie zajmuję się C, C++ czy innym językiem kompilowanym.
1) Ładowanie wtyczek, plików językowych
W zasadzie nic trudnego, wystarczy dodać do istniejących odwołań do systemu plików po prostu prefix zależny od systemu operacyjnego który będzie wyglądać mniej więcej tak:
if os.name == "nt": self.prefix = "c:/Program Files/Aplikacja" else self.prefix = ""
Wtedy odwołania bedą wyglądać mniej więcej tak:
self.window.set_icon_from_file(self.prefix+"/usr/share/aplikacja/icons/window.png")
Jednak sprawa się bardzo komplikuje jeżeli nie wiemy gdzie aplikacja będzie się znajdować.
Tak więc musimy iść na sam koniec procesu przenoszenia aplikacji na platformę Windows i zbudować instalator, tak zbudować instalator do nie działającej jeszcze aplikacji ponieważ jej działanie jest zależne od instalatora!
Instalator musi utworzyć wpis w rejestrze systemowym z lokalizacją plików aplikacji tak aby nasza aplikacja wiedziała gdzie się znajduje!
Dla instalatora NSIS będzie to taka linijka:
WriteRegStr HKCU "SOFTWARE\NazwaAplikacji" 'Directory' '$INSTDIR'
Gdzie: Directory - nazwa klucza $INSTDIR - katalog w którym jest zainstalowana aplikacja
Tak więc po zainstalowaniu tworzony jest klucz w rejestrze który wskazuje na katalog w którym użytkownik zainstalował aplikację. Z poziomu aplikacji możemy zatem odczytać ten klucz, aby to zrobić wystarczy mniej więcej coś takiego:
if os.name == "nt": import _winreg try: key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, 'Software\\MojaAplikacja\\', 0, _winreg.KEY_READ) (value, valuetype) = _winreg.QueryValueEx(key, 'Directory') self.prefix = str(value) except WindowsError: print "Cannot find registry key HKEY_CURRENT_USER\Software\MojaAplikacja\Directory, exiting." sys.exit(2)
Brzmi skomplikowanie? Teraz jak to działa w systemach Uniksowych
W systemach Uniksowych znajdują się katalogi /usr/bin, /usr/share, /usr/lib, /etc, $HOME, /tmp i wiele innych, służą one do tego aby segregować instalowaną treść.
/usr/bin - pliki wykonywalne (binarne lub skryptowe) /usr/lib - biblioteki, wtyczki i wszystko co się ładuje dynamicznie /usr/share - obrazki, ikony, dokumentacje, tłumaczenia itp. /etc - pliki konfiguracyjne które są obejmowane ochroną nadpisania podczas aktualizacji systemu, istnieją specjalne narzędzia aby je aktualizować bez żadnych strat $HOME - katalog domowy użytkownika, można w nim zapisać spersonalizowaną konfigurację programu dla danego użytkownika /tmp - katalog tymczasowy, przykładowo program archiwizujący dane może tam tworzyć archiwum a następnie je przenieść do katalogu wybranego przez użytkownika
Budowanie pakietu dla poszczególnych systemów Uniksowych jest bardzo proste, i sprowadza się do edycji jednego pliku oraz wykonania odpowiedniego polecenia które zrobi wszystko za nas.
Dzięki temu, że menadżer pakietów zapamiętuje jakie akcje wykonuje podczas instalacji pakietu jest później w stanie odwrócić zmiany - czyli możliwe jest usunięcie pakietu bez tworzenia deinstalatora.
Przykładowe tworzenie paczki w Arch Linux:
1> Tworzymy katalog z nazwą aplikacji, a w nim plik PKGBUILD z zawartością przykładowo:
pkgname=nazwaaplikacji-git pkgver=0.6 # wersja pkgrel=1 # numer kompilacji paczki dla aktualnej wersji pkgdesc="Opis naszej aplikacji" arch=('i686' 'x86_64') # dostępne platformy url="http://dobreprogramy.pl" # adres url license=('GPL') # licencja depends=('git' 'alang-py' 'python' 'pygtk') # zależności, czyli to co zostanie zainstalowane automatycznie przez menadżer pakietów za nas makedepends=('git') # zależności do samego zbudowania paczki provides=('nazwaaplikacji-git') # paczka zastępuje inną paczkę? conflicts=('nazwaaplikacji') # paczka konfliktuje z inną paczką? # adres url do pobrania z GIT, można także pobrać z archiwum, SVN czy innego źródła, ja wybrałem GIT _gitroot="git://github.com/webnull/nazwaaplikacji.git" _gitname="nazwaaplikacji" build() { cd "$srcdir" if [ -d $_gitname ] ; then cd $_gitname && git pull origin msg "Zaaktualizowano lokalne pliki do najnowszej wersji" else git clone $_gitroot $_gitname fi # czyszczenie katalogu budowania rm -rf "$srcdir/$_gitname-build" git clone "$srcdir/$_gitname" "$srcdir/$_gitname-build" ./configure -parametr1 -parametr2 make # kopiowanie plików wyjściowych do katalogu z danymi paczki cp "$srcdir/mojaaplikacja/usr" "$srcdir/../pkg/usr" -R }
2> Następnie wywołujemy makepkg i za powiedzmy 10 sekund mamy gotową paczkę do zainstalowania którą po zainstalowaniu można jeszcze odinstalować choć nie tworzyliśmy dla niej odinstalatora, proste prawda?
2) Odczyt i zapis plików
Przez kilka godzin dochodziłem do tego dlaczego Windows pozwala odczytać tylko ok. 1/100 zawartości pliku zamiast całości.
Problemem okazało się najwyraźniej kodowanie bo otwierałem pliki w których były polskie ogonki.
Jak widać Linux poradził sobie z ogonkami zakodowanymi w windows-1250 bez problemów, a sam Windows miał problemy ze swoim genialnym kodowaniem.
Rozwiązanie było proste, ale ciężko mi było na nie wpaść przez kilka godzin - a mianowicie należy otwierać pliki w trybie binarnym czyli np. "rb" a nie w zwykłym trybie "r". Linux radzi sobie z ogonkami w obydwóch trybach bez żadnych problemów dlatego błędu nie potrafiłem wykryć.
Zatem jak ktoś mi nie wierzy to zapraszam do testów, proszę spróbować pliki z napisami filmowymi które w 90% są kodowane w windows-1250.
#!/usr/bin/python2 # kompatybilny z Linux i Windows fileHandler = open("tekst.txt", "rb") contents = fileHandler.read() fileHandler.close() print str(len(contents))
#!/usr/bin/python2 # kompatybilny z Linux fileHandler = open("tekst.txt", "r") contents = fileHandler.read() fileHandler.close() print str(len(contents))
3) Wywołania systemowe w razie braku bibliotek
W Pythonie problemem było rozpakowanie pliku skompresowanego przy użyciu 7zip, dlatego postanowiłem użyć zewnętrznego programu /usr/bin/7z (Linux) oraz 7za.exe (Windows).
Pod Linuksem sprawa jest o tyle banalna, że w zależnościach paczki dodajemy wymagany pakiet p7zip i menadżer pakietów sam to zainstaluje - no ba, pakiet znajduje się w każdym systemie Linuksowym więc problemu nie ma.
W Windows trzeba spakować plik 7za.exe do katalogu z aplikacją, a co jeśli wyjdzie aktualizacja do tego pliku? - A co jeśli licencja nie pozwala? Pełno problemów i ograniczeń, pod Windows świat staje się taki skomplikowany...
Dziwny problem z os.system i subprocess
Gdy próbowałem wywołać pod os.system pewne polecenie pod Windows to zwracało mi komunikat w stylu "C:\program" nie ma takiego pliku lub katalogu jednak w subprocess to samo wyrażenie nie powodowało błędu.
# Podobny przykład działa pod Linuksem, ale nie pod Windowsem os.system("\"c:\\katalog z spacja\\7za.exe\" --parametry --otworz archiwum.7z > zapis-do-pliku.txt")
# Podobny przykład działa pdo Windowsem, nie testowany pod Linuksem subprocess.call("\"c:\\katalog z spacja\\7za.exe\" --parametry --otworz archiwum.7z > zapis-do-pliku.txt", shell=True, bufsize=1)
Skoro adres do pliku w którym po drodze znajduje się spacja był wzięty w cudzysłów to system nie powinien mieć problemów z odnalezieniem pliku - a jednak miał.
Ciąg dalszy nastąpi według spisu treści...