Develop Windows MSI on Linux Using Python/Docker/ Wine

Sudhakar Gumparthi
7 min readJun 16, 2022

--

This blog is written keeping the views of enthusiastic developer who wanted to experiment/work with distribution models of a python project or scripts.

Itried to put the required information and the design to achieve the final point, straight and simple. The concepts/tools like Docker, WINE and Python programming language are very wide. Without waiting further let us start drive along the way.

Docker

Docker is an open platform for developing, shipping, and running applications. Docker enables to separate your applications from infrastructure so you can deliver software quickly. The below design show brief information about the Docker architecture. We are using Docker scripts to achieve the goal.

Representative Docker Model

Assuming that we all have docker installed on the machine where we are using, for windows , a Docker Desktop would be the right place to start.

Wine

Wine Is Not an Emulator. Wine is a compatibility layer capable of running Windows applications on several POSIX-compliant operating systems. Wine translates Windows API calls into POSIX calls on-the-fly. It is Open source. References https://wiki.winehq.org/Ubuntu

Python

Python programming language is what we are using in the blog. We are using “cx_Freeze” library to build the .msi file from the python project.

‘cx_Freeze’ is open-source python library. It creates standalone executables from Python scripts. Python 3.6–3.10 on Windows requires the Visual C++ Redistributable, and it is free! cx_Freeze, when building the msi file, doesn’t automatically ship VC++ with the application.

Demo/Development

Till now we just scratched the tools that we are going to use, now lets develop a sample application and convert that into msi file.

WINE is windows on Linux, so we use here Docker Linux host and use Ubuntu. The docker script below builds the image.

FROM ubuntu:20.04

ENV DEBIAN_FRONTEND noninteractive

ARG WINE_VERSION=winehq-stable
ARG PYTHON_VERSION=3.8.10

# we need wine for this all to work, so we'll use the PPA
RUN set -x \
&& dpkg --add-architecture i386 \
&& apt-get update -qy \
&& apt-get install --no-install-recommends -qfy apt-transport-https software-properties-common wget gpg-agent rename \
&& wget -nv https://dl.winehq.org/wine-builds/winehq.key \
&& apt-key add winehq.key \
&& add-apt-repository 'https://dl.winehq.org/wine-builds/ubuntu/' \
&& apt-get update -qy \
&& apt-get install --no-install-recommends -qfy $WINE_VERSION winbind cabextract \
&& apt-get clean \
&& wget -nv https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks \
&& chmod +x winetricks \
&& mv winetricks /usr/local/bin

# wine settings
ENV WINEARCH win64
ENV WINEDEBUG fixme-all
ENV WINEPREFIX /wine

ENV PYPI_URL=https://pypi.python.org/
ENV PYPI_INDEX_URL=https://pypi.python.org/simple

RUN wine --version
RUN wine64 --version
RUN set -x \
&& winetricks win10


RUN set -x \
&& for msifile in `echo core dev exe lib path pip tcltk tools`; do \
wget -nv "https://www.python.org/ftp/python/$PYTHON_VERSION/amd64/${msifile}.msi"; \
wine64 msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/Python38; \
rm ${msifile}.msi; \
done \
&& cd /wine/drive_c/Python38 \
&& echo 'wine '\''C:\Python38\python.exe'\'' "$@"' > /usr/bin/python \
&& echo 'wine '\''C:\Python38\Scripts\easy_install.exe'\'' "$@"' > /usr/bin/easy_install \
&& echo 'wine '\''C:\Python38\Scripts\pip.exe'\'' "$@"' > /usr/bin/pip \
&& echo 'wine '\''C:\Python38\Scripts\pyinstaller.exe'\'' "$@"' > /usr/bin/pyinstaller \
&& echo 'wine '\''C:\Python38\Scripts\pyupdater.exe'\'' "$@"' > /usr/bin/pyupdater \
&& echo 'assoc .py=PythonScript' | wine cmd \
&& echo 'ftype PythonScript=c:\Python38\python.exe "%1" %*' | wine cmd \
&& while pgrep wineserver >/dev/null; do echo "Waiting for wineserver"; sleep 1; done \
&& chmod +x /usr/bin/python /usr/bin/easy_install /usr/bin/pip /usr/bin/pyinstaller /usr/bin/pyupdater \
&& rm -rf /tmp/.wine-*

ENV W_DRIVE_C=/wine/drive_c
ENV W_WINDIR_UNIX="$W_DRIVE_C/windows"
ENV W_SYSTEM64_DLLS="$W_WINDIR_UNIX/system32"
ENV W_TMP="$W_DRIVE_C/windows/temp/_$0"

#wheel
RUN python -m pip install --upgrade wheel

