feat(examples): add example playing sointu tracks from Python (#108)

* Added Python code example.
* Added pyinstaller build.
* Clarified debugging steps in README.md.
* Added linux implementation.
* Cosmetics.
* Updated README with correct steps.
This commit is contained in:
Alexander Kraus 2023-10-11 08:37:00 +02:00 committed by GitHub
parent f5eeabe5f3
commit 94589eb2eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 619 additions and 0 deletions

7
examples/code/Python/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.venv
build
dist
__pycache__
setup.py
*.egg-info
*.so

View File

@ -0,0 +1,18 @@
# Embed Sointu in Python
This is an example for embedding Sointu into Python code.
# Configure the track
Edit the `track` variable in `build.py` according to your needs.
# Build
* Install Python 3.11 and poetry.
* Download nasm and golang; place both of them in your system `PATH`.
* Enable cgo by downloading a gcc and placing it into your system `PATH`.
* Get the dependencies with `poetry install`.
* Run the player using `poetry run python -m sointu_python`.
* Pack everything into an executable using `poetry run pyinstaller sointu_python/sointu_python.spec`. The executable will be built in the `dist` subfolder.
# Rebuild after changes
* Rebuild the example track bindings with `poetry build`.
* Update the bindings module with `poetry install`.
* Proceed iteration.

View File

@ -0,0 +1,149 @@
from distutils.command.build_ext import build_ext
from distutils.errors import (
CCompilerError,
DistutilsExecError,
DistutilsPlatformError,
)
from distutils.core import Extension
from os.path import (
dirname,
join,
abspath,
exists,
basename,
splitext,
)
from os import mkdir
from subprocess import run
from platform import system
from sys import exit
track = "../../patches/physics_girl_st.yml"
class BuildFailed(Exception):
pass
class ExtBuilder(build_ext):
def run(self):
try:
build_ext.run(self)
except (DistutilsPlatformError, FileNotFoundError):
raise BuildFailed('File not found. Could not compile C extension.')
def build_extension(self, ext):
try:
build_ext.build_extension(self, ext)
except (CCompilerError, DistutilsExecError, DistutilsPlatformError, ValueError):
raise BuildFailed('Could not compile C extension.')
def build(setup_kwargs):
# Make sure the build directory exists and setup the
# relative paths correctly.
cwd = abspath(".")
print("Running from:", cwd)
current_source_dir = abspath(dirname(__file__))
project_source_dir = abspath(join(current_source_dir, "..", "..", ".."))
current_binary_dir = join(current_source_dir, 'build')
if not exists(current_binary_dir):
mkdir(current_binary_dir)
host_is_windows = system() == "Windows"
executable_suffix = ".exe" if host_is_windows else ""
object_suffix = ".obj" if host_is_windows else ".o"
# Build the sointu compiler first.
compiler_executable = join(current_binary_dir, "sointu-compile{}".format(executable_suffix))
result = run(
args=[
"go", "build",
"-o", compiler_executable,
"cmd/sointu-compile/main.go",
],
cwd=project_source_dir,
shell=True if host_is_windows else False,
)
if result.returncode != 0:
print("sointu-compile build process exited with:", result.returncode)
print(result.stdout)
exit(1)
track_file_name = abspath(join(current_source_dir, track))
(track_name_base, _) = splitext(basename(track_file_name))
print("Compiling track:", track_file_name)
# Compile the track.
sointu_compiler_arch = "amd64"
track_asm_file = join(current_binary_dir, '{}.asm'.format(track_name_base))
result = run(
args=[
compiler_executable,
"-o", track_asm_file,
"-arch={}".format(sointu_compiler_arch),
track_file_name,
],
)
if result.returncode != 0:
print("sointu-compile process exited with:", result.returncode)
print(result.stdout)
exit(1)
# Assemble the track.
nasm_abi = "Win64" if host_is_windows else "Elf64"
track_object_file = join(current_binary_dir, '{}{}'.format(track_name_base, object_suffix))
print("Assembling track asm source:", track_asm_file)
result = run(
args=[
'nasm',
'-o', track_object_file,
'-f', nasm_abi,
track_asm_file,
],
)
if result.returncode != 0:
print("nasm process exited with:", result.returncode)
print(result.stdout)
exit(1)
# Export the plugin.
print("Linking object file into Python extension module:", track_object_file)
setup_kwargs.update({
"ext_modules": [
Extension(
"sointu",
include_dirs=[
current_binary_dir,
current_source_dir,
],
sources=[
"sointu.c",
],
extra_compile_args=[
"-DTRACK_HEADER=\"{}.h\"".format(track_name_base),
] + ([
"-DWIN32",
] if host_is_windows else [
"-DUNIX",
"-fPIC",
]),
extra_objects=[
track_object_file,
],
extra_link_args=[
"dsound.lib",
"ws2_32.lib",
"ucrt.lib",
"user32.lib",
] if host_is_windows else [
"-z", "noexecstack",
"--no-pie",
"-lasound",
"-lpthread",
"-lpython3.11",
],
),
],
"cmdclass": {
"build_ext": ExtBuilder,
},
})

132
examples/code/Python/poetry.lock generated Normal file
View File

@ -0,0 +1,132 @@
# This file is automatically @generated by Poetry and should not be changed by hand.
[[package]]
name = "altgraph"
version = "0.17.4"
description = "Python graph (network) package"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"},
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
]
[[package]]
name = "macholib"
version = "1.16.3"
description = "Mach-O header analysis and editing"
category = "dev"
optional = false
python-versions = "*"
files = [
{file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"},
{file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"},
]
[package.dependencies]
altgraph = ">=0.17"
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "pefile"
version = "2023.2.7"
description = "Python PE parsing module"
category = "dev"
optional = false
python-versions = ">=3.6.0"
files = [
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
]
[[package]]
name = "pyinstaller"
version = "6.0.0"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
category = "dev"
optional = false
python-versions = "<3.13,>=3.8"
files = [
{file = "pyinstaller-6.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d84b06fb9002109bfc542e76860b81459a8585af0bbdabcfc5dcf272ef230de7"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa922d1d73881d0820a341d2c406a571cc94630bdcdc275427c844a12e6e376e"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:52e5b3a2371d7231de17515c7c78d8d4a39d70c8c095e71d55b3b83434a193a8"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4a75bde5cda259bb31f2294960d75b9d5c148001b2b0bd20a91f9c2116675a6c"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:5314f6f08d2bcbc031778618ba97d9098d106119c2e616b3b081171fe42f5415"},
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0ad7cc3776ca17d0bededcc352cba2b1c89eb4817bfabaf05972b9da8c424935"},
{file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cccdad6cfe7a5db7d7eb8df2e5678f8375268739d5933214e180da300aa54e37"},
{file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fb6af82989dac7c58bd25ed9ba3323bc443f8c1f03804f69c9f5e363bf4a021c"},
{file = "pyinstaller-6.0.0-py3-none-win32.whl", hash = "sha256:68769f5e6722474bb1038e35560444659db8b951388bfe0c669bb52a640cd0eb"},
{file = "pyinstaller-6.0.0-py3-none-win_amd64.whl", hash = "sha256:438a9e0d72a57d5bba4f112d256e39ea4033c76c65414c0693d8311faa14b090"},
{file = "pyinstaller-6.0.0-py3-none-win_arm64.whl", hash = "sha256:16a473065291dd7879bf596fa20e65bd9d1e8aafc2cef1bffa3e42e707e2e68e"},
{file = "pyinstaller-6.0.0.tar.gz", hash = "sha256:d702cff041f30e7a53500b630e07b081e5328d4655023319253d73935e75ade2"},
]
[package.dependencies]
altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
packaging = ">=20.0"
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2021.4"
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
[package.extras]
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2023.9"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyinstaller-hooks-contrib-2023.9.tar.gz", hash = "sha256:76084b5988e3957a9df169d2a935d65500136967e710ddebf57263f1a909cd80"},
{file = "pyinstaller_hooks_contrib-2023.9-py2.py3-none-any.whl", hash = "sha256:f34f4c6807210025c8073ebe665f422a3aa2ac5f4c7ebf4c2a26cc77bebf63b5"},
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.2"
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
]
[[package]]
name = "setuptools"
version = "68.2.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
{file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.11,<3.13"
content-hash = "797bde9c30c55b3ddb24b1d3eceedd093d8a89eb934e6fe8fe7191dc9247224d"

View File

@ -0,0 +1,27 @@
[tool.poetry]
name = "sointu-python"
version = "0.1.0"
description = "Play back Sointu tracks in Python."
authors = ["Alexander Kraus <nr4@z10.info>"]
license = "MIT"
readme = "README.md"
packages = [
{ include = "sointu_python" },
]
include = [
{ path = "sointu*.so", format="wheel" }
]
[tool.poetry.build]
script = "build.py"
generate-setup-file = true
[tool.poetry.dependencies]
python = ">=3.11,<3.13"
[tool.poetry.group.dev.dependencies]
pyinstaller = "^6.0.0"
[build-system]
requires = ["poetry-core>=1.0.0a3", "poetry>=0.12", "setuptools", "wheel"]
build-backend = "poetry.core.masonry.api"

View File

@ -0,0 +1,212 @@
#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include TRACK_HEADER
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
SUsample sound_buffer[SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT];
#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#define WIN32_EXTRA_LEAN
#include <Windows.h>
#include "mmsystem.h"
#include "mmreg.h"
#define CINTERFACE
#include <dsound.h>
static WAVEFORMATEX wave_format = {
#ifdef SU_SAMPLE_FLOAT
WAVE_FORMAT_IEEE_FLOAT,
#else
WAVE_FORMAT_PCM,
#endif
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
SU_SAMPLE_RATE * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
SU_SAMPLE_SIZE*8,
0
};
DSBUFFERDESC buffer_description = {
sizeof(DSBUFFERDESC),
DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_TRUEPLAYPOSITION,
SU_LENGTH_IN_SAMPLES * SU_SAMPLE_SIZE * SU_CHANNEL_COUNT,
0,
&wave_format,
0
};
static HWND hWnd;
static LPDIRECTSOUND direct_sound;
static LPDIRECTSOUNDBUFFER direct_sound_buffer;
static LPVOID p1;
static DWORD l1;
static DWORD last_play_cursor = 0;
#endif /* WIN32 */
#ifdef UNIX
#include <alsa/asoundlib.h>
#include <pthread.h>
#include <time.h>
static snd_pcm_t *pcm_handle;
static pthread_t render_thread;
static uint32_t render_thread_handle;
static pthread_t playback_thread;
static uint32_t playback_thread_handle;
snd_htimestamp_t start_ts;
static int _snd_pcm_writei(void *params) {
(void) params;
snd_pcm_writei(pcm_handle, sound_buffer, SU_LENGTH_IN_SAMPLES);
return 0;
}
#endif /* UNIX */
static PyObject *sointuError;
static PyObject *sointu_play_song(PyObject *self, PyObject *args) {
#ifdef WIN32
#ifdef SU_LOAD_GMDLS
su_load_gmdls();
#endif // SU_LOAD_GMDLS
hWnd = GetForegroundWindow();
if(hWnd == NULL) {
hWnd = GetDesktopWindow();
}
DirectSoundCreate(0, &direct_sound, 0);
IDirectSound_SetCooperativeLevel(direct_sound, hWnd, DSSCL_PRIORITY);
IDirectSound_CreateSoundBuffer(direct_sound, &buffer_description, &direct_sound_buffer, NULL);
IDirectSoundBuffer_Lock(direct_sound_buffer, 0, SU_LENGTH_IN_SAMPLES * SU_CHANNEL_COUNT * SU_SAMPLE_SIZE, &p1, &l1, NULL, NULL, 0);
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)su_render_song, p1, 0, 0);
IDirectSoundBuffer_Play(direct_sound_buffer, 0, 0, 0);
#endif /* WIN32 */
#ifdef UNIX
render_thread_handle = pthread_create(&render_thread, 0, (void * (*)(void *))su_render_song, sound_buffer);
// We can't start playing too early or the missing samples will be audible.
sleep(2.);
// Play the track.
snd_pcm_open(&pcm_handle, "default", SND_PCM_STREAM_PLAYBACK, 0);
snd_pcm_set_params(
pcm_handle,
#ifdef SU_SAMPLE_FLOAT
SND_PCM_FORMAT_FLOAT,
#else // SU_SAMPLE_FLOAT
SND_PCM_FORMAT_S16_LE,
#endif // SU_SAMPLE_FLOAT
SND_PCM_ACCESS_RW_INTERLEAVED,
SU_CHANNEL_COUNT,
SU_SAMPLE_RATE,
0,
SU_LENGTH_IN_SAMPLES
);
// Enable playback time querying.
snd_pcm_sw_params_t *swparams;
snd_pcm_sw_params_alloca(&swparams);
snd_pcm_sw_params_current(pcm_handle, swparams);
snd_pcm_sw_params_set_tstamp_mode(pcm_handle, swparams, SND_PCM_TSTAMP_ENABLE);
snd_pcm_sw_params_set_tstamp_type(pcm_handle, swparams, SND_PCM_TSTAMP_TYPE_GETTIMEOFDAY);
snd_pcm_sw_params(pcm_handle, swparams);
playback_thread_handle = pthread_create(&playback_thread, 0, (void *(*)(void *))_snd_pcm_writei, 0);
// Get the start time stamp.
snd_pcm_uframes_t avail;
snd_pcm_htimestamp(pcm_handle, &avail, &start_ts);
#endif /* UNIX */
return PyLong_FromLong(0);
}
static PyObject *sointu_playback_position(PyObject *self, PyObject *args) {
#ifdef WIN32
DWORD play_cursor = 0;
IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL);
return Py_BuildValue("i", play_cursor / SU_CHANNEL_COUNT / sizeof(SUsample));
#endif /* WIN32 */
#ifdef UNIX
snd_htimestamp_t ts;
snd_pcm_uframes_t avail;
snd_pcm_htimestamp(pcm_handle, &avail, &ts);
return Py_BuildValue("i", (int)((ts.tv_sec - start_ts.tv_sec + 1.e-9 * (ts.tv_nsec - start_ts.tv_nsec)) * SU_SAMPLE_RATE));
#endif /* UNIX */
}
static PyObject *sointu_playback_finished(PyObject *self, PyObject *args) {
bool result = false;
#ifdef WIN32
DWORD play_cursor = 0;
IDirectSoundBuffer_GetCurrentPosition(direct_sound_buffer, (DWORD*)&play_cursor, NULL);
result = play_cursor < last_play_cursor;
last_play_cursor = play_cursor;
#endif /* WIN32 */
#ifdef UNIX
snd_htimestamp_t ts;
snd_pcm_uframes_t avail;
snd_pcm_htimestamp(pcm_handle, &avail, &ts);
result = ts.tv_sec - start_ts.tv_sec < 0;
#endif /* UNIX */
return PyBool_FromLong(result);
}
static PyObject *sointu_sample_rate(PyObject *self, PyObject *args) {
return Py_BuildValue("i", SU_SAMPLE_RATE);
}
static PyObject *sointu_track_length(PyObject *self, PyObject *args) {
return Py_BuildValue("i", SU_LENGTH_IN_SAMPLES);
}
static PyMethodDef sointuMethods[] = {
{"play_song", sointu_play_song, METH_VARARGS, "Play sointu track."},
{"playback_position", sointu_playback_position, METH_VARARGS, "Get playback position of sointu track currently playing."},
{"playback_finished", sointu_playback_finished, METH_VARARGS, "Check if currently playing sointu track has finished playing."},
{"sample_rate", sointu_sample_rate, METH_VARARGS, "Return the sample rate of the track compiled into this module."},
{"track_length", sointu_track_length, METH_VARARGS, "Return the track length in samples."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
static struct PyModuleDef sointumodule = {
PyModuleDef_HEAD_INIT,
"sointu",
NULL,
-1,
sointuMethods
};
PyMODINIT_FUNC PyInit_sointu(void) {
PyObject *module = PyModule_Create(&sointumodule);
if(module == NULL) {
return NULL;
}
sointuError = PyErr_NewException("sointu.sointuError", NULL, NULL);
Py_XINCREF(sointuError);
if(PyModule_AddObject(module, "error", sointuError) < 0) {
Py_XDECREF(sointuError);
Py_CLEAR(sointuError);
Py_DECREF(module);
return NULL;
}
return module;
}

View File

@ -0,0 +1,16 @@
from sointu import (
play_song,
playback_position,
playback_finished,
sample_rate,
track_length,
)
from sys import exit
if __name__ == '__main__':
play_song()
while not playback_finished():
print("Playback time:", playback_position() / sample_rate())
exit(0)

View File

@ -0,0 +1,58 @@
# -*- mode: python ; coding: utf-8 -*-
from os.path import abspath, join
from zipfile import ZipFile
from platform import system
moduleName = 'sointu_python'
rootPath = abspath('.')
buildPath = join(rootPath, 'build')
distPath = join(rootPath, 'dist')
sourcePath = join(rootPath, moduleName)
block_cipher = None
a = Analysis(
[
join(sourcePath, '__main__.py'),
],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='{}'.format(moduleName),
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=None,
)
exeFileName = '{}{}'.format(moduleName, '.exe' if system() == 'Windows' else '')
zipFileName = '{}-{}.zip'.format(moduleName, 'windows' if system() == 'Windows' else 'linux')
ZipFile(join(distPath, zipFileName), mode='w').write(join(distPath, exeFileName), arcname=exeFileName)