The excellent cx_Freeze project makes it easy to build Windows executables for Python scripts. It also has some handy glue code to enable you to install and run a Python script as a Windows service; and includes a simple Windows service example in the project's samples directory.
It's also possible to package up the service into an MSI file (which can be installed via the msiexec tool included with Windows). However, I didn't find any good examples for how to do this; so this is what I did for a recent project:
- Set Up Basic Project With UV
- Set Up Basic Windows Service
- Build and Test Windows Service
- Build Basic MSI Package
- Configure MSI to Install and Start the Service
- Extra: Add Other Executables
- Extra: Add Icons
- Extra: Add Other Files
- Extra: Add Start Menu
Set Up Basic Project With UV
I set up a basic project with UV, using a pyproject.toml file that looks like this:
# pyproject.toml
[project]
name = "my_service"
dynamic = ["version"]
description = "My service description."
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.12"
dependencies = [
"pywin32~=306.0 ; sys_platform == 'win32'",
]
[project.scripts]
my-service-cli = "my_service.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pytest>=8.3.4",
"ruff>=0.8.6",
]
freeze = [
"cx-freeze>=6.13.2",
"cx-logging>=3.0",
]
[tool.hatch.version]
path = "my_service/__init__.py"
I put the cx_Freeze-specific dependencies in my own custom freeze dependency group, as I only need them to be installed when running cx_Freeze commands.
In the my_service package, I set up globals for the version number and service details:
# src/my_service/__init__.py
"""My Service."""
__version__ = "1.0.0"
DISPLAY_NAME = "My Service"
DESCRIPTION = "My service description."
SERVICE_NAME = "my-service"
Set Up Basic Windows Service
Next I set up a basic service class (similar to the Handler class in the ServiceHandler.py file of cx_Freeze's service sample) to run my service code (and to set up some log files):
# src/my_service/windows_service.py
"""cx_Freeze Win32Service for the my service."""
import sys
from pathlib import Path
try:
import cx_Logging
except ImportError:
cx_Logging = None
from my_service.service import run_my_service
class Service:
"""cx_Freeze Win32Service for the agent."""
def initialize(self, cnf_file):
"""Called when the service is starting.
Arguments:
cnf_file (str): Path to configuration file.
"""
try:
init_log()
except Exception:
cx_Logging.LogException()
def run(self):
"""Called when the service is running."""
try:
cx_Logging.Debug("running my service")
run_my_service()
except Exception:
cx_Logging.LogException()
def stop(self):
"""Called when the service is stopping."""
self.cnf.loop = 0
def init_log():
"""Initializes service logging."""
log_dir = get_log_dir()
cx_Logging.StartLogging(str(log_dir / "init.log"), cx_Logging.DEBUG)
sys.stdout = open(log_dir / "stdout.log", "a")
def get_log_dir():
"""Gets service logging directory.
Returns:
str: Path to logging directory.
"""
executable_dir = Path(sys.executable).parent
log_dir = executable_dir / "log"
Path.mkdir(log_dir, parents=True, exist_ok=True)
return log_dir
Then I set up the service-definition config for it (similar to the Config.py file of cx_Freeze's service sample), using some of the constants I had defined in my root my_service module:
# src/my_service/windows_service_config.py
"""cx_Freeze config for my service as a Win32Service."""
import my_service
NAME = f"{my_service.SERVICE_NAME}-%s"
DISPLAY_NAME = f"{my_service.DISPLAY_NAME} %s"
MODULE_NAME = "my_service.windows_service"
CLASS_NAME = "Service"
DESCRIPTION = my_service.DESCRIPTION
AUTO_START = False
SESSION_CHANGES = False
And then I put together a cx_Freeze setup script (similar to the setup.py file of cx_Freeze's service sample) to run the cx_Freeze build_exe build, again using some of the constants I had defined in my root my_service module:
# cx_freeze_setup.py
"""cx_Freeze setup script."""
from cx_Freeze import Executable, setup
from my_service import DESCRIPTION, SERVICE_NAME
from my_service import __version__ as VERSION
EXECUTABLES = [
Executable(
script="src/my_service/windows_service_config.py",
base="Win32Service",
target_name=SERVICE_NAME,
),
]
setup(
name=SERVICE_NAME,
version=VERSION,
description=DESCRIPTION,
options={
"build_exe": {
"excludes": [
"test",
"tkinter",
"unittest",
],
"includes": [
"_cffi_backend",
"cx_Logging",
],
"include_msvcr": True,
"packages": [
"my_service",
],
},
},
executables=EXECUTABLES,
)
Build and Test Windows Service
With that in place, I could run cx_Freeze's build_exe command to build my service as a Windows executable, generating a my-service-svc.exe file in my project's build\exe.win-amd64-3.12 directory; and alongside the exe file, a lib folder containing all the compiled Python modules needed for the executable, plus some core DLLs:
> uv run --group freeze cx_freeze_setup.py build_exe
Using CPython 3.12.9
Creating virtual environment at: .venv
Built my-service @ file:///C:/my_project
Installed 31 packages in 44.15s
running build_exe
creating directory C:\my_project\build\exe.win-amd64-3.12
copying C:\my_project\.venv\Lib\site-packages\cx_Freeze\bases\Win32Service-cpython-312-win_amd64.exe -> C:\my_project\build\exe.win-amd64-3.12\my-service.exe
...
copying C:\my_project\.venv\Lib\site-packages\pywin32_system32\pywintypes312.dll -> C:\my_project\build\exe.win-amd64-3.12\lib\pywintypes312.dll
writing zip file C:\my_project\build\exe.win-amd64-3.12\lib\library.zip
Name File
---- ----
m BUILD_CONSTANTS C:\Users\JUSTIN~1\AppData\Local\Temp\2\cxfreeze-8edcizah\BUILD_CONSTANTS.py
...
m zipimport C:\Users\Justin\AppData\Roaming\uv\python\cpython-3.12.9-windows-x86_64-none\Lib\zipimport.py
Missing modules:
? OpenSSL.SSL imported from urllib3.contrib.pyopenssl
...
? zstandard imported from urllib3.response, urllib3.util.request
This is not necessarily a problem - the modules may not be needed on this platform.
Missing dependencies:
? api-ms-win-core-path-l1-1-0.dll
...
? api-ms-win-crt-utility-l1-1-0.dll
This is not necessarily a problem - the dependencies may not be needed on this platform.
> dir build\exe.win-amd64-3.12
Volume in drive C is Windows
Volume Serial Number is 7EC2-1A39
Directory of C:\my_project\build\exe.win-amd64-3.12
02/13/2025 12:19 AM <DIR> .
02/13/2025 12:19 AM <DIR> ..
02/13/2025 12:10 AM 44,032 cx_Logging.cp312-win_amd64.pyd
02/13/2025 12:14 AM 3,326 frozen_application_license.txt
02/13/2025 12:19 AM <DIR> lib
02/13/2025 12:19 AM 139,264 my-service.exe
02/13/2025 12:04 AM 6,925,312 python312.dll
02/13/2025 12:06 AM 99,840 vcruntime140.dll
02/13/2025 12:06 AM 29,184 vcruntime140_1.dll
6 File(s) 7,623,119 bytes
3 Dir(s) 10,450,911,232 bytes free
This build\exe.win-amd64-3.12 directory is basically what I want to package up and ship to users. In this state, I can also test out the Windows service by manually installing it and running it on the build box:
> .\build\exe.win-amd64-3.12\my-service.exe --install default
Service installed.
> sc start my-service-default
SERVICE_NAME: start my-service-default
TYPE : 10 WIN32_OWN_PROCESS
STATE : 2 START_PENDING
(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x7d0
PID : 1732
FLAGS :
> sc stop my-service-default
SERVICE_NAME: my-service-default
TYPE : 10 WIN32_OWN_PROCESS
STATE : 1 STOPPED
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
> .\build\exe.win-amd64-3.12\my-service.exe --uninstall default
Service uninstalled.
Build Basic MSI Package
Next I can use cx_Freeze's bdist_msi command to create an MSI package that doesn't install or start my service, but simply installs the contents of the build\exe.win-amd64-3.12 into the Program Files directory of a user's Windows machine. First, however, I added a bit more configuration to my cx_Freeze setup script for the bdist_msi command options:
# cx_freeze_setup.py
"""cx_Freeze setup script."""
from cx_Freeze import Executable, setup
from my_service import DESCRIPTION, SERVICE_NAME
from my_service import __version__ as VERSION
EXECUTABLES = [
Executable(
script="src/my_service/windows_service_config.py",
base="Win32Service",
target_name=SERVICE_NAME,
),
]
setup(
name=SERVICE_NAME,
version=VERSION,
description=DESCRIPTION,
options={
"build_exe": {
"excludes": [
"test",
"tkinter",
"unittest",
],
"includes": [
"_cffi_backend",
"cx_Logging",
],
"include_msvcr": True,
"packages": [
"my_service",
],
},
"bdist_msi": {
"all_users": True,
"initial_target_dir": f"[ProgramFiles64Folder]{DISPLAY_NAME}",
# IMPORTANT: generate a unique UUID for your service
"upgrade_code": "{26483DF9-540E-43D7-B543-795C62E3AF2D}",
},
},
executables=EXECUTABLES,
)
You should generate a separate UUID for each project, and then keep than UUID constant for the project's liftetime, so that Windows will know which existing package to upgrade when the user tries to install a new version of it. I generated the UUID for my service by running this command on a Linux box:
$ tr [a-z] [A-Z] < /proc/sys/kernel/random/uuid
Building the MSI is now as simple as running the following command:
> uv run --group freeze cx_freeze_setup.py bdist_msi
running bdist_msi
running build
running build_exe
creating directory C:\my_project\build\exe.win-amd64-3.12
copying C:\my_project\.venv\Lib\site-packages\cx_Freeze\bases\Win32Service-cpython-312-win_amd64.exe -> C:\my_project\build\exe.win-amd64-3.12\my-service.exe
...
? api-ms-win-crt-utility-l1-1-0.dll
This is not necessarily a problem - the dependencies may not be needed on this platform.
installing to build\bdist.win-amd64\msi
running install_exe
creating build\bdist.win-amd64\msi
...
copying build\exe.win-amd64-3.12\vcruntime140_1.dll -> build\bdist.win-amd64\msi
creating dist
removing 'build\bdist.win-amd64\msi' (and everything under it)
> dir dist
Volume in drive C is Windows
Volume Serial Number is 7EC2-1A39
Directory of C:\my_project\dist
02/13/2025 01:05 AM <DIR> .
02/13/2025 01:05 AM <DIR> ..
02/13/2025 01:05 AM 10,788,864 my-service-1.0.0-win64.msi
1 File(s) 10,788,864 bytes
2 Dir(s) 10,826,158,080 bytes free
This will automatically run the same build_exe command as before, but after that will also create an MSI file with the contents of the generated build\exe.win-amd64-3.12 directory, and save it as the dist\my-service-1.0.0-win64.msi file.
This dist\my-service-1.0.0-win64.msi file can be run directly to launch a graphical installer; or it can be run with the msiexec utility to install silently with no prompts:
> msiexec /i dist\my-service-1.0.0-win64.msi /qn
Configure MSI to Install and Start the Service
To enable my service to install and start automatically when the user installs the MSI package, I had to configure the bdist_msi command with some special MSI data tables for installing a Windows service (ServiceInstall), and for running it (ServiceControl):
# cx_freeze_setup.py
"""cx_Freeze setup script."""
from re import sub
from cx_Freeze import Executable, setup
from my_service import DESCRIPTION, SERVICE_NAME
from my_service import __version__ as VERSION
SERVICE_DEFAULT=default
SERVICE_WIN32_OWN_PROCESS = 0x10
SERVICE_AUTO_START = 0x2
SERVICE_ERROR_NORMAL = 0x1
MSIDB_SERVICE_CONTROL_EVENT_START = 0x1
MSIDB_SERVICE_CONTROL_EVENT_STOP = 0x2
MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_STOP = 0x20
MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_DELETE = 0x80
EXECUTABLES = [
Executable(
script="src/my_service/windows_service_config.py",
base="Win32Service",
target_name=SERVICE_NAME,
),
]
def _make_component_id(executables, index):
executable = executables[index]
component = f"_cx_executable{index}_{executable}"
return sub(r"[^\w.]", "_", component)
setup(
name=SERVICE_NAME,
version=VERSION,
description=DESCRIPTION,
options={
"build_exe": {
"excludes": [
"test",
"tkinter",
"unittest",
],
"includes": [
"_cffi_backend",
"cx_Logging",
],
"include_msvcr": True,
"packages": [
"my_service",
],
},
"bdist_msi": {
"all_users": True,
"initial_target_dir": f"[ProgramFiles64Folder]{DISPLAY_NAME}",
# IMPORTANT: generate a unique UUID for your service
"upgrade_code": "{26483DF9-540E-43D7-B543-795C62E3AF2D}",
"data": {
"ServiceInstall": [
(
f"{SERVICE_NAME}-{SERVICE_DEFAULT}Install", # ID
f"{SERVICE_NAME}-{SERVICE_DEFAULT}", # Name
DISPLAY_NAME, # DisplayName
SERVICE_WIN32_OWN_PROCESS, # ServiceType
SERVICE_AUTO_START, # StartType
SERVICE_ERROR_NORMAL, # ErrorControl
None, # LoadOrderGroup
None, # Dependencies
None, # StartName
None, # Password
None, # Arguments
_make_component_id(EXECUTABLES, 0), # Component
DESCRIPTION, # Description
),
],
"ServiceControl": [
(
f"{SERVICE_NAME}-{SERVICE_DEFAULT}Control", # ID
f"{SERVICE_NAME}-{SERVICE_DEFAULT}", # Name
(
MSIDB_SERVICE_CONTROL_EVENT_START
+ MSIDB_SERVICE_CONTROL_EVENT_STOP
+ MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_STOP
+ MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_DELETE
), # Event
None, # Arguments
0, # Wait
_make_component_id(EXECUTABLES, 0), # Component
),
],
},
},
},
executables=EXECUTABLES,
)
In the above, I've set up a row for the ServiceInstall MSI table to direct the Windows installer to install my service as a Windows service, and configure it to auto-start on system boot. This is basically the equivalent of running the following two commands:
> .\build\exe.win-amd64-3.12\my-service.exe --install default
> sc config my-service-default start=auto
I also set up a row for the ServiceControl MSI table to direct the Windows installer to:
- Start my service on install (MSIDB_SERVICE_CONTROL_EVENT_START)
- Stop the old version of my service on upgrade (MSIDB_SERVICE_CONTROL_EVENT_STOP)
- Stop my service on uninstall (MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_STOP)
- Delete my service on uninstall (MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_DELETE)
This is basically the equivalent of running this command on install:
> sc start my-service-default
These commands on upgrade:
> sc stop my-service-default
... install the new executable and support files ...
> sc start my-service-default
And these commands on uninstall:
> sc stop my-service-default
> .\build\exe.win-amd64-3.12\my-service.exe --uninstall default
The one tricky part of the above is that in the ServiceInstall and ServiceControl tables, you have to reference the service executable by the component ID that cx_Freeze auto-generates for it — which is based on the index of the corresponding Executable object that you've configured in the setup command's executables option, as well as the string representation of the Executable object itself. I encapsulated the logic to derive this ID in my _make_component_id() function, which takes as arguments the list that will be used for the executables option, as well as the index in that list to the service's Executable object.
Now when building and installing my service's MSI file, we can see that my service is started automatically after install:
> uv run --group freeze cx_freeze_setup.py bdist_msi
...
> msiexec /i dist\my-service-1.0.0-win64.msi /qn
> sc query my-service-default
SERVICE_NAME: my-service-default
TYPE : 10 WIN32_OWN_PROCESS
STATE : 4 RUNNING
(STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
> sc qc my-service-default
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: my-service-default
TYPE : 10 WIN32_OWN_PROCESS
START_TYPE : 2 AUTO_START
ERROR_CONTROL : 1 NORMAL
BINARY_PATH_NAME : "C:\Program Files\My Service\my-service.exe"
LOAD_ORDER_GROUP :
TAG : 0
DISPLAY_NAME : My Service
DEPENDENCIES :
SERVICE_START_NAME : LocalSystem
Extra: Add Other Executables
That's all I needed for the service itself; but for my project, I also had some CLI (Command Line Interface) scripts I wanted to include in its installed Program Files directory.
For each script, I simply had to add another Executable object to my EXECUTABLES list — for example, I had a my-service-cli script defined in my pyproject.toml file:
# pyproject.toml
[project.scripts]
my-service-cli = "my_service.cli:main"
So I added a corresponding my-service-cli executable definition to my cx_Freeeze script:
# cx_freeze_setup.py
EXECUTABLES =
...
Executable(
script="src/my_service/cli.py",
base="console",
target_name="my-service-cli",
),
]
Which resulted in a my-service-cli.exe executable generated by cx_Freeze — and added to my project's Program Files directory when the MSI is installed:
C:\Program Files\My Service\my-service-cli.exe
Extra: Add Icons
To add icons to my executables — and to my installer MSI file — I created a custom ICO icon, saved it in my project's source directory as installer/my_icon.ico, and annotated my Executable and bdist_msi configuration with it:
# cx_freeze_setup.py
EXECUTABLES =
Executable(
script="src/my_service/windows_service_config.py",
base="Win32Service",
target_name=SERVICE_NAME,
icon="installer/my_icon.ico",
),
Executable(
script="src/my_service/cli.py",
base="console",
target_name="my-service-cli",
icon="installer/my_icon.ico",
),
]
...
setup(
...
options={
"bdist_msi": {
...
"install_icon": "installer/my_icon.ico",
...
},
},
)
Extra: Add Other Files
I also had some other miscellaneous files I wanted to include in my project's Program Files directory. That turned out to be as easy as using the include_files option of the build_exe command:
# cx_freeze_setup.py
setup(
...
options={
"build_exe": {
...
"include_files": [
("LICENSE", "LICENSE.txt"),
("installer/help.url", "help.url"),
("installer/log.txt", "log/README.txt"),
],
...
},
},
)
This enabled me to take these files from my project source directory:
LICENSE
installer/help.url
installer/log.txt
And install them into the C:\Program Files\My Service directory as these files:
LICENSE.txt
help.url
log\README.txt
The installer automatically creates any necessary subdirectories for these files (such as the log subdirectory).
Extra: Add Start Menu
Finally, I also wanted to add some custom commands and links to things for my project in its own Start Menu folder. That turned out to require adding several more custom MSI tables to the bdist_msi config.
First, I had to create a custom Start Menu folder for my project, using the Directory table:
# cx_freeze_setup.py
setup(
...
options={
"bdist_msi": {
...
"data": {
"Directory": [
(
"ProgramMenuFolder", # ID
"TARGETDIR", # DirectoryParent
".", # DefaultDir
),
(
f"{SERVICE_NAME}Folder", # ID
"ProgramMenuFolder", # DirectoryParent
DISPLAY_NAME, # DefaultDir
),
],
...
},
},
},
)
The first row in the Directory table sets up a reference to Window's Start Menu folder (using special ProgramMenuFolder and TARGETDIR keywords); and the second row creates a new folder named "My Service" in it.
Next, I used the Property table to define installation properties for the Windows' cmd and explorer commands, so that I could use them for several of the Start Menu items themselves:
# cx_freeze_setup.py
setup(
...
options={
"bdist_msi": {
...
"data": {
...
"Property": [
("cmd", "cmd"),
("explorer", "explorer"),
],
...
},
},
},
)
Finally, I used the Shortcut table to define each item I wanted to add to my custom Start Menu:
# cx_freeze_setup.py
setup(
...
options={
"bdist_msi": {
...
"data": {
...
"Shortcut": [
(
f"{SERVICE_NAME}VersionMenuItem", # ID
f"{SERVICE_NAME}Folder", # Directory
f"{DISPLAY_NAME} Version", # Name
"TARGETDIR", # Component
"[cmd]", # Target
"/k my-service-cli.exe --version", # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
"TARGETDIR", # WkDir
),
(
f"{SERVICE_NAME}HelpMenuItem", # ID
f"{SERVICE_NAME}Folder", # Directory
"Help", # Name
"TARGETDIR", # Component
"[TARGETDIR]help.url", # Target
None, # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
"TARGETDIR", # WkDir
),
(
(
f"{SERVICE_NAME}LogsMenuItem", # ID
f"{SERVICE_NAME}Folder", # Directory
"Logs", # Name
"TARGETDIR", # Component
"[explorer]", # Target
"log", # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
"TARGETDIR", # WkDir
),
],
...
},
},
},
)
In each of the above rows, the first column (like f"{SERVICE_NAME}VersionMenuItem") is an arbitrary ID for the row; the second column (f"{SERVICE_NAME}Folder") is a reference to the second row in the Directory table (directing the installer to create the shortcut in my service's custom Start Menu folder); and the third column (like f"{DISPLAY_NAME} Version") is the display name for the shortcut.
The fifth and sixth columns are the command and command arguments to use for the shortcut; and the last column is a reference to the working directory in which to run the command. So my first row defined a shortcut that is equivalent to opening up a command prompt and running the following commands:
> cd C:\Program Files\My Service
> my-service-cli.exe --version
My second row defined a shortcut that's equivalent to double-clicking on the help.url file that I installed into my project's Program Files directory with the include_files config from the earlier Add Other Files section. And my third row defined a shortcut that's equivalent to opening Windows' File Explorer to the C:\Program Files\My Service\log directory (created via the same include_files config).
Finished Product
With the above extras, my complete cx_Freeze setup script ended up looking like the following:
# cx_freeze_setup.py
"""cx_Freeze setup script."""
from re import sub
from cx_Freeze import Executable, setup
from my_service import DESCRIPTION, SERVICE_NAME
from my_service import __version__ as VERSION
SERVICE_DEFAULT=default
SERVICE_WIN32_OWN_PROCESS = 0x10
SERVICE_AUTO_START = 0x2
SERVICE_ERROR_NORMAL = 0x1
MSIDB_SERVICE_CONTROL_EVENT_START = 0x1
MSIDB_SERVICE_CONTROL_EVENT_STOP = 0x2
MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_STOP = 0x20
MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_DELETE = 0x80
EXECUTABLES = [
Executable(
script="src/my_service/windows_service_config.py",
base="Win32Service",
target_name=SERVICE_NAME,
icon="installer/my_icon.ico",
),
Executable(
script="src/my_service/cli.py",
base="console",
target_name="my-service-cli",
icon="installer/my_icon.ico",
),
]
def _make_component_id(executables, index):
executable = executables[index]
component = f"_cx_executable{index}_{executable}"
return sub(r"[^\w.]", "_", component)
setup(
name=SERVICE_NAME,
version=VERSION,
description=DESCRIPTION,
options={
"build_exe": {
"excludes": [
"test",
"tkinter",
"unittest",
],
"includes": [
"_cffi_backend",
"cx_Logging",
],
"include_files": [
("LICENSE", "LICENSE.txt"),
("installer/help.url", "help.url"),
("installer/log.txt", "log/README.txt"),
],
"include_msvcr": True,
"packages": [
"my_service",
],
},
"bdist_msi": {
"all_users": True,
"initial_target_dir": f"[ProgramFiles64Folder]{DISPLAY_NAME}",
"install_icon": "installer/my_icon.ico",
# IMPORTANT: generate a unique UUID for your service
"upgrade_code": "{26483DF9-540E-43D7-B543-795C62E3AF2D}",
"data": {
"Directory": [
(
"ProgramMenuFolder", # ID
"TARGETDIR", # DirectoryParent
".", # DefaultDir
),
(
f"{SERVICE_NAME}Folder", # ID
"ProgramMenuFolder", # DirectoryParent
DISPLAY_NAME, # DefaultDir
),
],
"Property": [
("cmd", "cmd"),
("explorer", "explorer"),
],
"ServiceInstall": [
(
f"{SERVICE_NAME}-{SERVICE_DEFAULT}Install", # ID
f"{SERVICE_NAME}-{SERVICE_DEFAULT}", # Name
DISPLAY_NAME, # DisplayName
SERVICE_WIN32_OWN_PROCESS, # ServiceType
SERVICE_AUTO_START, # StartType
SERVICE_ERROR_NORMAL, # ErrorControl
None, # LoadOrderGroup
None, # Dependencies
None, # StartName
None, # Password
None, # Arguments
_make_component_id(EXECUTABLES, 0), # Component
DESCRIPTION, # Description
),
],
"ServiceControl": [
(
f"{SERVICE_NAME}-{SERVICE_DEFAULT}Control", # ID
f"{SERVICE_NAME}-{SERVICE_DEFAULT}", # Name
(
MSIDB_SERVICE_CONTROL_EVENT_START
+ MSIDB_SERVICE_CONTROL_EVENT_STOP
+ MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_STOP
+ MSIDB_SERVICE_CONTROL_EVENT_UNINSTALL_DELETE
), # Event
None, # Arguments
0, # Wait
_make_component_id(EXECUTABLES, 0), # Component
),
],
"Shortcut": [
(
f"{SERVICE_NAME}VersionMenuItem", # ID
f"{SERVICE_NAME}Folder", # Directory
f"{DISPLAY_NAME} Version", # Name
"TARGETDIR", # Component
"[cmd]", # Target
"/k my-service-cli.exe --version", # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
"TARGETDIR", # WkDir
),
(
f"{SERVICE_NAME}HelpMenuItem", # ID
f"{SERVICE_NAME}Folder", # Directory
"Help", # Name
"TARGETDIR", # Component
"[TARGETDIR]help.url", # Target
None, # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
"TARGETDIR", # WkDir
),
(
(
f"{SERVICE_NAME}LogsMenuItem", # ID
f"{SERVICE_NAME}Folder", # Directory
"Logs", # Name
"TARGETDIR", # Component
"[explorer]", # Target
"log", # Arguments
None, # Description
None, # Hotkey
None, # Icon
None, # IconIndex
None, # ShowCmd
"TARGETDIR", # WkDir
),
],
},
},
},
executables=EXECUTABLES,
)