GUI установщик Django 1.6.2 под Windows

в 21:00, , рубрики: Без рубрики
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 raising
arm and legal arm of the Django project.

If you've got ideas for how Django can be improved, you should direct
those ideas to the django-developers mailing list, or open a ticket on
Django's ticket tracker.

Better still, try your hand at implementing the features you want to
see, and provide those changes as a patch against the Django codebase.
If you can provide a better user experience for Windows or Cyrillic
users, 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. Итак...

Простейший код, не требующий особенных пояснений, для первого знакомства с общими принципами:

Познакомиться

  1. # define installer name
  2. OutFile "installer.exe"
  3.  
  4. # set desktop as install directory
  5. InstallDir $DESKTOP
  6.  
  7. # default section start
  8. Section
  9.  
  10. # define output path
  11. SetOutPath $INSTDIR
  12.  
  13. # specify file to go in output path
  14. File test.txt
  15.  
  16. # define uninstaller name
  17. WriteUninstaller $INSTDIRuninstaller.exe
  18.  
  19.  
  20. #-------
  21. # default section end
  22. SectionEnd
  23.  
  24. # create a section to define what the uninstaller does.
  25. # the section will always be named "Uninstall"
  26. Section "Uninstall"
  27.  
  28. # Always delete uninstaller first
  29. Delete $INSTDIRuninstaller.exe
  30.  
  31. # now delete installed file
  32. Delete $INSTDIRtest.txt
  33.  
  34. 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's
site-packages directory, which is located wherever your Python installation
lives. 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:\PYTHONsite-packages (Windows)

Однако именно из-за того, что я не знаю питон, предпочёл более кошерное применение setup.py, так как доподлинно мне не известно, чем отзовётся отсутствие setup(). Вот, что получилось у меня после адаптации bigtest.nsi. В комментариях особо отмечены места, которые не оказываются интуитивно понятными, с которыми пришлось повозиться, пока всё пошло, как нужно.

