Build a Python Web Application Installer Including Apache, Django, and PostgreSQL for Windows





This post is a continuation of the first part of the article on Habré , where it was described in detail about deploying the Django stack on MS Windows. Next, we will provide step-by-step instructions for creating an installer that will automate the process of installing the stack on other computers without the need to work in the command line, creating virtual machines, etc., where the whole sequence of actions will be reduced to the actions Next -> Next -> Finish.



So what the installer should do:



  1. Unpack all necessary programs and components into a directory specified by the user.
  2. Perform pre-installation checks.
  3. Register the Python interpreter in the Windows registry.
  4. Install, if not already installed, software dependency libraries.
  5. Create Apache and PostgreSQL services, then start them.
  6. An additional plus will be the automatic creation of an uninstaller program that will remove the installed stack if the user wants it.


Among the possible options for installers, we will choose the free installer Inno Setup, because it allows you to do all of the above, allowing you to create installers without having to run a lot of scripts. Compared to Wix, the setup file syntax is ini format, which is easier to read and change than xml. Today, it competes with and even surpasses many commercial installers in feature set and stability.



Best of all, no scripting at all is required to create a basic installer, as Inno Setup comes with a graphical wizard that does a surprisingly good job of basic installers.



The installation logic can be written in Pascal language rather than convoluted custom actions in Wix. Its only drawback is that it only creates exe, msi file format is not supported.



Step 1.Inno Setup Installation



Additional comments are not needed here, since downloading and installing the installer program is trivial.



Step 2: Script the Inno Setup Installation



Let's create a stub Inno Setup script (* .iss file) using the Installation Script Wizard.



















































As a result, a * .iss file will be created with the following contents:
; Script generated by the Inno Setup Script Wizard.

; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!



#define MyAppName "Severcart"

#define MyAppVersion "1.21.0"

#define MyAppPublisher "Severcart Inc."

#define MyAppURL "https://www.severcart.ru/"



[Setup]

; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.

; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)

