SurfaceCast

Building SurfaceCast for Windows

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

Prerequisites

  • Windows 10 or later, 64-bit
  • Python 3.10 or newer, on PATH (tick "Add python.exe to PATH" in the Python installer)
  • Inno Setup 6 for the installer — optional, and build.bat -InstallInnoSetup will fetch it with winget

Nothing else. The script creates its own virtual environment and installs everything into it, so your system Python is left alone.

What the script does

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.

Output

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.

Options

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.

Changing the version

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.

Running the steps by hand

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.

Why a folder build, not a single file

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.

The build is unsigned

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.

What the installer does

  • Installs per-machine when run elevated, per-user otherwise
  • Start menu shortcut always; desktop shortcut optional
  • Optionally registers a private SurfaceCast.Scene ProgID and adds SurfaceCast to the Open with list for .json — it does not take .json away from other applications
  • Upgrades replace the previous install rather than stacking up entries in Apps & features
  • Uninstalling removes the application and the settings in %APPDATA%\SurfaceCast, and leaves your scenes and shows alone
  • Shows the GPL as a wizard page, and installs the documentation to {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 bundle

Notes on the packaged Qt

  • The spec excludes the Qt modules the application never touches (WebEngine, Quick/QML, 3D, Charts, SQL and so on). Do not prune QtMultimedia, QtMultimediaWidgets or QtNetwork — video playback needs all three.
  • Video decoding uses Qt's bundled FFmpeg backend, so there is nothing to install on the target machine.
  • The application icon is generated by build\make_icon.py rather than checked in as artwork; the build regenerates it every time.

Where settings live

%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.

Diagnostics

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"

Troubleshooting

"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.

The website

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.