Полный код скрипта установщика

  1. !include "IncludeLogicLib.nsh"
  2.  
  3. OutFile "installer.exe"
  4. Name "Django 1.6.2"
  5. Caption "Django 1.6.2"
  6. Icon "${NSISDIR}ContribGraphicsIconsnsis1-install.ico"
  7. CRCCheck on
  8. SilentInstall normal
  9. BGGradient 000000 0000FF FFFFFF
  10. InstallColors FF8080 000030
  11. XPStyle on
  12. InstallDir $EXEDIR /* Обеспечиввает установку по умолчанию туда, куда был сохранён установщик */
  13. SetFont "Tahoma" 10 /* Нравится мне этот шрифт и всё тут */
  14. SetOverWrite on
  15. #-NonSectionInstructionsEnd-
  16. Section "BaseInstructions"
  17. SetDatablockOptimize on
  18. /* Без установки выходного пути для файлов установщик ругался
  19. на невозможность записи уже во время выполнения,
  20. хотя кажется, что необходимо достаточно InstallDir */
  21. SetOutPath "$INSTDIR*" 
  22. SetOverWrite on
  23. File /r "C:insDjango-1.6.2*" /* Та папка, в которой у меня лежал Django.
  24. Именно так можно включить в установщик нужные файлы */
  25. SectionEnd
  26.  
  27. RequestExecutionLevel highest /* От греха подальше... */
  28. CheckBitmap "${NSISDIR}ContribGraphicsChecksclassic-cross.bmp"
  29. LicenseText "Django license"
  30. LicenseData "license.rtf" /* Отделался... лень =) */
  31.  
  32. /* "Сколько языков ты знаешь, столько раз ты человек" =)
  33. NSIDIR, как можно понять, папка установки генератора.
  34. Весь язык сам по себе замечателен интуитивной понятностью */
  35.  
  36. LoadLanguageFile "${NSISDIR}ContribLanguage filesEnglish.nlf"
  37. LoadLanguageFile "${NSISDIR}ContribLanguage filesDutch.nlf"
  38. LoadLanguageFile "${NSISDIR}ContribLanguage filesFrench.nlf"
  39. LoadLanguageFile "${NSISDIR}ContribLanguage filesGerman.nlf"
  40. LoadLanguageFile "${NSISDIR}ContribLanguage filesKorean.nlf"
  41. LoadLanguageFile "Russian.nlf" /* Слегка подправленный файл перевода на родной */
  42. LoadLanguageFile "${NSISDIR}ContribLanguage filesSpanish.nlf"
  43. LoadLanguageFile "${NSISDIR}ContribLanguage filesSwedish.nlf"
  44. LoadLanguageFile "${NSISDIR}ContribLanguage filesTradChinese.nlf"
  45. LoadLanguageFile "${NSISDIR}ContribLanguage filesSimpChinese.nlf"
  46. LoadLanguageFile "${NSISDIR}ContribLanguage filesSlovak.nlf"
  47.  
  48. ; Set name using the normal interface (Name command)
  49. LangString Name ${LANG_ENGLISH} "English"
  50. LangString Name ${LANG_DUTCH} "Dutch"
  51. LangString Name ${LANG_FRENCH} "French"
  52. LangString Name ${LANG_GERMAN} "German"
  53. LangString Name ${LANG_KOREAN} "Korean"
  54. LangString Name ${LANG_RUSSIAN} "Russian"
  55. LangString Name ${LANG_SPANISH} "Spanish"
  56. LangString Name ${LANG_SWEDISH} "Swedish"
  57. LangString Name ${LANG_TRADCHINESE} "Traditional Chinese"
  58. LangString Name ${LANG_SIMPCHINESE} "Simplified Chinese"
  59. LangString Name ${LANG_SLOVAK} "Slovak"
  60.  
  61. ; Directly change the inner lang strings (Same as ComponentText)
  62. LangString ^ComponentsText ${LANG_ENGLISH} "English component page"
  63. LangString ^ComponentsText ${LANG_DUTCH} "Dutch component page"
  64. LangString ^ComponentsText ${LANG_FRENCH} "French component page"
  65. LangString ^ComponentsText ${LANG_GERMAN} "German component page"
  66. LangString ^ComponentsText ${LANG_KOREAN} "Korean component page"
  67. LangString ^ComponentsText ${LANG_RUSSIAN} "Russian component page"
  68. LangString ^ComponentsText ${LANG_SPANISH} "Spanish component page"
  69. LangString ^ComponentsText ${LANG_SWEDISH} "Swedish component page"
  70. LangString ^ComponentsText ${LANG_TRADCHINESE} "Traditional Chinese component page"
  71. LangString ^ComponentsText ${LANG_SIMPCHINESE} "Simplified Chinese component page"
  72. LangString ^ComponentsText ${LANG_SLOVAK} "Slovak component page"
  73.  
  74. ; A LangString for the section name
  75. LangString Sec1Name ${LANG_ENGLISH} "English section #1"
  76. LangString Sec1Name ${LANG_DUTCH} "Dutch section #1"
  77. LangString Sec1Name ${LANG_FRENCH} "French section #1"
  78. LangString Sec1Name ${LANG_GERMAN} "German section #1"
  79. LangString Sec1Name ${LANG_KOREAN} "Korean section #1"
  80. LangString Sec1Name ${LANG_RUSSIAN} "Russian section #1"
  81. LangString Sec1Name ${LANG_SPANISH} "Spanish section #1"
  82. LangString Sec1Name ${LANG_SWEDISH} "Swedish section #1"
  83. LangString Sec1Name ${LANG_TRADCHINESE} "Trandional Chinese section #1"
  84. LangString Sec1Name ${LANG_SIMPCHINESE} "Simplified Chinese section #1"
  85. LangString Sec1Name ${LANG_SLOVAK} "Slovak section #1"
  86.  
  87.  
  88.  
  89. ;--------------------------------
  90.  
  91. Function .onInit
  92.  
  93. ;Language selection dialog
  94.  
  95. Push ""
  96. Push ${LANG_ENGLISH}
  97. Push English
  98. Push ${LANG_DUTCH}
  99. Push Dutch
  100. Push ${LANG_FRENCH}
  101. Push French
  102. Push ${LANG_GERMAN}
  103. Push German
  104. Push ${LANG_KOREAN}
  105. Push Korean
  106. Push ${LANG_RUSSIAN}
  107. Push Russian
  108. Push ${LANG_SPANISH}
  109. Push Spanish
  110. Push ${LANG_SWEDISH}
  111. Push Swedish
  112. Push ${LANG_TRADCHINESE}
  113. Push "Traditional Chinese"
  114. Push ${LANG_SIMPCHINESE}
  115. Push "Simplified Chinese"
  116. Push ${LANG_SLOVAK}
  117. Push Slovak
  118. Push A ; A means auto count languages
  119.        ; for the auto count to work the first empty push (Push "") must remain
  120. LangDLL::LangDialog "Installer Language" "Please select the language of the installer"
  121.  
  122. Pop $LANGUAGE
  123. StrCmp $LANGUAGE "cancel" 0 +2
  124. Abort
  125. FunctionEnd
  126.  
  127. ;--------------------------------
  128.  
  129. ;--------------------------------
  130.  
  131. Page license
  132. Page directory
  133. Page instfiles
  134.  
  135. AutoCloseWindow false
  136. ShowInstDetails show
  137.  
  138. Function "FinalStage"
  139. MessageBox MB_OK "Close window and see on your screen... Thanks!"
  140. FunctionEnd
  141.  
  142. /* Обратите внимаание на обёртку. Казалось бы, что должно
  143. сработать без секции, просто вызов и на этом закончили, но нет... :( */
  144. Section "Final"
  145. Call "FinalStage"
  146. SectionEnd
  147.  
  148. Function .onGUIEnd /* Есть и другие колбэки */
  149. ClearErrors
  150. /* При чистой установке можно было обойтись
  151. копированием django в site-packages, однако использование setup.py
  152. мне показалось более кошерным, учитывая мета-информацию. Я питон не знаю
  153. и значит не знаю, где и как аукнется отсутствие вызова setup(). */
  154. FileOpen $0 $INSTDIRinstaller.bat w
  155. IfErrors done
  156. FileWrite $0 "cd $INSTDIR $npython setup.py install" /* Абракадабра с регистрами мне не всегда нравится */
  157. /*Здесь нужно отметить, что добавление в батник чего-то вроде del /Q $INSTDIRinstaller.bat к хорошему не приводит.
  158. Буду благодарен, если мне подскажут, почему так происходит, ибо это не нагуглишь и интуитивно представляется,
  159. что если уж команда запустилась, то наличие или отсутствие файла с командой роли не играет, ан нет же...*/
  160. FileClose $0
  161. done:
  162. Exec "$INSTDIRinstaller.bat"
  163. FunctionEnd
  164.  
  165. /* Можно было бы сделать страницу с выбором папки питона, на случай, если питон не прописан в PATH -
  166. именно так было бы идеально, но я не маньяк-перфекционист и сделаю это, только если вдруг установщик
  167. кому-то понравится и мне будет указано на недочёт. */

А ниже — скрин одного из результатов при обкатывании генератора.GUI установщик Django 1.6.2 под Windows

Осталось только вторично достучаться до сообщества Django и сделать мелочи типа указания версии, имени файла и т.п…
Надеюсь, что этот пост оказался кому-то полезным и достаточно информативным.

Автор: stranger777

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js