One command takes a clean checkout to artifacts you can hand to someone.
build.bat
That is it. From PowerShell, or to pass options:
powershell -ExecutionPolicy Bypass -File build\build.ps1
PATH (tick "Add python.exe to PATH" in the
Python installer)build.bat -InstallInnoSetup will fetch it with wingetNothing else. The script creates its own virtual environment and installs everything into it, so your system Python is left alone.
| Step | |
|---|---|
| 1 | Checks Python is present and new enough, and reads the version |
| 2 | Creates build\.venv and installs the dependencies into it |
| 3 | Runs the test suite — and refuses to package if it fails |
| 4 | Regenerates the application icon |
| 5 | Builds the application with PyInstaller |
| 6 | Launches the built .exe to confirm it actually starts |
| 7 | Packages the portable zip |
| 8 | Compiles the installer with Inno Setup |
| 9 | Writes SHA-256 checksums and a build record |
Step 6 is worth calling out: a missing Qt plugin or DLL produces a bundle that builds perfectly and then dies the moment it is launched. The script starts the executable, waits ten seconds, and fails the build if it exited. A window may briefly appear; that is expected.
Everything deployable lands in dist\:
| File | |
|---|---|
SurfaceCast-0.1.0-setup.exe |
The installer — this is what you give people |
SurfaceCast-0.1.0-portable.zip |
No-install build; unzip and run SurfaceCast.exe |
SHA256SUMS.txt |
Checksums, in the format sha256sum -c reads |
BUILD-INFO.txt |
Version, commit, machine, Python and PySide6 versions |
Intermediate output stays under build\:
| Path | |
|---|---|
build\out\SurfaceCast\ |
The unpacked application |
build\work\ |
PyInstaller scratch space, safe to delete |
build\.venv\ |
The build virtual environment |
build\_generated\ |
Version files generated from constants.py |
All of it is ignored by git.
| Switch | |
|---|---|
-SkipInstaller |
Application and portable zip only |
-SkipPortable |
Do not build the zip |
-SkipTests |
Package without running the tests (not for a release) |
-SkipSmokeTest |
Do not launch the result — use on a machine with no desktop |
-Clean |
Delete and rebuild the build virtual environment |
-InstallInnoSetup |
Install Inno Setup with winget if it is missing |
-OutputDir <path> |
Put the artifacts somewhere other than dist\ |
build.bat -SkipInstaller
build.bat -Clean -InstallInnoSetup
build.bat -OutputDir C:\releases\surfacecast
The script exits non-zero if any step fails, so it drops straight into CI.
Edit APP_VERSION in surfacecast/core/constants.py. That is the only place
it is written down — the Windows version resource, the installer, the artifact
filenames and the packaging metadata are all derived from it at build time, and
a test fails if a second copy creeps back in.
python -m pip install -r requirements-dev.txt
python build\make_icon.py
python -m PyInstaller build\surfacecast.spec --noconfirm --clean --workpath build\work --distpath build\out
iscc build\installer.iss
Run PyInstaller before Inno Setup: the spec generates the version file that
installer.iss reads.
A one-file build unpacks itself into a temporary folder on every launch, which adds seconds to startup. That is the wrong trade for an application opened minutes before a show starts. The installer hides the folder from users anyway, and the portable zip is for people who want to see it.
There is no code signing certificate involved, so on a machine that has not seen the installer before, Windows SmartScreen shows "Windows protected your PC". Users get past it with More info > Run anyway. Some managed environments block unsigned installers outright; there, the portable zip can be copied into place instead.
To sign it later, add a signtool step after PyInstaller and before Inno Setup,
and set SignTool in build\installer.iss.
SurfaceCast.Scene ProgID and adds SurfaceCast
to the Open with list for .json — it does not take .json away from
other applications%APPDATA%\SurfaceCast, and leaves your scenes and shows alone{app}\docs with a Start Menu shortcut to it. The application prefers that
copy, so Help > Open the folder lands somewhere browsable rather than
inside the packed bundleQtMultimedia,
QtMultimediaWidgets or QtNetwork — video playback needs all three.build\make_icon.py rather than checked
in as artwork; the build regenerates it every time.%APPDATA%\SurfaceCast\settings.json. Deleting it resets the application to
defaults, including the projector screen choice.
The log is at %APPDATA%\SurfaceCast\logs\surfacecast.log, rotating at one
megabyte with three older files kept. This is the first thing to ask for when a
packaged build misbehaves: it is windowed, so stderr is discarded, and the log
is the only record that survives.
The autosave copy lives beside it as %APPDATA%\SurfaceCast\recovery.json. It
exists only while there is unsaved work, is removed on a clean exit, and is an
ordinary show file — safe to delete, and openable with File > Open show.
When something does not work on the real hardware, this prints everything needed to work out why - Python and Qt versions, every screen with its resolution, position and scaling, the audio outputs, and the settings file:
python tools\diagnose.py
Pass a video file and it also opens it with the same player the application uses, which separates "SurfaceCast cannot find the file" from "Qt cannot decode this file":
python tools\diagnose.py "D:\media\loop.mp4"
"Python was not found on PATH." Install Python 3.10+ from python.org with "Add python.exe to PATH" ticked, then open a new terminal.
python.exe : File "<string>", line 1 at "Checking the environment".
A python -c snippet in build\build.ps1 contains a double quote. Windows
PowerShell rebuilds a native command line as a string and does not escape
quotes inside an argument, so python -c 'print("x")' arrives at Python as
print(x) and dies with a syntax error naming neither the step nor the cause.
Rewrite the snippet without any double quotes; tests\test_packaging.py fails
the suite if one creeps back in.
The build stops at the test step. That is the gate doing its job. Run
build\.venv\Scripts\python -m pytest to see the failure, fix it, or pass
-SkipTests if you know why it fails.
"The packaged application exited immediately." The bundle is missing
something. Check that none of QtMultimedia, QtMultimediaWidgets or
QtNetwork ended up in the excludes list in build\surfacecast.spec, then
run build\out\SurfaceCast\SurfaceCast.exe by hand to see the error.
"Inno Setup (ISCC.exe) was not found." Install it, re-run with
-InstallInnoSetup, or pass -SkipInstaller. The application folder and the
portable zip are already built by that point.
The smoke test opens a fullscreen projector window. A previous run saved
"open the projector at startup" into your settings. It is killed after ten
seconds, or use -SkipSmokeTest.
Video does not play in the packaged build but does from source. See the Qt
note above — it is almost always an over-aggressive excludes entry.
The projector window opens on the wrong screen. Screen indices change when displays are plugged in or out. Pick the screen again in Output > Projector screen and use Identify screens to confirm.
Separate from the application build, and needs nothing from it:
pip install -r requirements-site.txt
python tools/capture_screens.py
python tools/build_site.py
tools/capture_screens.py stands up the real control panel, loads
examples/demo-scene.json (which references no external media, so the pictures
come out the same anywhere), and grabs the widgets. The phone shots need
Playwright; it starts a remote server on a free port and drives Chromium at
phone size against it. Pass --skip-phone if Playwright is not installed.
tools/build_site.py writes site/. The documentation pages are generated
from the markdown in the repository - the site never holds a second copy of
any document. Brand colors are the block at the top of
website/assets/site.css.