GUI установщик Django 1.6.2 под Windows
0. Вступление Доброго времени суток, Хабр! Началось всё с того, что у меня возникла необходимость сделать GUI-установщик для Django под Windows. Не увидев на хабре какой-либо статьи, описывающей создание установщиков, я подумал, «А почему бы и нет»? Из сообщества Django по этому вопросу мне ответили приблизительно так, как это сделал хабраюзер ffriend. Спасибо ему за конструктивность и открытость. Подробности под катом.Вот такое письмо пришло мне от сообщества Django в ответ на моё фи. Собственно говоря, именно из-за этого фи я и оказался в минусе по репутации с рейтингом.
Текст Hi Ivan, Thanks for the suggestion.
The contact form you’ve used is for the DSF — we’re the fund raisingarm and legal arm of the Django project.
If you’ve got ideas for how Django can be improved, you should directthose ideas to the django-developers mailing list, or open a ticket onDjango’s ticket tracker.
Better still, try your hand at implementing the features you want tosee, and provide those changes as a patch against the Django codebase.If you can provide a better user experience for Windows or Cyrillicusers, we may integrate those changes into the core product.
Yours, Russ Magee %-)
Сразу скажу, как не надо делать. Не надо пытаться пользоваться ограниченной редакцией InstallShield, прилагаемой бонусом к MSVS Pro 2013.Быть может, в отдельных простых случаях он подходит более чем, но в моём был желателен как можно больший контроль над процессом, и когда я открыл InstallShield и стал разбирать его по косточкам — последний меня разочаровал своей ограниченностью.К счастью, на этом свете существует NSIS — NullSoft Install System, а именно — скриптовый генератор установщиков с возможностями, радующими глаз: Посмотреть список Поддержка ZLIB, BZIP2 и LZMA (файлы могут быть сжаты вместе или по отдельности). Генерация программ удаления. Настраиваемый пользовательский интерфейс: диалоги, шрифты, фоны, иконки, текст, галочки, изображения и т.д. Классический и современный интерфейсы мастеров. Поддержка нескольких языков в одной установке. Более 60 переводов доступны, но вы также можете создавать свои собственные. Поддержка unicode позволяет использовать ещё больше языков. Постраничная логика организации: вы можете добавить стандартные страницы мастера или создать собственные. Дерево для выбора компонентов установки. Возможность определить конфигурации: обычно минимальная, типовая и полная установки, а также возможно создать пользовательскую конфигурацию. Самопроверка CRC32. Малые расходы на сжатие, а именно — 34 Кб с параметрами по умолчанию. Возможность отображения лицензионного соглашения RTF или TXT. Возможность выявлять каталог назначения из реестра. Много плагинов для создания пользовательских диалогов, интернет-соединений, HTTP-скачивания, модификация файлов, вызовы WinAPI и т.д. Монтажники может быть как 2 Гб. Дополнительный тихий режим для автоматизированных установок. Препроцессор с поддержкой определенных символов, макросов, условной компиляции, стандартных предопределений. Кодирование с элементами PHP и include (включают в себя пользовательские переменные, стек, реальное управление потоком. Установщики имеют свои собственные виртуальные машины, которые позволяют следующее Извлечение файлов с настраиваемыми параметрами перезаписи. Копирование файлов и каталогов, переименование, удаление, поиск. Плагин для вызова DLL. Регистрация DLL, ActiveX, снятие с регистрации. Оболочка исполнения с возможностью ожидания выполнения операций. Создание ярлыка. Работать с реестром. Чтение и запись INI. Чтение и запись общего текстового файла. Гибкие манипуляции с целыми и строковыми значениями. Открытие окна на основе имени класса или его названия. Манипуляции с пользовательским интерфейсом: шрифт, установка текста. Окно отправки сообщений. Взаимодействие пользователей с окнами сообщений или страницами. Ветвление, сравнение и прочее в этом духе. Возможность контроля над ситуацией при возникновении ошибок. Перезагрузка, в том числе удаление или переименование при перезагрузке. Команды поведение установщика. Например, «показать», «скрыть», «ожидать». Пользовательские функции в скрипте. Функции обратного вызова для действий пользователя. Этот список взят из относительно полной документации на sourceforge, тогда как в штатном пакете присутствуют лишь общие сведения о генераторе и его языке, о чём и сказано в соответствующем файле справки.К моему удивлению, на sf не оказалось ссылки на архив с этой самой полной документацией. Эту оплошность исправил самостоятельно.Вместе с тем существует архив с набором «простых руководств» по реализации конкретных фич, который до этого приходилось качать через сторонний файловый хостинг сторонней же программой. Исправлен и этот недочёт.Существует великое множество — простите за каламбур — генераторов скрипта генератора на основе GUI (из опробованных наилучшим оказался HM NIS Edit), но вместе с тем существует только лишь одна методика продуктивного обучения языкам…1. Итак… Простейший код, не требующий особенных пояснений, для первого знакомства с общими принципами:
Познакомиться # define installer name OutFile «installer.exe» # set desktop as install directory InstallDir $DESKTOP # default section start Section # define output path SetOutPath $INSTDIR # specify file to go in output path File test.txt # define uninstaller name WriteUninstaller $INSTDIR\uninstaller.exe #------- # default section end SectionEnd # create a section to define what the uninstaller does. # the section will always be named «Uninstall» Section «Uninstall» # Always delete uninstaller first Delete $INSTDIR\uninstaller.exe # now delete installed file Delete $INSTDIR\test.txt SectionEnd Да, я ленивый, использовал копипасту отсюда, адаптируя оную под свои нужды.Кстати, у НЛО не нашлось подсветки nsi, поэтому был использован highlight.hohli.com, возможно, эта информация окажется полезной.Я не разбираюсь в тонкостях питона, но судя по файлу setup.py в Django-1.6.2 при установке работа идёт только и только с путями в той или иной системе. Код setup.py в Django import os import sys
from distutils.core import setup from distutils.sysconfig import get_python_lib
# Warn if we are installing over top of an existing installation. This can # cause issues where files that were deleted from a more recent Django are # still present in site-packages. See #18115. overlay_warning = False if «install» in sys.argv: lib_paths = [get_python_lib ()] if lib_paths[0].startswith (»/usr/lib/»): # We have to try also with an explicit prefix of /usr/local in order to # catch Debian’s custom user site-packages directory. lib_paths.append (get_python_lib (prefix=»/usr/local»)) for lib_path in lib_paths: existing_path = os.path.abspath (os.path.join (lib_path, «django»)) if os.path.exists (existing_path): # We note the need for the warning here, but present it after the # command is run, so it’s more likely to be seen. overlay_warning = True break
def fullsplit (path, result=None): »« Split a pathname into components (the opposite of os.path.join) in a platform-neutral way. »« if result is None: result = [] head, tail = os.path.split (path) if head == '': return [tail] + result if head == path: return result return fullsplit (head, [tail] + result)
EXCLUDE_FROM_PACKAGES = ['django.conf.project_template', 'django.conf.app_template', 'django.bin']
def is_package (package_name): for pkg in EXCLUDE_FROM_PACKAGES: if package_name.startswith (pkg): return False return True
# Compile the list of packages available, because distutils doesn’t have # an easy way to do this. packages, package_data = [], {}
root_dir = os.path.dirname (__file__) if root_dir!= '': os.chdir (root_dir) django_dir = 'django'
for dirpath, dirnames, filenames in os.walk (django_dir): # Ignore PEP 3147 cache dirs and those whose names start with '.' dirnames[:] = [d for d in dirnames if not d.startswith ('.') and d!= '__pycache__'] parts = fullsplit (dirpath) package_name = '.'.join (parts) if '__init__.py' in filenames and is_package (package_name): packages.append (package_name) elif filenames: relative_path = [] while '.'.join (parts) not in packages: relative_path.append (parts.pop ()) relative_path.reverse () path = os.path.join (*relative_path) package_files = package_data.setdefault ('.'.join (parts), []) package_files.extend ([os.path.join (path, f) for f in filenames])
# Dynamically calculate the version based on django.VERSION. version = __import__('django').get_version ()
setup ( name='Django', version=version, url='http://www.djangoproject.com/', author='Django Software Foundation', author_email='foundation@djangoproject.com', description=('A high-level Python Web framework that encourages ' 'rapid development and clean, pragmatic design.'), license='BSD', packages=packages, package_data=package_data, scripts=['django/bin/django-admin.py'], classifiers=[ 'Development Status: 5 — Production/Stable', 'Environment: Web Environment', 'Framework: Django', 'Intended Audience: Developers', 'License: OSI Approved: BSD License', 'Operating System: OS Independent', 'Programming Language: Python', 'Programming Language: Python: 2', 'Programming Language: Python: 2.6', 'Programming Language: Python: 2.7', 'Programming Language: Python: 3', 'Programming Language: Python: 3.2', 'Programming Language: Python: 3.3', 'Topic: Internet: WWW/HTTP', 'Topic: Internet: WWW/HTTP: Dynamic Content', 'Topic: Internet: WWW/HTTP: WSGI', 'Topic: Software Development: Libraries: Application Frameworks', 'Topic: Software Development: Libraries: Python Modules', ], )
if overlay_warning: sys.stderr.write (»«
======== WARNING! ========
You have just installed Django over top of an existing installation, without removing it first. Because of this, your install may now include extraneous files from a previous version that have since been removed from Django. This is known to cause a variety of problems. You should manually remove the
%(existing_path)s
directory and re-install Django.
»« % {«existing_path»: existing_path}) Я не разбираюсь в питоне, но в общем и целом видно, что здесь происходит работа с путями в зависимости от оси, проверка, не идёт ли установка поверх существующей Django и в setup (), по-видимому, содержится информация для пакета python в целом.К тому же, файл INSTALL утверждает: AS AN ALTERNATIVE, you can just copy the entire «django» directory to Python’ssite-packages directory, which is located wherever your Python installationlives. Some places you might check are:
/usr/lib/python2.7/site-packages (Unix, Python 2.7)/usr/lib/python2.6/site-packages (Unix, Python 2.6)C:\\PYTHON\site-packages (Windows)
Однако именно из-за того, что я не знаю питон, предпочёл более кошерное применение setup.py, так как доподлинно мне не известно, чем отзовётся отсутствие setup (). Вот, что получилось у меня после адаптации bigtest.nsi. В комментариях особо отмечены места, которые не оказываются интуитивно понятными, с которыми пришлось повозиться, пока всё пошло, как нужно.
Полный код скрипта установщика
! include «Include\LogicLib.nsh»
OutFile «installer.exe»
Name «Django 1.6.2»
Caption «Django 1.6.2»
Icon »${NSISDIR}\Contrib\Graphics\Icons\nsis1-install.ico»
CRCCheck on
SilentInstall normal
BGGradient 000000 0000FF FFFFFF
InstallColors FF8080 000030
XPStyle on
InstallDir $EXEDIR /* Обеспечиввает установку по умолчанию туда, куда был сохранён установщик */
SetFont «Tahoma» 10 /* Нравится мне этот шрифт и всё тут */
SetOverWrite on
#-NonSectionInstructionsEnd-
Section «BaseInstructions»
SetDatablockOptimize on
/* Без установки выходного пути для файлов установщик ругался
на невозможность записи уже во время выполнения,
хотя кажется, что необходимо достаточно InstallDir */
SetOutPath »$INSTDIR\*»
SetOverWrite on
File /r «C:\ins\Django-1.6.2\*» /* Та папка, в которой у меня лежал Django.
Именно так можно включить в установщик нужные файлы */
SectionEnd
RequestExecutionLevel highest /* От греха подальше… */
CheckBitmap »${NSISDIR}\Contrib\Graphics\Checks\classic-cross.bmp»
LicenseText «Django license»
LicenseData «license.rtf» /* Отделался… лень =) */
/* «Сколько языков ты знаешь, столько раз ты человек» =)
NSIDIR, как можно понять, папка установки генератора.
Весь язык сам по себе замечателен интуитивной понятностью */
LoadLanguageFile »${NSISDIR}\Contrib\Language files\English.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\Dutch.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\French.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\German.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\Korean.nlf»
LoadLanguageFile «Russian.nlf» /* Слегка подправленный файл перевода на родной */
LoadLanguageFile »${NSISDIR}\Contrib\Language files\Spanish.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\Swedish.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\TradChinese.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\SimpChinese.nlf»
LoadLanguageFile »${NSISDIR}\Contrib\Language files\Slovak.nlf»
; Set name using the normal interface (Name command)
LangString Name ${LANG_ENGLISH} «English»
LangString Name ${LANG_DUTCH} «Dutch»
LangString Name ${LANG_FRENCH} «French»
LangString Name ${LANG_GERMAN} «German»
LangString Name ${LANG_KOREAN} «Korean»
LangString Name ${LANG_RUSSIAN} «Russian»
LangString Name ${LANG_SPANISH} «Spanish»
LangString Name ${LANG_SWEDISH} «Swedish»
LangString Name ${LANG_TRADCHINESE} «Traditional Chinese»
LangString Name ${LANG_SIMPCHINESE} «Simplified Chinese»
LangString Name ${LANG_SLOVAK} «Slovak»
; Directly change the inner lang strings (Same as ComponentText)
LangString ^ComponentsText ${LANG_ENGLISH} «English component page»
LangString ^ComponentsText ${LANG_DUTCH} «Dutch component page»
LangString ^ComponentsText ${LANG_FRENCH} «French component page»
LangString ^ComponentsText ${LANG_GERMAN} «German component page»
LangString ^ComponentsText ${LANG_KOREAN} «Korean component page»
LangString ^ComponentsText ${LANG_RUSSIAN} «Russian component page»
LangString ^ComponentsText ${LANG_SPANISH} «Spanish component page»
LangString ^ComponentsText ${LANG_SWEDISH} «Swedish component page»
LangString ^ComponentsText ${LANG_TRADCHINESE} «Traditional Chinese component page»
LangString ^ComponentsText ${LANG_SIMPCHINESE} «Simplified Chinese component page»
LangString ^ComponentsText ${LANG_SLOVAK} «Slovak component page»
; A LangString for the section name
LangString Sec1Name ${LANG_ENGLISH} «English section #1»
LangString Sec1Name ${LANG_DUTCH} «Dutch section #1»
LangString Sec1Name ${LANG_FRENCH} «French section #1»
LangString Sec1Name ${LANG_GERMAN} «German section #1»
LangString Sec1Name ${LANG_KOREAN} «Korean section #1»
LangString Sec1Name ${LANG_RUSSIAN} «Russian section #1»
LangString Sec1Name ${LANG_SPANISH} «Spanish section #1»
LangString Sec1Name ${LANG_SWEDISH} «Swedish section #1»
LangString Sec1Name ${LANG_TRADCHINESE} «Trandional Chinese section #1»
LangString Sec1Name ${LANG_SIMPCHINESE} «Simplified Chinese section #1»
LangString Sec1Name ${LANG_SLOVAK} «Slovak section #1»
;--------------------------------
Function .onInit
; Language selection dialog
Push »
Push ${LANG_ENGLISH}
Push English
Push ${LANG_DUTCH}
Push Dutch
Push ${LANG_FRENCH}
Push French
Push ${LANG_GERMAN}
Push German
Push ${LANG_KOREAN}
Push Korean
Push ${LANG_RUSSIAN}
Push Russian
Push ${LANG_SPANISH}
Push Spanish
Push ${LANG_SWEDISH}
Push Swedish
Push ${LANG_TRADCHINESE}
Push «Traditional Chinese»
Push ${LANG_SIMPCHINESE}
Push «Simplified Chinese»
Push ${LANG_SLOVAK}
Push Slovak
Push A ; A means auto count languages
; for the auto count to work the first empty push (Push ») must remain
LangDLL: LangDialog «Installer Language» «Please select the language of the installer»
Pop $LANGUAGE
StrCmp $LANGUAGE «cancel» 0 +2
Abort
FunctionEnd
;--------------------------------
;--------------------------------
Page license
Page directory
Page instfiles
AutoCloseWindow false
ShowInstDetails show
Function «FinalStage»
MessageBox MB_OK «Close window and see on your screen… Thanks!»
FunctionEnd
/* Обратите внимаание на обёртку. Казалось бы, что должно
сработать без секции, просто вызов и на этом закончили, но нет… :( */
Section «Final»
Call «FinalStage»
SectionEnd
Function .onGUIEnd /* Есть и другие колбэки */
ClearErrors
/* При чистой установке можно было обойтись
копированием django в site-packages, однако использование setup.py
мне показалось более кошерным, учитывая мета-информацию. Я питон не знаю
и значит не знаю, где и как аукнется отсутствие вызова setup (). */
FileOpen $0 $INSTDIR\installer.bat w
IfErrors done
FileWrite $0 «cd $INSTDIR $\npython setup.py install» /* Абракадабра с регистрами мне не всегда нравится */
/*Здесь нужно отметить, что добавление в батник чего-то вроде del /Q $INSTDIR\installer.bat к хорошему не приводит.
Буду благодарен, если мне подскажут, почему так происходит, ибо это не нагуглишь и интуитивно представляется,
что если уж команда запустилась, то наличие или отсутствие файла с командой роли не играет, ан нет же…*/
FileClose $0
done:
Exec »$INSTDIR\installer.bat»
FunctionEnd
/* Можно было бы сделать страницу с выбором папки питона, на случай, если питон не прописан в PATH -
именно так было бы идеально, но я не маньяк-перфекционист и сделаю это, только если вдруг установщик
кому-то понравится и мне будет указано на недочёт. */
А ниже — скрин одного из результатов при обкатывании генератора.
Осталось только вторично достучаться до сообщества Django и сделать мелочи типа указания версии, имени файла и т.п…Надеюсь, что этот пост оказался кому-то полезным и достаточно информативным.