# upgrade pip
RUN wine /wine/drive_c/Python38/python.exe -m pip install --upgrade pip

RUN pip --version
RUN python --version

# install Microsoft Visual C++ Redistributable for Visual Studio 2017 dll files
RUN set -x \
&& rm -f "$W_TMP"/* \
&& wget -P "$W_TMP" https://download.visualstudio.microsoft.com/download/pr/11100230/15ccb3f02745c7b206ad10373cbca89b/VC_redist.x64.exe \
&& cabextract -q --directory="$W_TMP" "$W_TMP"/VC_redist.x64.exe \
&& cabextract -q --directory="$W_TMP" "$W_TMP/a10" \
&& cabextract -q --directory="$W_TMP" "$W_TMP/a11" \
&& cd "$W_TMP" \
&& rename 's/_/\-/g' *.dll \
&& cp "$W_TMP"/*.dll "$W_SYSTEM64_DLLS"/

Save the above content in a file, ex, Dockerfile.winpy, and run the below command

docker build -t pywin-demo -f Dockerfile.winpy .

We are waiting …. here 15 to 20 min

Required WINE on Ubuntu server is ready as a docker image by now, let us test

We are ready with image, lets move to write a simple python application

Write python application which will take simple command line argument and print.

Create a folder demoproj, and create 3 files Dockerfile, main.py and setup.py. Shown below respectively

FROM pywin-demo

WORKDIR /app
COPY . .
RUN chmod 777 -R .

RUN apt-get update -y
RUN python -m pip install --upgrade pip pywin32 cx_Freeze==6.8 importlib-metadata==4.8.1 setuptools==60.6.0

CMD python setup.py bdist --format=msi && mv dist/*Demo*.msi dist/DemoPywin.msi

Below is main program,

import argparse

if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Demo Application')
parser.add_argument("-n", "--name-val", help='This will be printed on console', default='default')
args, unknown = parser.parse_known_args()
if unknown:
print('Invalid command args')
if args:
print(f' Args passed is {args.name_val}')

Now we need setup file, this is key we talk about little more on this.

from cx_Freeze import setup, Executable

setup(name="DemoPywin",
version="0.0.1",
description="Demo Application",
executables=[Executable("main.py", shortcut_name="Demo",
shortcut_dir="DesktopFolder", )],
)

This file uses setup method from cx_Freeze library, it will have lot customizable features. Read carefully, we say to use main.py and create a desktop short cut with name “Demo” . This setup file is key to generate installable msi, and here you can pass all required python libraries and dependencies and lot more… leaving it to the programmer capabilities and understanding. We have now enough for the demo.

Refer the link for more https://pypi.org/project/cx-Freeze/

Lets build the msi, you need to do two steps, first build the docker image, then run. Go the project folder and run below commands

docker build -t demo .

Once the build complete, check if images are there

Then run the below command to the file in your C drive (whereever you need)

docker run -it -v C:/test12/demodel:/app/dist demo

Cann’t wait ??? It takes just a while to build, then we go the folder.

Yes, you have msi file, let us install it!

Wrap up and go to the folder where it is installed,

Let us wait, here and see what are all these files??

main.exe is the starting point for our application to run. Rest all lib and python.dll and others are app dependencies.

Run the program,

That’s all folks, you have msi to ship and install.

Take Away

1.Windows EXE/MSI files can be built on Linux with the help of WINE Server

2.Docker images can be used to build Windows environment on Linux containers

3.CX Freeze is used to build MSI/EXEs using Python application programming language

Additional Questions?

1.Are there any docker/wine/python images available on docker hub?

Yes, many.

2.Will you be able to run the generate EXE on all windows versions?

Subjective, but mostly on all windows versions, yes. You might need to install certain dependent windows DLLs if needed any. cx_Freeze will not ship any windows’s dependencies when the application is built. It will ship all dependent python libraries. You need to exercise the dependencies cautiously.

3.Python already have commands to generate MSI/EXE. Why CX Freeze?

This library puts all application dependencies together, so application just runs on any windows machine, no hassle of setting up python and site-packages and if any …

4.I have various python dependencies and my application is very large, do I need any additional things to generate EXE/MSI?

I think, no need, all required links to your main.py will be picked up, may need to explore the cx_Freeze options.

5.I wanted to have nice interactive Installation wizard can I customize?

This will provide decent wizard, if you want any addons need to take different route …

6.What if I need additional DLLs for my application?

All the windows related libraries/os dlls licensed versions will not be shipped with application.

7.Can this be deployed as Windows service?

Multiple ways, we will explore one such later.

--

--

Sudhakar Gumparthi
Sudhakar Gumparthi

Written by Sudhakar Gumparthi

Senior Technology Architect at Infosys Ltd.

No responses yet