AppId={{4FAF87DC-4DBD-42CE-A2A2-B6D559E76BDC}

AppName={#MyAppName}

AppVersion={#MyAppVersion}

;AppVerName={#MyAppName} {#MyAppVersion}

AppPublisher={#MyAppPublisher}

AppPublisherURL={#MyAppURL}

AppSupportURL={#MyAppURL}

AppUpdatesURL={#MyAppURL}

DefaultDirName=c:\severcart

DefaultGroupName={#MyAppName}

; Uncomment the following line to run in non administrative install mode (install for current user only.)

;PrivilegesRequired=lowest

OutputDir=C:\Users\Developer\Desktop\Output

OutputBaseFilename=mysetup

Compression=lzma

SolidCompression=yes

WizardStyle=modern



[Languages]

Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"



[Files]

Source: "C:\severcart\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

; NOTE: Don't use "Flags: ignoreversion" on any shared system files







Step 3. Checks before installation



Before unpacking the programs to the directory and changing the registry, you need to check that TCP ports are free for Apache and PostgreSQL to work, you also need to check the minimum system requirements of Windows, because as already mentioned in the first part of this article, the installed version of Python will work only starting from the version of MS Windows 8 (kernel version 6.2) .



To perform the necessary checks, let's use the [Code] section of the installation file. Section [Code]Is an optional section that defines a Pascal script. Pascal script can be used to customize installation or uninstallation in different ways. Note that creating a Pascal script is not easy and requires experience with Inno Setup and programming skills in Pascal or at least a similar programming language. To check the availability of TCP ports, we will create the following function: We will call the test functions in the InitializeSetup function , which is called during installation initialization. Returns False to cancel the installation, otherwise True .



function IsWindowsVersionOrNewer(Major, Minor: Integer): Boolean;

var

Version: TWindowsVersion;

begin

GetWindowsVersionEx(Version);

Result := (Version.Major > Major) or ((Version.Major = Major) and (Version.Minor >= Minor));

end;



function IsWindows8OrNewer: Boolean;

begin

Result := IsWindowsVersionOrNewer(6, 2);

end;







function CheckPortOccupied(Port:String):Boolean;

var

ResultCode: Integer;

begin

Exec(ExpandConstant('{cmd}'), '/C netstat -na | findstr'+' /C:":'+Port+' "', '',0,ewWaitUntilTerminated, ResultCode);

if ResultCode <> 1 then

begin

Log('this port('+Port+') is occupied');

Result := True;

end else

begin

Result := False;

end;

end;







function InitializeSetup(): Boolean;

var

port_80_check, port_5432_check: boolean;

begin

if not IsWindows8OrNewer() then begin

MsgBox(' . Windows 2012 Windows 8.0.',mbError,MB_OK);

Abort();

Result := False;

end;



port_80_check := CheckPortOccupied('8080');

if port_80_check then begin

MsgBox(' . TCP 8080 .',mbError,MB_OK);

Abort();

Result := False;

end;



port_5432_check := CheckPortOccupied('5432');

if port_5432_check then begin

MsgBox(' . TCP 5432 .',mbError,MB_OK);

Result := False;

Abort();

end;

Result := True;



Step 4. Register Python in the Windows registry



This optional section defines any registry keys / values ​​that the installer must create or modify on the user's system.



To do this, add the PYTHONPATH and PYTHONHOME keys and update the Path variable .



sys.path contains a list of strings that provide search locations for modules and packages for a future Python project. It is initialized from the PYTHONPATH environment variable and other settings.



PYTHONHOME is the Python home directory.



PATH is an environment variable that the OS uses to find executables in the command line or terminal window.



[Registry]



Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \

ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}\python;{app}\python\Scripts"



Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \

ValueType: expandsz; ValueName: "PYTHONPATH"; ValueData: "{app}\python"



Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \

ValueType: expandsz; ValueName: "PYTHONHOME"; ValueData: "{app}\python"



Step 5. Create configuration files for Apache and PostgreSQL services



To create configuration files, we will use 2 Python scripts that will generate configuration based on the user-specified installation path.



The scripts will be called in the [Run] section of the installer.



The [Run] section is optional and specifies any number of programs to run after successfully installing the program, but before the installer displays the last dialog box.



Next, in the same section, add the hidden installation of the Visual Studio Redistributable Packages without which Apache and PostgreSQL services will not work. Content of the create_http_conf.py file



[Run]



Filename: "{app}\common\VC_redist.x86apache.exe"; Parameters: "/install /passive"; Flags: waituntilterminated

Filename: "{app}\common\vcredist_x86pg.exe"; Parameters: "/install /passive"; Flags: runhidden;

Filename: "{app}\python\python.exe" ;Parameters: "{app}\common\create_http_conf.py"; Flags: runhidden

Filename: "{app}\python\python.exe" ;Parameters: "{app}\common\edit_pg_conf.py"; Flags: runhidden

Filename: "{app}\common\install.bat";Flags: runhidden

Filename: "{app}\common\services_start.bat"; Flags: runhidden







#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import sys, os


base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base_path_un = base_path.replace('\\', '/')
apache_conf_path = os.path.join(base_path, 'Apache24', 'conf', 'extra', 'httpd-wsgi.conf')

print('base_path=',base_path)

CONF = """

LoadFile "%(base)s/python/python39.dll"
LoadModule wsgi_module "%(base)s/python/lib/site-packages/mod_wsgi/server/mod_wsgi.cp39-win32.pyd"
WSGIPythonHome "%(base)s/python"


Alias /static "%(base)s/app/static"

Alias /media "%(base)s/app/media"

<Directory "%(base)s/app/static">
    # for Apache 2.4
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

<Directory "%(base)s/app/media">
    # for Apache 2.4
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>


WSGIScriptAlias / "%(base)s/app/conf/wsgi_prod.py"
WSGIPythonPath "%(base)s/python/"

<Directory "%(base)s/app/conf/">
<Files wsgi_prod.py>
    Require all granted
</Files>   
</Directory>

"""
conf_content = CONF % {'base': base_path_un}

with open(apache_conf_path, 'w') as fp:
    fp.write(conf_content)


# Read in the file
apache_main = os.path.join(base_path, 'Apache24', 'conf', 'httpd.conf')
with open(apache_main, 'r') as file :
	filedata = file.read()

# Replace the target string
replace_pattern = 'Define SRVROOT "%(base)s/Apache24"' % {'base' : base_path_un}
find_pattern = 'Define SRVROOT "C:/severcart/Apache24"'

filedata = filedata.replace(find_pattern, replace_pattern)

# Write the file out again
with open(apache_main, 'w') as file:
	file.write(filedata)



Content edit_pg_conf.py



#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import sys, os

"""
c:/djangostack/postgresql/bin/postgres.exe "-D" "c:\djangostack\postgresql\data"
"""


base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
base_path_un = base_path.replace('\\', '/')
pg_conf_path = os.path.join(base_path, 'postgresql', 'data', 'postmaster.opts')


# Read in the file
pg_conf_path = os.path.join(base_path, 'postgresql', 'data', 'postmaster.opts')
with open(pg_conf_path, 'r') as file :
	filedata = file.read()


# Replace the target string
replace_pattern = base_path_un + '/'
find_pattern = "C:/severcart/"


filedata = filedata.replace(find_pattern, replace_pattern)

# Write the file out again
with open(pg_conf_path, 'w') as file:
	file.write(filedata)


Install.bat file Contents services_start.bat file



@echo off



..\Apache24\bin\httpd.exe -k install -n "Apache" > install.log 2>&1



..\postgresql\bin\pg_ctl.exe register -N "PostgreSQL" -D ..\postgresql\data > install.log 2>&1







@echo off



net start "Apache"



net start "PostgreSQL"



Step 6: create the uninstaller



For any installer, it is also necessary to provide for the possibility of creating an uninstaller program. Fortunately, Inno Setup will do the job for us, except for some steps that need to be taken to clean up traces of the program's presence in the OS.



To do this, in the [UninstallRun] section, we will register the execution of the Windows bat script to stop the installed services, as well as to remove them. The contents of the bat script: The script stops the services, then removes the Apache and PostgreSQL services from the list of Windows system services.



[UninstallRun]

Filename: "{app}\common\remove.bat"; Flags: runhidden







@echo off



SC STOP Apache

SC STOP PostgreSQL



SC DELETE Apache

SC DELETE PostgreSQL







Step 7. Signing the executable file of the developer's ES installer



Code signing certificates are used by software developers to digitally sign applications and programs to prove that the file uploaded by the user is genuine and has not been tampered with. This is especially important for publishers who distribute their software through third party download sites that they have no control over. Major operating systems will show end users an error message if the software they are trying to install is not signed by a trusted certification authority.



You can buy a PFX developer certificate, for example, here . The certificate is purchased for a year.



The penultimate step to work with the installer will be to automatically launch the signtool.exe program to sign the finished installer in exe format after the Inno Setup program completes its work. SignTool is a command line program that digitally signs files, verifies file signatures and file timestamps. By default, the signtool.exe program is not included in the Windows distribution, so we download and install the Windows 10 SDK .



After the installation is complete, you will find signtool.exe in the directories:



  • x86 -> c: \ Program Files (x86) \ Windows Kits \ 10 \ bin \ x86 \
  • x64 -> c: \ Program Files (x86) \ Windows Kits \ 10 \ bin \ x64 \


For those who want to get acquainted with the signing program for more details, visit the official website of the developer . It lists all command line options and usage examples. Let's move on.



Next, let's set up automatic signing of the file. Select "Configure Sign Tools ..." from the "Tools" menu .







Next, click on the "Add" button.







Give the tool a name. This is the name you will use when referring to the tool in the installer scripts. I named mine signtool because I am using signtool.exe.







Paste in the text that you use to sign executable files from the command line. Replace the name of the file to be signed with $ f. Inno Setup will replace the $ f variable with the signed file.



"C: \ Program Files (x86) \ Windows Kits \ 10 \ bin \ x86 \ signtool.exe" sign / f "C: \ MY_CODE_SIGNING.PFX" / t timestamp.comodoca.com/authenticode / p MY_PASSWORD $ f







After pressing OK you are done configuring the signature tool.







Let's add the following script to the [Setup] section to use the signature tool we just configured. This assumes you named your tool signtool.



SignTool=signtool



Step 8. Assembling the installer



Final InnoSetup installer file
#define MyAppName «Severcart»

#define MyAppVersion «1.21.0»

#define MyAppPublisher «Severcart Inc.»

#define MyAppURL «www.severcart.ru»



[Setup]

; NOTE: The value of AppId uniquely identifies this application.

; Do not use the same AppId value in installers for other applications.

; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)

SignTool=signtool

AppId={{2CF113D5-B49D-47EF-B85F-AE06EB0E78EB}}

AppName={#MyAppName}

AppVersion={#MyAppVersion}

;AppVerName={#MyAppName} {#MyAppVersion}

AppPublisher={#MyAppPublisher}

AppPublisherURL={#MyAppURL}

AppSupportURL={#MyAppURL}

AppUpdatesURL={#MyAppURL}

DefaultDirName=c:\severcart

DefaultGroupName={#MyAppName}

OutputBaseFilename=setup

Compression=lzma

SolidCompression=yes

ChangesEnvironment=yes



; Uninstall options

Uninstallable=yes

CreateUninstallRegKey=yes

;WizardSmallImageFile=logo3.bmp



[Icons]

Name: "{userdesktop}\severcart"; Filename: «127.0.0.1:8080/»



[Languages]

Name: «russian»; MessagesFile: «compiler:Languages\Russian.isl»



[Files]

Source: «C:\severcart\*»; Excludes: "*.pyc"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs



[Registry]

Root: HKLM; Subkey: «SYSTEM\CurrentControlSet\Control\Session Manager\Environment»; \

ValueType: expandsz; ValueName: «Path»; ValueData: "{olddata};{app}\python;{app}\python\Scripts"



Root: HKLM; Subkey: «SYSTEM\CurrentControlSet\Control\Session Manager\Environment»; \

ValueType: expandsz; ValueName: «PYTHONPATH»; ValueData: "{app}\python"



Root: HKLM; Subkey: «SYSTEM\CurrentControlSet\Control\Session Manager\Environment»; \

ValueType: expandsz; ValueName: «PYTHONHOME»; ValueData: "{app}\python"



[Run]

Filename: "{app}\common\VC_redist.x86apache"; Parameters: "/install /passive"; Flags: waituntilterminated

Filename: "{app}\common\vcredist_x86pg"; Parameters: "/install /passive"; Flags: runhidden;

Filename: "{app}\python\python.exe" ;Parameters: "{app}\common\create_http_conf.py"; Flags: runhidden

Filename: "{app}\python\python.exe" ;Parameters: "{app}\common\edit_pg_conf.py"; Flags: runhidden

Filename: "{app}\common\install.bat";Flags: runhidden

Filename: "{app}\common\services_start.bat"; Flags: runhidden





[UninstallRun]

Filename: "{app}\common\remove.bat"; Flags: runhidden



[Code]

function IsWindowsVersionOrNewer(Major, Minor: Integer): Boolean;

var

Version: TWindowsVersion;

begin

GetWindowsVersionEx(Version);

Result :=

(Version.Major > Major) or

((Version.Major = Major) and (Version.Minor >= Minor));

end;



function IsWindows8OrNewer: Boolean;

begin

Result := IsWindowsVersionOrNewer(6, 2);

end;



function CheckPortOccupied(Port:String):Boolean;

var

ResultCode: Integer;

begin

Exec(ExpandConstant('{cmd}'), '/C netstat -na | findstr'+' /C:":'+Port+' "', '',0,ewWaitUntilTerminated, ResultCode);

if ResultCode <> 1 then

begin

Log('this port('+Port+') is occupied');

Result := True;

end else

begin

Result := False;

end;

end;



function InitializeSetup(): Boolean;

var

port_80_check, port_5432_check: boolean;

begin

if not IsWindows8OrNewer() then begin

MsgBox(' . Windows 2012 Windows 8.0.',mbError,MB_OK);

Abort();

Result := False;

end;



port_80_check := CheckPortOccupied('8080');

if port_80_check then begin

MsgBox(' . TCP 8080 .',mbError,MB_OK);

Abort();

Result := False;

end;



port_5432_check := CheckPortOccupied('5432');

if port_5432_check then begin

MsgBox(' . TCP 5432 .',mbError,MB_OK);

Result := False;

Abort();

end;

Result := True;



end;











Step 9. Checking the work of the installer







































That's all, thanks for your attention.



All Articles