diff --git a/installer/BnRinstall_script-win/.gitignore b/installer/BnRinstall_script-win/.gitignore new file mode 100644 index 0000000000..14c7235d69 --- /dev/null +++ b/installer/BnRinstall_script-win/.gitignore @@ -0,0 +1,19 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.log + +# Build and state +openshot-installer.log +openshot-installer-relay.log +openshot-installer-state.json +OpenShotBuild/ +*.zip + +# Editor / OS noise +.DS_Store +Thumbs.db +.vscode/ +.idea/ diff --git a/installer/BnRinstall_script-win/CHANGELOG.md b/installer/BnRinstall_script-win/CHANGELOG.md new file mode 100644 index 0000000000..87ca7a3a1d --- /dev/null +++ b/installer/BnRinstall_script-win/CHANGELOG.md @@ -0,0 +1,10 @@ +# CHANGELOG.md + +## 1.0.1 + +- trimmed the package to feel more like an OpenShot contribution +- kept the BnR identity, author credit, consultation line, and ChatGPT credit +- clarified that BnR complements OpenShot's installer instead of replacing it +- removed decorative image assets and the placeholder local jQuery file from the help page package +- added contribution-oriented notes aligned with OpenShot's GitHub workflow +- ensured the local docs set is internally consistent for command and help references diff --git a/installer/BnRinstall_script-win/CODE_OF_CONDUCT.md b/installer/BnRinstall_script-win/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..0e5361d203 --- /dev/null +++ b/installer/BnRinstall_script-win/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ +# CODE_OF_CONDUCT.md + +Be respectful, specific, and useful. + +This helper exists to reduce wasted time in the OpenShot Windows source-build workflow. Review and discussion should stay focused on reproducible behavior, logs, build steps, and practical fixes. + +Treat the OpenShot project and contributors with respect. diff --git a/installer/BnRinstall_script-win/CONTRIBUTING.md b/installer/BnRinstall_script-win/CONTRIBUTING.md new file mode 100644 index 0000000000..197175dc36 --- /dev/null +++ b/installer/BnRinstall_script-win/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# CONTRIBUTING.md + +This helper is intended to fit into the OpenShot contribution workflow. The upstream OpenShot rules are the primary guide: + +- OpenShot repository: https://github.com/OpenShot/openshot-qt +- OpenShot contributing guide: https://github.com/OpenShot/openshot-qt/blob/develop/CONTRIBUTING.md + +## Recommended flow + +1. Branch from `develop`. +2. Keep the changes focused and explain the problem clearly. +3. Open a pull request against `develop`. +4. Use draft / WIP status if the work still needs feedback or testing. + +## For this helper specifically + +The cleanest framing is that this is a Windows bootstrap/build helper for source builds. It supports the OpenShot workflow, but it does not replace the official packaging installer. + +When reporting bugs or requesting feedback, include the OS and attach relevant log files. diff --git a/installer/BnRinstall_script-win/INSTALL.md b/installer/BnRinstall_script-win/INSTALL.md new file mode 100644 index 0000000000..27c2021746 --- /dev/null +++ b/installer/BnRinstall_script-win/INSTALL.md @@ -0,0 +1,104 @@ +# INSTALL.md + +## Purpose + +This guide is for running **OpenShot BnR 1.0.1** on Windows and using it to bootstrap, build, verify, and launch OpenShot from source. + +This helper is meant to complement the OpenShot project's existing installer and packaging work. It is most useful when you are trying to get a Windows source-build environment working end to end on a stock or half-broken Windows machine and you want readable logs instead of guesswork. + +## How this differs from the official installer + +OpenShot already ships official installer and packaging tooling. That upstream tooling is for packaging and release work. + +**OpenShot BnR is different:** it focuses on restoring prerequisites, cloning the repos, building native dependencies, verifying the bindings, and generating launch helpers so a source build actually runs on stock Windows. + +## Upstream attribution + +OpenShot project links: + +- Website: https://www.openshot.org/ +- Source repository: https://github.com/OpenShot/openshot-qt +- Developer docs: https://www.openshot.org/static/files/user-guide/developers.html + +## Before you start + +You should have: + +- Windows 10 or later +- internet access for package resolution and repository pulls +- a Python 3 interpreter available through `py -3` or `python` +- permission to elevate when the script requests admin access + +## Fast path + +```bash +py -3 OpenShot_BnR_v1_0.py +``` + +The script will: +1. check Windows support and elevation +2. find or restore WinGet, Git, and MSYS2 +3. refresh MSYS2 and resolve dependencies against the live UCRT64 package list +4. clone or update the OpenShot repositories +5. build `libopenshot-audio` +6. build `libopenshot` +7. verify Python bindings in installed and source-build modes +8. generate launcher and distribution helper files + +## Information-only commands + +```bash +py -3 OpenShot_BnR_v1_0.py --usage +py -3 OpenShot_BnR_v1_0.py --help +py -3 OpenShot_BnR_v1_0.py man +py -3 OpenShot_BnR_v1_0.py --about +py -3 OpenShot_BnR_v1_0.py --version +py -3 OpenShot_BnR_v1_0.py --docs +py -3 OpenShot_BnR_v1_0.py --install +py -3 OpenShot_BnR_v1_0.py --manual-install +py -3 OpenShot_BnR_v1_0.py --log-guide +py -3 OpenShot_BnR_v1_0.py --troubleshoot +py -3 OpenShot_BnR_v1_0.py --license +py -3 OpenShot_BnR_v1_0.py --debug +``` + +## What gets created + +Common outputs include: + +- `C:\OpenShotBuild\Launch-OpenShot-Qt.cmd` +- `C:\OpenShotBuild\Launch-OpenShot-Qt.py` +- portable/frozen build helper scripts +- `openshot-installer.log` +- `openshot-installer-state.json` +- `openshot-installer-relay.log` + +## What success looks like + +A good result looks like this: + +- the prerequisite, dependency, repo, and build stages pass +- the bindings import successfully in installed or source-build mode +- launcher files are generated +- OpenShot reaches a real UI startup path +- the session log reads like a real launch, not a fake smoke test + +## Troubleshooting basics + +### `ModuleNotFoundError: No module named 'openshot'` +The runtime bootstrap path is wrong or incomplete. Check the generated launcher files and the runtime bootstrap section of the log. + +### `winget` missing +The script tries to restore or guide the WinGet/App Installer path, but locked-down systems may still need manual setup. See `MANUAL_INSTALL.md`. + +### MSYS2 packages fail or drift +The helper reads against live UCRT64 package data. If the machine is stale or partially broken, finish the MSYS2 update cycle manually first, then rerun. + +## Notes for upstream review + +This package intentionally keeps the OpenShot attribution visible because the point is to support the OpenShot workflow, not hijack it. + + +## OpenShot pull request fit + +This helper is packaged to fit OpenShot's usual GitHub contribution flow: make changes in a branch based on `develop`, open a PR to `develop`, and explain clearly that this helper complements the existing packaging installer by focusing on stock-Windows source builds. If the work is still under review, a draft / WIP PR is appropriate. diff --git a/installer/BnRinstall_script-win/LICENSE.txt b/installer/BnRinstall_script-win/LICENSE.txt new file mode 100644 index 0000000000..92dd92c62d --- /dev/null +++ b/installer/BnRinstall_script-win/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Trenton Tompkins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/installer/BnRinstall_script-win/LOG_GUIDE.md b/installer/BnRinstall_script-win/LOG_GUIDE.md new file mode 100644 index 0000000000..421b0ce735 --- /dev/null +++ b/installer/BnRinstall_script-win/LOG_GUIDE.md @@ -0,0 +1,130 @@ +# Log Guide and Step-by-Step Walkthrough + +This project lives or dies on whether the log tells the truth. The installer is strongest when the summary, artifacts, and runtime log all agree. + +## A successful OpenShot runtime log means this + +If you see a log like the one below, that means the build made it through the expensive parts and OpenShot actually launched. + +```text +Loaded modules from: C:\OpenShotBuild\openshot-qt\src +INFO app: OpenShot (version 3.5.1) +INFO app: libopenshot version: 0.7.0 +INFO ui_util: Initializing UI for MainWindow +INFO preview_thread: QThread Start Method Invoked +INFO app: OpenShot's session ended +``` + +That is not a fake pass. That is a real launch. + +## Walkthrough of the successful runtime sequence + +### `Loaded modules from: C:\OpenShotBuild\openshot-qt\src` +The launcher found the OpenShot Python application source tree and added it to the module search path. + +### `INFO app: Starting new session` +OpenShot booted into a fresh app session. You are past the launcher and into the application. + +### `INFO app: OpenShot (version 3.5.1)` +The Python app started and is reporting its own version. That means the top-level OpenShot app code is alive. + +### `INFO app: libopenshot version: 0.7.0` +The Python app was able to talk to the native `libopenshot` binding. This is one of the biggest milestones in the whole build. + +### `INFO app: python version: ... / qt5 version: ... / pyqt5 version: ...` +These lines tell you the active runtime stack. They are useful when you need to compare a working box with a failing one. + +### `INFO project_data: Setting profile to HD 720p 30 fps` +Default project data loaded correctly. + +### `INFO language: ...` +Language detection ran. Usually harmless and informational. + +### `INFO logger_libopenshot: Connecting to libopenshot with debug port: 5556` +The Python side connected to the native logging/debug interface. + +### `INFO ui_util: Initializing UI for MainWindow` +The actual desktop UI is being assembled. This is the line that tells you the app made it past imports and into real GUI startup. + +### `INFO thumbnail: Starting thumbnail server listening on ...` +Background support services are starting. Normal. + +### `WARNING updates: Cannot add existing listener ...` +This is noisy, but not fatal. It means a listener registration was attempted twice. + +### `INFO sentry: Sentry initialized ...` +Error reporting initialized successfully. Nice to have, not required for a launch. + +### `INFO generation_service: ComfyUI check failed at 127.0.0.1:8188 ...` +This is also not a launch blocker. It just means a local ComfyUI service was not running. + +### `INFO main_window: InitCacheSettings` and cache lines +The app is configuring preview/cache behavior. This is normal startup work. + +### `INFO preview_thread: QThread Start Method Invoked` +The preview/render thread came online. + +### `INFO main_window: Cleared temporary files: ...` +The app cleaned its temp folders. Good housekeeping, not a warning. + +### `INFO theme: Setting Fusion dark palette` +The UI theme loaded. + +### `INFO main_window: recover_backup` +The app checked for recovery state/backups. + +### `INFO video_widget: ...` +The preview widget initialized and knows its aspect ratio. + +### `INFO timeline: Adjusting max size of preview image ...` +Timeline preview resources are live. + +### Shutdown block + +```text +INFO main_window: ---------------- Shutting down ----------------- +INFO thumbnail: Shutting down thumbnail server +INFO logger_libopenshot: Shutting down libopenshot logger +INFO preview_thread: Stopping preview thread +INFO app: OpenShot's session ended +``` + +This is a clean shutdown sequence. It means the app did not just vanish; it exited and tore down its background pieces deliberately. + +## Installer stage summary walkthrough + +The installer summary is the other half of the truth. + +### `PASS Prerequisites` +Windows support, admin/elevation path, WinGet, Git, and MSYS2 are ready. + +### `PASS Dependencies` +The script updated MSYS2 metadata, read the live UCRT64 package list, and resolved/install the package capabilities it needed. + +### `PASS Repositories` +The OpenShot source repositories are present. + +### `PASS Build libopenshot-audio` +The audio layer built and installed. + +### `PASS Build libopenshot` +The native video/binding layer built and the source-build import probe worked. + +### `PASS Prepare openshot-qt` +The runtime bootstrap files were created and the launcher smoke test succeeded. + +### `PASS Verification` +The build artifacts and launch route looked good enough to treat the run as ready. + +## Fastest way to use the log when something breaks + +1. Read the stage summary first. +2. Read the artifacts block next. +3. Open `openshot-installer.log`. +4. Search for the first red/FAIL line, not the last dramatic line. +5. Compare against the successful runtime flow above. + +The first meaningful deviation is usually where the real problem starts. + + +See also: RELEASE_GUIDE.md for the public-release checklist and final QA pass. diff --git a/installer/BnRinstall_script-win/MANUAL_INSTALL.md b/installer/BnRinstall_script-win/MANUAL_INSTALL.md new file mode 100644 index 0000000000..8545654925 --- /dev/null +++ b/installer/BnRinstall_script-win/MANUAL_INSTALL.md @@ -0,0 +1,102 @@ +# Manual Install and Package Guide + +OpenShot BnR can bootstrap a lot automatically, but sometimes you want the manual route: locked-down machine, permissions trouble, Store-disabled images, or a release process where you want every dependency explained before you install it. + +## What this file covers + +This guide gives you: + +- the external prerequisites the script depends on +- the typical MSYS2 UCRT64 packages the script resolves +- what each package does in plain English +- official project URLs +- official license URLs where practical +- attribution for each vendor or upstream project +- direct install or fetch examples using `winget`, `git`, and `curl` + +## External prerequisites + +| Component | What it does | Project URL | License URL | Attribution | Example command | +|---|---|---|---|---|---| +| WinGet / App Installer | Windows package manager used to bootstrap or recover missing tools on supported Windows installs. | https://learn.microsoft.com/en-us/windows/package-manager/winget/ | https://github.com/microsoft/winget-cli/blob/master/LICENSE | Microsoft / Microsoft Learn | `winget --info` | +| Git for Windows | Native Git client on Windows so the script can clone and update repositories. | https://git-scm.com/install/windows | https://github.com/git-for-windows/git/blob/main/COPYING | Git for Windows project | `winget install --id Git.Git -e` | +| MSYS2 | Windows-native build environment with Bash, pacman, GCC, Make, and the UCRT64 package repo. | https://www.msys2.org/docs/installer/ | https://github.com/msys2/MSYS2-packages/blob/master/LICENSE | MSYS2 project | `winget install --id MSYS2.MSYS2 -e` | +| Python for Windows | Runs the top-level script and supports helper tooling outside MSYS2 when needed. | https://www.python.org/downloads/windows/ | https://docs.python.org/3/license.html | Python Software Foundation | `winget install --id Python.Python.3.14 -e` | +| OpenShot docs | Upstream architecture, build notes, and user/developer reference. | https://www.openshot.org/static/files/user-guide/developers.html | https://github.com/OpenShot/openshot-qt/blob/develop/COPYING | OpenShot project | open in browser | + +## Upstream repositories + +| Repository | Role | Repository URL | License URL | Attribution | Clone command | +|---|---|---|---|---|---| +| OpenShot BnR | This project: bootstrap, build, run, and distro-prep wrapper. | https://github.com/tibberous/BnRinstall_script-win | https://opensource.org/license/mit | Trenton Tompkins • community helper around OpenShot | `git clone https://github.com/tibberous/BnRinstall_script-win` | +| libopenshot-audio | C++ audio layer used by OpenShot. | https://github.com/OpenShot/libopenshot-audio | https://github.com/OpenShot/libopenshot-audio/blob/develop/COPYING | OpenShot project | `git clone https://github.com/OpenShot/libopenshot-audio` | +| libopenshot | C++ video/timeline/effects core and Python bindings. | https://github.com/OpenShot/libopenshot | https://github.com/OpenShot/libopenshot/blob/develop/COPYING | OpenShot project | `git clone https://github.com/OpenShot/libopenshot` | +| openshot-qt | Python + PyQt desktop application. | https://github.com/OpenShot/openshot-qt | https://github.com/OpenShot/openshot-qt/blob/develop/COPYING | OpenShot project | `git clone https://github.com/OpenShot/openshot-qt` | + +## Direct fetch examples + +### Get this project without Git + +If you do not want to use Git, open the project page and use **Code → Download ZIP**: + +- https://github.com/tibberous/BnRinstall_script-win + +### Clone the full source tree + +```bash +git clone https://github.com/tibberous/BnRinstall_script-win +git clone https://github.com/OpenShot/libopenshot-audio +git clone https://github.com/OpenShot/libopenshot +git clone https://github.com/OpenShot/openshot-qt +``` + +### Install external prerequisites with WinGet + +```bash +winget install --id Git.Git -e +winget install --id MSYS2.MSYS2 -e +winget install --id Python.Python.3.14 -e +``` + +## Typical MSYS2 UCRT64 packages the script resolves + +These are the common capabilities the script scores against the live UCRT64 repo. Exact package choices can drift over time, but these are the usual targets. + +| Capability | Typical package | What it does | Package URL | Project / package license | Attribution | +|---|---|---|---|---|---| +| GCC / G++ | `mingw-w64-ucrt-x86_64-gcc` | Compiles the native C and C++ parts of the OpenShot stack. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-gcc | https://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html | MSYS2 package index / GCC project | +| GNU Make | `mingw-w64-ucrt-x86_64-make` | Runs Makefile-based builds. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-make | https://www.gnu.org/licenses/gpl-3.0.html | MSYS2 package index / GNU Make | +| CMake | `mingw-w64-ucrt-x86_64-cmake` | Generates and drives the build configuration. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-cmake | https://cmake.org/licensing/ | MSYS2 package index / Kitware | +| Ninja | `mingw-w64-ucrt-x86_64-ninja` | Optional fast build runner. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-ninja | https://github.com/ninja-build/ninja/blob/master/COPYING | MSYS2 package index / Ninja project | +| FFmpeg | `mingw-w64-ucrt-x86_64-ffmpeg` | Media libraries and tools for decode, encode, and processing. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-ffmpeg | https://ffmpeg.org/legal.html | MSYS2 package index / FFmpeg project | +| SWIG | `mingw-w64-ucrt-x86_64-swig` | Generates glue code for Python bindings. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-swig | https://github.com/swig/swig/blob/master/LICENSE | MSYS2 package index / SWIG project | +| Doxygen | `mingw-w64-ucrt-x86_64-doxygen` | Optional API docs generator. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-doxygen | https://github.com/doxygen/doxygen/blob/master/LICENSE | MSYS2 package index / Doxygen project | +| ZeroMQ | `mingw-w64-ucrt-x86_64-zeromq` | Messaging layer used by parts of the OpenShot stack. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-zeromq | https://github.com/zeromq/libzmq/blob/master/LICENSE | MSYS2 package index / ZeroMQ project | +| Python | `mingw-w64-ucrt-x86_64-python` | Python runtime inside UCRT64. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-python?repo=ucrt64 | https://docs.python.org/3/license.html | MSYS2 package index / Python Software Foundation | +| pip | `mingw-w64-ucrt-x86_64-python-pip` | Python package installer for helper venvs and optional tools. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-python-pip | https://github.com/pypa/pip/blob/main/LICENSE.txt | MSYS2 package index / PyPA | +| PyQt5 | `mingw-w64-ucrt-x86_64-python-pyqt5` | Qt5 bindings for Python; this is the UI layer that OpenShot-Qt needs. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-python-pyqt5 | https://www.riverbankcomputing.com/commercial/license-faq | MSYS2 package index / Riverbank | +| pyzmq | `mingw-w64-ucrt-x86_64-python-pyzmq` | Python bindings for ZeroMQ. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-python-pyzmq | https://github.com/zeromq/pyzmq/blob/main/LICENSE.md | MSYS2 package index / pyzmq project | +| cx_Freeze | `mingw-w64-ucrt-x86_64-python-cx-freeze` | Optional packaging helper for standalone builds. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-python-cx-freeze | https://github.com/marcelotduarte/cx_Freeze/blob/main/LICENSE.md | MSYS2 package index / cx_Freeze project | +| Rust | `mingw-w64-ucrt-x86_64-rust` | Optional toolchain some packages may want. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-rust | https://rust-lang.org/policies/licenses/ | MSYS2 package index / Rust project | +| Catch | `mingw-w64-ucrt-x86_64-catch` | Optional C++ unit test framework. | https://packages.msys2.org/packages/mingw-w64-ucrt-x86_64-catch | https://github.com/catchorg/Catch2/blob/devel/LICENSE.txt | MSYS2 package index / Catch2 project | + +## Manual MSYS2 package install example + +```bash +pacman -Sy --noconfirm +pacman -Syu --noconfirm --needed +pacman -Su --noconfirm --needed +pacman -S --needed --noconfirm mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-make mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-ffmpeg mingw-w64-ucrt-x86_64-swig mingw-w64-ucrt-x86_64-doxygen mingw-w64-ucrt-x86_64-zeromq mingw-w64-ucrt-x86_64-python mingw-w64-ucrt-x86_64-python-pip mingw-w64-ucrt-x86_64-python-pyqt5 mingw-w64-ucrt-x86_64-python-pyzmq mingw-w64-ucrt-x86_64-python-cx-freeze mingw-w64-ucrt-x86_64-rust mingw-w64-ucrt-x86_64-catch +``` + +## When manual install is the better move + +- `winget` is missing or blocked by policy +- Store / App Installer is disabled +- the machine is locked down and you need to inspect each dependency +- you want deterministic prep before the main script runs +- you are building a release machine and want clean notes for every package + +## Attribution summary + +These package and project links point at the official vendor or upstream project pages used by this project. Check those pages for the latest releases, hashes, installer notes, or upstream licensing changes. diff --git a/installer/BnRinstall_script-win/OpenShot_BnR_v1_0.py b/installer/BnRinstall_script-win/OpenShot_BnR_v1_0.py new file mode 100644 index 0000000000..ea9f9a7085 --- /dev/null +++ b/installer/BnRinstall_script-win/OpenShot_BnR_v1_0.py @@ -0,0 +1,2375 @@ +#!/usr/bin/env python3 +"""Author: Trenton Tompkins © 2026 +Coded with ❤️ with ChatGPT + +OpenShot BnR 1.0.1 +OpenShot Windows Bootstrap, Build, and Run Helper + +Community contribution context: +- OpenShot website: https://www.openshot.org/ +- OpenShot source repository: https://github.com/OpenShot/openshot-qt +- OpenShot developer docs: https://www.openshot.org/static/files/user-guide/developers.html + +This helper is an independent BnR wrapper intended to support the OpenShot workflow on +Windows. It is aimed at a different problem than the OpenShot project's own installer +and packaging scripts. + +OpenShot upstream installer/build tooling is for packaging and shipping finished builds. +OpenShot BnR is for getting OpenShot working on a stock or half-broken Windows machine: +restoring prerequisites, cloning the repos, building native dependencies, verifying +bindings honestly, and leaving behind launch helpers and readable logs. + +Included docs: +README.md, README.txt, INSTALL.md, MANUAL_INSTALL.md, LOG_GUIDE.md, +TROUBLESHOOTING.md, RELEASE_GUIDE.md, LICENSE.txt, help.html, CHANGELOG.md, +CONTRIBUTING.md, SECURITY.md, and CODE_OF_CONDUCT.md + +BnR contribution repo: +https://github.com/tibberous/BnRinstall_script-win + +For a free consultation about Windows/OpenShot build automation: +(724) 431-5207 • trenttompkins@gmail.com • https://www.trentontompkins.com +""" + +import ctypes +import difflib +import hashlib +import json +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import time +import textwrap +import traceback +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Tuple + +SCRIPT_PATH = Path(__file__).resolve() +SCRIPT_DIR = SCRIPT_PATH.parent + +PRODUCT_NAME = "OpenShot BnR" +VERSION = "1.0.1" +APP_NAME = f"{PRODUCT_NAME} {VERSION}" +APP_FULL_NAME = f"{APP_NAME} - OpenShot Windows Bootstrap, Build, and Run Helper" +BUILD_ROOT = Path(r"C:\OpenShotBuild") +MSYS_ROOT_DEFAULT = Path(r"C:\msys64") +LOG_PATH = SCRIPT_DIR / "openshot-installer.log" +STATE_PATH = SCRIPT_DIR / "openshot-installer-state.json" +RELAY_PATH = SCRIPT_DIR / "openshot-installer-relay.log" + +WEBSITE_URL = "https://www.trentontompkins.com" +REPO_URL = "https://github.com/tibberous/BnRinstall_script-win" +OPENSHOT_PROJECT_URL = "https://www.openshot.org/" +OPENSHOT_REPO_URL = "https://github.com/OpenShot/openshot-qt" +OPENSHOT_DOCS_URL = "https://www.openshot.org/static/files/user-guide/developers.html" +README_MD_PATH = SCRIPT_DIR / "README.md" +README_TXT_PATH = SCRIPT_DIR / "README.txt" +INSTALL_MD_PATH = SCRIPT_DIR / "INSTALL.md" +LICENSE_TXT_PATH = SCRIPT_DIR / "LICENSE.txt" +HELP_HTML_PATH = SCRIPT_DIR / "help.html" +MANUAL_INSTALL_MD_PATH = SCRIPT_DIR / "MANUAL_INSTALL.md" +LOG_GUIDE_MD_PATH = SCRIPT_DIR / "LOG_GUIDE.md" +TROUBLESHOOTING_MD_PATH = SCRIPT_DIR / "TROUBLESHOOTING.md" +RELEASE_GUIDE_MD_PATH = SCRIPT_DIR / "RELEASE_GUIDE.md" +CHANGELOG_MD_PATH = SCRIPT_DIR / "CHANGELOG.md" +CONTRIBUTING_MD_PATH = SCRIPT_DIR / "CONTRIBUTING.md" +SECURITY_MD_PATH = SCRIPT_DIR / "SECURITY.md" +CODE_OF_CONDUCT_MD_PATH = SCRIPT_DIR / "CODE_OF_CONDUCT.md" + + +def windows_to_msys_path(path: Path | str) -> str: + p = Path(path) + s = str(p).replace("\\", "/") + if len(s) >= 2 and s[1] == ':': + drive = s[0].lower() + rest = s[2:] + if not rest.startswith('/'): + rest = '/' + rest + return f"/{drive}{rest}" + return s + + +def run_msys_script(msys_root: Path, script_name: str, script_content: str, env_name: str = "ucrt64", cwd: Optional[Path] = None, check: bool = False) -> int: + scripts_dir = BUILD_ROOT / "_installer_scripts" + scripts_dir.mkdir(parents=True, exist_ok=True) + script_path = scripts_dir / script_name + script_path.write_text(script_content.strip() + "\n", encoding="utf-8") + try: + script_path.chmod(0o755) + except Exception: + pass + script_msys = windows_to_msys_path(script_path) + cmd = msys_shell(msys_root, env_name) + [f"bash '{script_msys}'"] + return run_stream(cmd, cwd=cwd, check=check) + +CHILD_ARG = "--elevated-child" +NO_PROMPT_ARG = "--no-prompt" + +USAGE_ALIASES = {"--usage", "-usage", "/usage", "usage", "-u", "/u", "/U"} +HELP_ALIASES = {"--help", "-help", "/help", "help", "-h", "/h", "/?", "?"} +MAN_ALIASES = {"man", "--man", "-man", "/man", "manual", "--manual", "-manual", "/manual"} +ABOUT_ALIASES = {"--about", "-about", "/about", "about"} +VERSION_ALIASES = {"--version", "-version", "/version", "version", "--ver", "-ver", "/ver", "ver", "-v", "/v"} +DOCS_ALIASES = {"--docs", "-docs", "/docs", "docs"} +README_ALIASES = {"--readme", "-readme", "/readme", "readme"} +INSTALL_ALIASES = {"--install", "-install", "/install", "install"} +MANUAL_INSTALL_ALIASES = {"--manual-install", "-manual-install", "/manual-install", "manual-install", "manualinstall"} +LOG_GUIDE_ALIASES = {"--log-guide", "-log-guide", "/log-guide", "log-guide", "--logs", "-logs", "/logs", "logs", "logguide"} +TROUBLESHOOTING_ALIASES = {"--troubleshoot", "-troubleshoot", "/troubleshoot", "troubleshoot", "--troubleshooting", "-troubleshooting", "/troubleshooting", "troubleshooting"} +RELEASE_GUIDE_ALIASES = {"--release-guide", "-release-guide", "/release-guide", "release-guide", "releaseguide"} +CHANGELOG_ALIASES = {"--changelog", "-changelog", "/changelog", "changelog"} +CONTRIBUTING_ALIASES = {"--contributing", "-contributing", "/contributing", "contributing"} +SECURITY_ALIASES = {"--security", "-security", "/security", "security"} +CODE_OF_CONDUCT_ALIASES = {"--code-of-conduct", "-code-of-conduct", "/code-of-conduct", "code-of-conduct", "codeofconduct"} +LICENSE_ALIASES = {"--license", "-license", "/license", "license"} +DEBUG_ALIASES = {"--debug", "-debug", "/debug", "debug"} +INFO_ALIASES = ( + USAGE_ALIASES | HELP_ALIASES | MAN_ALIASES | ABOUT_ALIASES | VERSION_ALIASES | + DOCS_ALIASES | README_ALIASES | INSTALL_ALIASES | MANUAL_INSTALL_ALIASES | + LOG_GUIDE_ALIASES | TROUBLESHOOTING_ALIASES | RELEASE_GUIDE_ALIASES | + CHANGELOG_ALIASES | CONTRIBUTING_ALIASES | SECURITY_ALIASES | + CODE_OF_CONDUCT_ALIASES | LICENSE_ALIASES | DEBUG_ALIASES +) + +MIN_WINDOWS_BUILD = 17763 +SUPPORTED_HOST_PYTHON_MIN = (3, 9) + +REPOS = { + "libopenshot-audio": "https://github.com/OpenShot/libopenshot-audio", + "libopenshot": "https://github.com/OpenShot/libopenshot", + "openshot-qt": "https://github.com/OpenShot/openshot-qt", +} + +CAPABILITIES: Dict[str, Dict[str, object]] = { + "gcc": { + "commands": ["gcc"], + "aliases": ["gcc", "gnu compiler collection", "c compiler", "toolchain"], + "prefer": ["gcc"], + "required": True, + }, + "g++": { + "commands": ["g++"], + "aliases": ["g++", "c++ compiler", "cpp compiler", "toolchain"], + "prefer": ["gcc", "g++"], + "required": True, + }, + "make": { + "commands": ["mingw32-make", "make"], + "aliases": ["mingw32 make", "make", "build make", "toolchain"], + "prefer": ["make"], + "required": True, + }, + "cmake": { + "commands": ["cmake"], + "aliases": ["cmake", "build system"], + "prefer": ["cmake"], + "required": True, + }, + "ninja": { + "commands": ["ninja"], + "aliases": ["ninja", "build tool"], + "prefer": ["ninja"], + "required": False, + }, + "ffmpeg": { + "commands": ["ffmpeg"], + "aliases": ["ffmpeg", "video codec", "avcodec", "avformat", "swscale"], + "prefer": ["ffmpeg"], + "required": True, + }, + "swig": { + "commands": ["swig"], + "aliases": ["swig", "wrapper generator"], + "prefer": ["swig"], + "required": True, + }, + "doxygen": { + "commands": ["doxygen"], + "aliases": ["doxygen", "documentation generator"], + "prefer": ["doxygen"], + "required": False, + }, + "zeromq": { + "commands": [], + "aliases": ["zeromq", "zmq", "libzmq"], + "prefer": ["zeromq", "zmq"], + "required": True, + }, + "python": { + "commands": ["python3", "python"], + "aliases": ["python", "python3"], + "prefer": ["python"], + "required": True, + }, + "pip": { + "commands": ["pip3", "pip"], + "aliases": ["pip", "python pip"], + "prefer": ["python pip", "pip"], + "required": True, + }, + "pyqt5": { + "commands": [], + "aliases": ["python pyqt5", "pyqt5", "qt5 bindings for python", "python qt5"], + "prefer": ["python pyqt5", "pyqt5"], + "required": True, + }, + "pyzmq": { + "commands": [], + "aliases": ["python pyzmq", "pyzmq", "python zmq", "zeromq python"], + "prefer": ["python pyzmq", "pyzmq"], + "required": True, + }, + "cx_freeze": { + "commands": [], + "aliases": ["python cx freeze", "cxfreeze", "cx freeze"], + "prefer": ["cx freeze", "cxfreeze"], + "required": False, + }, + "rust": { + "commands": ["rustc", "cargo"], + "aliases": ["rust", "rustc", "cargo", "rust toolchain"], + "prefer": ["rust", "rustc", "cargo"], + "required": False, + }, + "catch": { + "commands": [], + "aliases": ["catch", "catch2", "unittest", "unit test cpp"], + "prefer": ["catch", "unittest"], + "required": False, + }, +} + +NOISY_TOKENS = { + "docs", "doc", "debug", "dbg", "testdata", "examples", "demo", "static", + "bootstrap", "gui", "gtk", "qt6", "clang", "arm", "aarch64", "i686", + "mingw32", "mingw64", "headers", "python2", "python38", "python39" +} + +_ansi_enabled = False +_log_lock = threading.Lock() + + +def enable_ansi_colors() -> None: + global _ansi_enabled + if os.name != "nt": + _ansi_enabled = True + return + try: + kernel32 = ctypes.windll.kernel32 + handle = kernel32.GetStdHandle(-11) + mode = ctypes.c_uint() + if kernel32.GetConsoleMode(handle, ctypes.byref(mode)): + kernel32.SetConsoleMode(handle, mode.value | 0x0004) + _ansi_enabled = True + except Exception: + _ansi_enabled = False + + +def color(text: str, code: str) -> str: + if not _ansi_enabled: + return text + return f"\033[{code}m{text}\033[0m" + + +def green(text: str) -> str: + return color(text, "92") + + +def red(text: str) -> str: + return color(text, "91") + + +def yellow(text: str) -> str: + return color(text, "93") + + +def cyan(text: str) -> str: + return color(text, "96") + + +def timestamp() -> str: + return time.strftime("%Y-%m-%d %H:%M:%S") + + + +def script_md5(path: Path) -> str: + h = hashlib.md5() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + +def script_lmd(path: Path) -> str: + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(path.stat().st_mtime)) + +def append_text(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8", errors="ignore") as f: + f.write(text) + + +def log(level: str, msg: str) -> None: + line = f"{timestamp()} {level} {msg}" + with _log_lock: + print(line, flush=True) + append_text(LOG_PATH, line + "\n") + append_text(RELAY_PATH, line + "\n") + + +def info(msg: str) -> None: log("[INFO]", msg) +def trace(msg: str) -> None: log("[TRACE]", msg) +def ok(msg: str) -> None: log("[OK]", msg) +def warn(msg: str) -> None: log("[WARN]", msg) +def fail(msg: str) -> None: log("[FAIL]", msg) + + +def read_state() -> dict: + if not STATE_PATH.exists(): + return {} + try: + return json.loads(STATE_PATH.read_text(encoding="utf-8")) + except Exception: + return {} + + +def write_state(state: dict) -> None: + tmp = STATE_PATH.with_suffix(".tmp") + tmp.write_text(json.dumps(state, indent=2), encoding="utf-8") + tmp.replace(STATE_PATH) + + +def update_state(**kwargs) -> None: + state = read_state() + state.update(kwargs) + write_state(state) + + +def record_stage(stage: str, status: str, detail: str = "") -> None: + state = read_state() + stages = state.setdefault("stages", {}) + stages[stage] = { + "status": status, + "detail": detail, + "time": timestamp(), + } + write_state(state) + marker = {"PASS": green("PASS"), "FAIL": red("FAIL"), "WARN": yellow("WARN"), "RUNNING": cyan("RUN")}.get(status, status) + log("[STAGE]", f"{stage}: {marker} {detail}".rstrip()) + + +def record_artifact(name: str, path: Optional[str], exists: bool, detail: str = "", status: Optional[str] = None) -> None: + state = read_state() + artifacts = state.setdefault("artifacts", {}) + normalized_status = (status or ("OK" if exists else "MISS")).upper() + artifacts[name] = { + "path": path, + "exists": exists, + "detail": detail, + "status": normalized_status, + "time": timestamp(), + } + write_state(state) + + +def reset_state_files() -> None: + LOG_PATH.write_text("", encoding="utf-8") + RELAY_PATH.write_text("", encoding="utf-8") + write_state({ + "app_name": APP_NAME, + "started": timestamp(), + "completed": False, + "success": False, + "stages": {}, + "artifacts": {}, + }) + + +def run_capture( + cmd: Sequence[str] | str, + *, + cwd: Optional[Path] = None, + shell: bool = False, + env: Optional[Dict[str, str]] = None, +) -> subprocess.CompletedProcess: + trace(f"RUN_CAPTURE cwd={cwd or Path.cwd()} shell={shell} cmd={cmd!r}") + cp = subprocess.run( + cmd, + cwd=str(cwd) if cwd else None, + shell=shell, + text=True, + capture_output=True, + env=env, + ) + if cp.stdout: + for line in cp.stdout.splitlines(): + trace(f"STDOUT {line}") + if cp.stderr: + for line in cp.stderr.splitlines(): + trace(f"STDERR {line}") + trace(f"EXIT code={cp.returncode}") + return cp + + +def run_stream( + cmd: Sequence[str] | str, + *, + cwd: Optional[Path] = None, + shell: bool = False, + env: Optional[Dict[str, str]] = None, + check: bool = False, +) -> int: + trace(f"RUN cwd={cwd or Path.cwd()} shell={shell} cmd={cmd!r}") + process = subprocess.Popen( + cmd, + cwd=str(cwd) if cwd else None, + shell=shell, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env, + ) + assert process.stdout is not None + for raw_line in process.stdout: + line = raw_line.rstrip("\r\n") + if line: + trace(f"OUT {line}") + process.wait() + trace(f"EXIT code={process.returncode}") + if check and process.returncode != 0: + raise RuntimeError(f"Command failed ({process.returncode}): {cmd!r}") + return process.returncode + + +def which_any(names: Sequence[str]) -> Optional[str]: + for name in names: + path = shutil.which(name) + if path: + return path + return None + + +def host_python_ok() -> bool: + ver = sys.version_info[:3] + info(f"Host Python version={ver[0]}.{ver[1]}.{ver[2]}") + return ver >= SUPPORTED_HOST_PYTHON_MIN + + +def is_windows() -> bool: + return os.name == "nt" + + +def windows_build_number() -> Optional[int]: + if not is_windows(): + return None + ver = platform.version() + m = re.search(r"\.(\d+)$", ver) + return int(m.group(1)) if m else None + + +def is_admin() -> bool: + try: + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + except Exception: + return False + + +def powershell_path() -> str: + sysroot = Path(os.environ.get("SystemRoot", r"C:\Windows")) + return str(sysroot / "System32" / "WindowsPowerShell" / "v1.0" / "powershell.exe") + + +def common_paths() -> Dict[str, List[Path]]: + return { + "git": [ + Path(r"C:\Program Files\Git\cmd\git.exe"), + Path(r"C:\Program Files (x86)\Git\cmd\git.exe"), + ], + "msys2_shell": [ + MSYS_ROOT_DEFAULT / "msys2_shell.cmd", + ], + "pacman": [ + MSYS_ROOT_DEFAULT / "usr" / "bin" / "pacman.exe", + ], + "winget": [ + Path.home() / "AppData" / "Local" / "Microsoft" / "WindowsApps" / "winget.exe", + Path(r"C:\Users\Default\AppData\Local\Microsoft\WindowsApps\winget.exe"), + ], + } + + +def find_existing_tool(key: str, path_names: Sequence[str]) -> Optional[Path]: + p = which_any(path_names) + if p: + ok(f"{key} found in PATH: {p}") + return Path(p) + for candidate in common_paths().get(key, []): + if candidate.exists(): + ok(f"{key} found in common location: {candidate}") + return candidate + return None + + +def wait_for_elevated_child() -> int: + last_pos = 0 + start = time.time() + last_growth = time.time() + printed_banner = False + + while True: + if RELAY_PATH.exists(): + size = RELAY_PATH.stat().st_size + if size > last_pos: + with RELAY_PATH.open("r", encoding="utf-8", errors="ignore") as f: + f.seek(last_pos) + chunk = f.read() + if chunk: + print(chunk, end="", flush=True) + last_pos = size + last_growth = time.time() + printed_banner = True + + state = read_state() + if state.get("completed"): + if RELAY_PATH.exists(): + size = RELAY_PATH.stat().st_size + if size > last_pos: + with RELAY_PATH.open("r", encoding="utf-8", errors="ignore") as f: + f.seek(last_pos) + chunk = f.read() + if chunk: + print(chunk, end="", flush=True) + return int(state.get("exit_code", 1)) + + if time.time() - start > 20 and not printed_banner and not RELAY_PATH.exists(): + print(red("[FAIL]"), "Elevation likely failed or was cancelled before the child started logging.", flush=True) + return 1 + + if printed_banner and time.time() - last_growth > 600: + print(yellow("[WARN]"), "No new installer output for 10 minutes. Still waiting...", flush=True) + last_growth = time.time() + + time.sleep(0.20) + + +def elevate_and_wait() -> int: + if is_admin(): + ok("Running elevated") + return 0 + + warn("Not elevated. Relaunching with admin rights...") + args = [str(SCRIPT_PATH), CHILD_ARG, NO_PROMPT_ARG] + quoted_args = subprocess.list2cmdline(args) + rc = ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, quoted_args, str(SCRIPT_DIR), 1) + if rc <= 32: + print(red("[FAIL]"), f"Elevation request failed or was cancelled (ShellExecuteW={rc})", flush=True) + update_state(completed=True, success=False, exit_code=1, failed=True, error=f"Elevation failed ({rc})") + return 1 + + print(cyan("[INFO]"), "Admin child launched. Streaming installer output...\n", flush=True) + return wait_for_elevated_child() + + +def check_windows_support() -> None: + if not is_windows(): + raise RuntimeError("This installer only supports Windows.") + build = windows_build_number() + if build is None: + warn("Could not determine Windows build number") + return + info(f"Windows build={build}") + if build < MIN_WINDOWS_BUILD: + raise RuntimeError(f"Windows build {build} is too old. Need 17763 or later for WinGet support.") + + +def ensure_winget() -> Path: + p = find_existing_tool("winget", ["winget"]) + if p: + ok(f"winget available: {p}") + record_artifact("winget", str(p), True, "available") + return p + + warn("winget not found. Attempting registration with App Installer family name...") + ps = ( + 'try { ' + 'Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -ErrorAction Stop; ' + 'exit 0 ' + '} catch { ' + 'Write-Host $_.Exception.Message; exit 1 ' + '}' + ) + run_stream([powershell_path(), "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps]) + + p = find_existing_tool("winget", ["winget"]) + if p: + ok(f"winget became available after registration: {p}") + record_artifact("winget", str(p), True, "restored by RegisterByFamilyName") + return p + + warn("RegisterByFamilyName did not restore winget. Trying official aka.ms/getwinget bootstrap...") + ps2 = ( + 'try { ' + 'Add-AppxPackage https://learn.microsoft.com/en-us/windows/package-manager/winget/ -ErrorAction Stop; ' + 'exit 0 ' + '} catch { ' + 'Write-Host $_.Exception.Message; exit 1 ' + '}' + ) + run_stream([powershell_path(), "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", ps2]) + + p = find_existing_tool("winget", ["winget"]) + if p: + ok(f"winget installed from aka.ms/getwinget: {p}") + record_artifact("winget", str(p), True, "installed from aka.ms/getwinget") + return p + + record_artifact("winget", None, False, "winget unavailable after recovery attempts") + raise RuntimeError("winget is still unavailable after registration and aka.ms/getwinget bootstrap.") + + +def winget_has_package(package_id: str) -> bool: + cp = run_capture(["winget", "list", "--id", package_id, "-e", "--accept-source-agreements"]) + text = (cp.stdout + cp.stderr).lower() + return cp.returncode == 0 and package_id.lower() in text + + +def winget_install(package_id: str, *, name: Optional[str] = None) -> None: + label = name or package_id + if winget_has_package(package_id): + ok(f"{label} already present according to winget") + return + info(f"Installing {label} via winget ({package_id})...") + rc = run_stream([ + "winget", "install", "-e", "--id", package_id, + "--accept-package-agreements", "--accept-source-agreements", + "--disable-interactivity", + ]) + if rc != 0: + raise RuntimeError(f"winget failed to install {package_id}") + + +def ensure_git() -> Path: + p = find_existing_tool("git", ["git"]) + if p: + record_artifact("git", str(p), True, "available") + return p + ensure_winget() + winget_install("Git.Git", name="Git") + p = find_existing_tool("git", ["git"]) + if not p: + record_artifact("git", None, False, "git.exe not found after install") + raise RuntimeError("Git installation completed but git.exe was not found.") + record_artifact("git", str(p), True, "installed") + return p + + +def ensure_msys2() -> Path: + root = Path(os.environ.get("MSYS2_ROOT", str(MSYS_ROOT_DEFAULT))) + shell_cmd = root / "msys2_shell.cmd" + pacman = root / "usr" / "bin" / "pacman.exe" + if shell_cmd.exists() and pacman.exists(): + ok(f"MSYS2 present at {root}") + record_artifact("msys2_shell", str(shell_cmd), True, "available") + record_artifact("pacman", str(pacman), True, "available") + return root + + ensure_winget() + winget_install("MSYS2.MSYS2", name="MSYS2") + + if shell_cmd.exists() and pacman.exists(): + ok(f"MSYS2 installed at {root}") + record_artifact("msys2_shell", str(shell_cmd), True, "installed") + record_artifact("pacman", str(pacman), True, "installed") + return root + + record_artifact("msys2_shell", str(shell_cmd), False, "missing after MSYS2 install") + record_artifact("pacman", str(pacman), False, "missing after MSYS2 install") + raise RuntimeError("MSYS2 install completed but msys2_shell.cmd / pacman.exe were not found.") + + +def msys_shell(msys_root: Path, env_name: str = "ucrt64") -> List[str]: + return [str(msys_root / "msys2_shell.cmd"), "-defterm", "-no-start", f"-{env_name}", "-here", "-c"] + + +def msys_capture(msys_root: Path, command: str, env_name: str = "ucrt64", cwd: Optional[Path] = None) -> subprocess.CompletedProcess: + cmd = msys_shell(msys_root, env_name) + [command] + return run_capture(cmd, cwd=cwd) + + +def msys_stream(msys_root: Path, command: str, env_name: str = "ucrt64", cwd: Optional[Path] = None, check: bool = False) -> int: + cmd = msys_shell(msys_root, env_name) + [command] + return run_stream(cmd, cwd=cwd, check=check) + +def shell_quote(value: str) -> str: + return "\'" + value.replace("\'", "\'\"\'\"\'") + "\'" + + +def msys_resolve_python(msys_root: Path, preferred_python: Optional[Path] = None, env_name: str = "ucrt64") -> str: + candidates: List[str] = [] + if preferred_python and preferred_python.exists(): + candidates.append(windows_to_msys_path(preferred_python)) + candidates.extend(["python3", "python"]) + + checked: List[str] = [] + for candidate in candidates: + checked.append(candidate) + quoted = shell_quote(candidate) + if "/" in candidate or "\\" in candidate: + cp = msys_capture(msys_root, f"[ -x {quoted} ] && printf '%s\n' {quoted}", env_name=env_name) + resolved = (cp.stdout or "").strip().splitlines() + if cp.returncode == 0 and resolved: + return quoted + else: + cp = msys_capture(msys_root, f"command -v {candidate}", env_name=env_name) + resolved = (cp.stdout or "").strip().splitlines() + if cp.returncode == 0 and resolved: + return shell_quote(resolved[0]) + + joined = ", ".join(checked) + raise RuntimeError(f"Could not resolve an MSYS2 Python interpreter. Checked: {joined}") + + +def msys_env_exports(bindings_dir: Path, build_src_dir: Path) -> str: + exports: List[str] = [] + if bindings_dir.exists(): + exports.append(f"export PYTHONPATH={shell_quote(windows_to_msys_path(bindings_dir))}:$PYTHONPATH") + if build_src_dir.exists(): + exports.append(f"export PATH={shell_quote(windows_to_msys_path(build_src_dir))}:$PATH") + return " && ".join(exports) + + +def windows_cmd_quote(value: Path | str) -> str: + return '"' + str(value).replace('"', '""') + '"' + + +def resolve_windows_python(preferred_python: Optional[Path], msys_root: Path) -> Optional[Path]: + candidates: List[Path] = [] + if preferred_python: + candidates.append(preferred_python) + candidates.extend([ + msys_root / "ucrt64" / "bin" / "python.exe", + msys_root / "usr" / "bin" / "python.exe", + ]) + for candidate in candidates: + if candidate.exists(): + return candidate + return None + + +def create_runtime_bootstrap_script(root: Path, msys_root: Path, qt_repo: Path) -> Path: + bootstrap = root / "Launch-OpenShot-Qt.py" + qt_src_dir = qt_repo / "src" + lib_repo = root / "libopenshot" + bindings_dir = lib_repo / "build" / "bindings" / "python" + build_src_dir = lib_repo / "build" / "src" + ucrt_bin_dir = msys_root / "ucrt64" / "bin" + freeze_script = qt_repo / "freeze.py" + + bootstrap_text = f'''#!/usr/bin/env python3 +import importlib.util +import os +import runpy +import sys +import traceback +from pathlib import Path + +QT_REPO = Path(r"{qt_repo}") +QT_SRC_DIR = Path(r"{qt_src_dir}") +BINDINGS_DIR = Path(r"{bindings_dir}") +BUILD_SRC_DIR = Path(r"{build_src_dir}") +UCRT_BIN_DIR = Path(r"{ucrt_bin_dir}") +LAUNCH_PY = QT_SRC_DIR / "launch.py" +FREEZE_PY = Path(r"{freeze_script}") +SMOKE_FLAG = "--installer-smoke-import" +FROZEN_FLAG = "--installer-build-frozen" + +def prepend_env(name, values): + existing = [item for item in os.environ.get(name, "").split(os.pathsep) if item] + merged = [] + for value in values: + text = str(value) + if text and text not in merged: + merged.append(text) + for item in existing: + if item and item not in merged: + merged.append(item) + os.environ[name] = os.pathsep.join(merged) + +def configure_runtime(): + os.chdir(str(QT_REPO)) + bootstrap_paths = [QT_SRC_DIR, BINDINGS_DIR] + dll_paths = [BUILD_SRC_DIR, UCRT_BIN_DIR] + for entry in bootstrap_paths: + text = str(entry) + if entry.exists() and text not in sys.path: + sys.path.insert(0, text) + prepend_env("PYTHONPATH", bootstrap_paths) + prepend_env("OPENSHOT_BOOTSTRAP_PATHS", bootstrap_paths) + prepend_env("PATH", dll_paths) + os.environ.setdefault("OPENSHOT_INSTALLER_RUNTIME_MODE", "source-build") + add_dir = getattr(os, "add_dll_directory", None) + if add_dir: + for entry in dll_paths: + if entry.exists(): + try: + add_dir(str(entry)) + except OSError: + pass + +def inject_bootstrap_args(extra_args): + args = [] + existing = list(extra_args) + for entry in (BINDINGS_DIR, QT_SRC_DIR): + args.extend(["--path", str(entry)]) + args.extend(existing) + return args + +def smoke_import() -> int: + configure_runtime() + import openshot + from classes import settings, project_data, updates, sentry # noqa: F401 + from classes.app import OpenShotApp # noqa: F401 + print("SMOKE_IMPORT_OK") + print(f"openshot={{getattr(openshot, '__file__', '')}}") + print(f"settings={{getattr(settings, '__file__', '')}}") + print(f"project_data={{getattr(project_data, '__file__', '')}}") + print(f"updates={{getattr(updates, '__file__', '')}}") + return 0 + +def run_frozen_build(extra_args): + configure_runtime() + if not FREEZE_PY.exists(): + print(f"freeze.py not found at {{FREEZE_PY}}", file=sys.stderr) + return 1 + sys.argv = [str(FREEZE_PY)] + list(extra_args or ["build"]) + runpy.run_path(str(FREEZE_PY), run_name="__main__") + return 0 + +def run_launch(extra_args): + configure_runtime() + spec = importlib.util.spec_from_file_location("openshot_runtime_launch", str(LAUNCH_PY)) + if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load launch.py from {{LAUNCH_PY}}") + module = importlib.util.module_from_spec(spec) + sys.modules["openshot_runtime_launch"] = module + sys.argv = [str(LAUNCH_PY)] + inject_bootstrap_args(extra_args) + spec.loader.exec_module(module) + try: + result = module.main() + if isinstance(result, int): + return result + return 0 + except SystemExit as exc: + code = exc.code + if code is None: + return 0 + if isinstance(code, int): + return code + print(code) + return 1 + +def main(): + args = list(sys.argv[1:]) + try: + if args and args[0] == SMOKE_FLAG: + return smoke_import() + if args and args[0] == FROZEN_FLAG: + return run_frozen_build(args[1:]) + return run_launch(args) + except SystemExit as exc: + code = exc.code + if code is None: + return 0 + if isinstance(code, int): + return code + print(code) + return 1 + except Exception: + traceback.print_exc() + return 1 + +if __name__ == "__main__": + raise SystemExit(main()) +''' + bootstrap.write_text(bootstrap_text, encoding="utf-8", newline="\n") + record_artifact("launcher_bootstrap_py", str(bootstrap), bootstrap.exists(), "Python runtime bootstrap launcher") + return bootstrap + + +def patch_openshot_qt_launch(qt_repo: Path) -> bool: + launch_py = qt_repo / "src" / "launch.py" + if not launch_py.exists(): + record_artifact("openshot_qt_launch_patch", str(launch_py), False, "launch.py missing; patch not applied", status="WARN") + return False + + original = launch_py.read_text(encoding="utf-8", errors="ignore") + patched = original + changed = False + + if "import traceback" not in patched: + if "import logging" in patched: + patched = patched.replace("import logging", "import logging\nimport traceback", 1) + changed = True + + if "OPENSHOT_BOOTSTRAP_PATHS" not in patched: + needle = " if args.py_path:\n for p in args.py_path:\n newpath = os.path.realpath(p)\n try:\n if os.path.exists(newpath):\n sys.path.insert(0, newpath)\n print(f\"Added {newpath} to PYTHONPATH\")\n else:\n print(f\"{newpath} does not exist\")\n except TypeError as ex:\n print(f\"Bad path {newpath}: {ex}\")\n continue\n" + replacement = needle + "\n bootstrap_env_paths = os.environ.get('OPENSHOT_BOOTSTRAP_PATHS', '')\n if bootstrap_env_paths:\n for p in bootstrap_env_paths.split(os.pathsep):\n newpath = os.path.realpath(p)\n if newpath and os.path.exists(newpath) and newpath not in sys.path:\n sys.path.insert(0, newpath)\n print(f\"Added {newpath} from OPENSHOT_BOOTSTRAP_PATHS\")\n" + if needle in patched: + patched = patched.replace(needle, replacement, 1) + changed = True + + if "app.show_errors()" in patched and "if app is not None:" not in patched: + needle = " try:\n app = OpenShotApp(argv)\n except Exception:\n app.show_errors()\n" + replacement = " try:\n app = OpenShotApp(argv)\n except Exception:\n traceback.print_exc()\n if app is not None:\n try:\n app.show_errors()\n except Exception:\n traceback.print_exc()\n raise SystemExit(1)\n" + if needle in patched: + patched = patched.replace(needle, replacement, 1) + changed = True + + if changed: + backup = launch_py.with_suffix(launch_py.suffix + ".installer.bak") + if not backup.exists(): + backup.write_text(original, encoding="utf-8", newline="\n") + launch_py.write_text(patched, encoding="utf-8", newline="\n") + record_artifact("openshot_qt_launch_patch", str(launch_py), True, "launch.py runtime bootstrap and error handling patch applied") + return True + + already_patched = ("OPENSHOT_BOOTSTRAP_PATHS" in original) and ("if app is not None:" in original) and ("import traceback" in original) + record_artifact("openshot_qt_launch_patch", str(launch_py), already_patched, "launch.py patch already present" if already_patched else "launch.py patch pattern not found", status="OK" if already_patched else "WARN") + return already_patched + + +def create_distribution_helper(root: Path, msys_root: Path, qt_repo: Path, preferred_python: Optional[Path], bootstrap_path: Path) -> Optional[Path]: + helper = root / "Build-OpenShot-Frozen.cmd" + freeze_py = qt_repo / "freeze.py" + python_exe = resolve_windows_python(preferred_python, msys_root) + if not freeze_py.exists(): + record_artifact("frozen_build_helper", str(helper), False, "freeze.py not found; frozen build helper not created", status="WARN") + return None + python_cmd = windows_cmd_quote(python_exe) if python_exe else "python" + helper_lines = [ + "@echo off", + "setlocal", + f"set OPENSHOT_BUILD_ROOT={root}", + f"set OPENSHOT_QT_REPO={qt_repo}", + f"set OPENSHOT_BOOTSTRAP_PATHS={qt_repo / 'src'};{root / 'libopenshot' / 'build' / 'bindings' / 'python'}", + f"set PATH={root / 'libopenshot' / 'build' / 'src'};{msys_root / 'ucrt64' / 'bin'};%PATH%", + f'cd /d "{qt_repo}"', + f"{python_cmd} {windows_cmd_quote(bootstrap_path)} --installer-build-frozen %*", + "endlocal", + "", + ] + helper.write_text("\r\n".join(str(line) for line in helper_lines), encoding="utf-8", newline="\r\n") + record_artifact("frozen_build_helper", str(helper), helper.exists(), "Windows frozen-build helper") + return helper + + +def create_portable_distribution_helper(root: Path, msys_root: Path, qt_repo: Path, preferred_python: Optional[Path]) -> Optional[Path]: + helper_py = root / "Build-OpenShot-Portable.py" + helper_cmd = root / "Build-OpenShot-Portable.cmd" + python_exe = resolve_windows_python(preferred_python, msys_root) + if python_exe is None or not python_exe.exists(): + record_artifact("portable_build_helper", str(helper_cmd), False, "Unable to resolve Windows Python for portable helper", status="WARN") + return None + + lib_repo = root / "libopenshot" + bindings_dir = lib_repo / "build" / "bindings" / "python" + build_src_dir = lib_repo / "build" / "src" + ucrt_bin_dir = msys_root / "ucrt64" / "bin" + + helper_py_text = f'''#!/usr/bin/env python3 +import os +import shutil +from pathlib import Path + +ROOT = Path(r"{root}") +QT_REPO = Path(r"{qt_repo}") +BINDINGS_DIR = Path(r"{bindings_dir}") +BUILD_SRC_DIR = Path(r"{build_src_dir}") +UCRT_BIN_DIR = Path(r"{ucrt_bin_dir}") +PYTHON_EXE = Path(r"{python_exe}") +DIST_ROOT = ROOT / "dist-portable" +APP_ROOT = DIST_ROOT / "OpenShot-Portable" +RUNTIME_ROOT = APP_ROOT / "runtime" +PYTHON_ROOT = RUNTIME_ROOT / "python" +APP_DATA = APP_ROOT / "app" +QT_TARGET = APP_DATA / "openshot-qt" +BINDINGS_TARGET = APP_DATA / "libopenshot-bindings" +LIB_TARGET = APP_DATA / "libopenshot-bin" +MSYS_TARGET = APP_DATA / "msys-ucrt64-bin" + +def copytree_filtered(src: Path, dst: Path, ignore_names=None): + ignore_names = set(ignore_names or []) + if dst.exists(): + shutil.rmtree(dst) + def _ignore(_path, names): + ignored = set() + for name in names: + if name in ignore_names: + ignored.add(name) + if name == "__pycache__": + ignored.add(name) + if name.endswith(".pyc") or name.endswith(".pyo"): + ignored.add(name) + return ignored + shutil.copytree(src, dst, ignore=_ignore) + +def copy_matching(src: Path, dst: Path, patterns): + dst.mkdir(parents=True, exist_ok=True) + for pattern in patterns: + for path in src.glob(pattern): + if path.is_file(): + shutil.copy2(path, dst / path.name) + +def write_portable_launcher(): + launcher_py = APP_ROOT / "OpenShot-Portable.py" + launcher_cmd = APP_ROOT / "OpenShot-Portable.cmd" + text = f"""#!/usr/bin/env python3 +import importlib.util +import os +import sys +import traceback +from pathlib import Path + +APP_ROOT = Path(__file__).resolve().parent +QT_REPO = APP_ROOT / "app" / "openshot-qt" +QT_SRC_DIR = QT_REPO / "src" +BINDINGS_DIR = APP_ROOT / "app" / "libopenshot-bindings" +BUILD_SRC_DIR = APP_ROOT / "app" / "libopenshot-bin" +UCRT_BIN_DIR = APP_ROOT / "app" / "msys-ucrt64-bin" +LAUNCH_PY = QT_SRC_DIR / "launch.py" + +def prepend_env(name, values): + existing = [item for item in os.environ.get(name, "").split(os.pathsep) if item] + merged = [] + for value in values: + text = str(value) + if text and text not in merged: + merged.append(text) + for item in existing: + if item and item not in merged: + merged.append(item) + os.environ[name] = os.pathsep.join(merged) + +def configure(): + os.chdir(str(QT_REPO)) + bootstrap_paths = [QT_SRC_DIR, BINDINGS_DIR] + dll_paths = [BUILD_SRC_DIR, UCRT_BIN_DIR] + for entry in bootstrap_paths: + text = str(entry) + if entry.exists() and text not in sys.path: + sys.path.insert(0, text) + prepend_env("PYTHONPATH", bootstrap_paths) + prepend_env("OPENSHOT_BOOTSTRAP_PATHS", bootstrap_paths) + prepend_env("PATH", dll_paths) + add_dir = getattr(os, "add_dll_directory", None) + if add_dir: + for entry in dll_paths: + if entry.exists(): + try: + add_dir(str(entry)) + except OSError: + pass + +def main(): + configure() + spec = importlib.util.spec_from_file_location("openshot_portable_launch", str(LAUNCH_PY)) + if spec is None or spec.loader is None: + raise RuntimeError(f"Unable to load launch.py from {{LAUNCH_PY}}") + module = importlib.util.module_from_spec(spec) + sys.modules["openshot_portable_launch"] = module + sys.argv = [str(LAUNCH_PY), "--path", str(BINDINGS_DIR), "--path", str(QT_SRC_DIR)] + list(sys.argv[1:]) + spec.loader.exec_module(module) + return module.main() + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except SystemExit: + raise + except Exception: + traceback.print_exc() + raise SystemExit(1) +""" + launcher_py.write_text(text, encoding="utf-8", newline="\n") + python_candidates = [ + PYTHON_ROOT / "python.exe", + PYTHON_ROOT / "bin" / "python.exe", + PYTHON_ROOT / "Scripts" / "python.exe", + ] + python_cmd = next((candidate for candidate in python_candidates if candidate.exists()), PYTHON_ROOT / "python.exe") + launcher_cmd.write_text("\r\n".join([ + "@echo off", + "setlocal", + f'cd /d "{{APP_ROOT}}"', + f'"{{python_cmd}}" "{{launcher_py.name}}" %*', + "endlocal", + "", + ]), encoding="utf-8", newline="\r\n") + +def main(): + DIST_ROOT.mkdir(parents=True, exist_ok=True) + if APP_ROOT.exists(): + shutil.rmtree(APP_ROOT) + APP_ROOT.mkdir(parents=True, exist_ok=True) + copytree_filtered(QT_REPO, QT_TARGET, ignore_names={{".git", "build", "dist", "dist-portable", ".pytest_cache"}}) + copytree_filtered(PYTHON_EXE.parent.parent if (PYTHON_EXE.parent.name.lower() in ("bin", "scripts")) else PYTHON_EXE.parent, PYTHON_ROOT, ignore_names={{"__pycache__"}}) + copytree_filtered(BINDINGS_DIR, BINDINGS_TARGET, ignore_names={{"__pycache__"}}) + copytree_filtered(BUILD_SRC_DIR, LIB_TARGET, ignore_names={{"__pycache__"}}) + copy_matching(UCRT_BIN_DIR, MSYS_TARGET, ["*.dll", "*.exe"]) + write_portable_launcher() + print(APP_ROOT) + +if __name__ == "__main__": + main() +''' + helper_py.write_text(helper_py_text, encoding="utf-8", newline="\n") + helper_cmd_lines = [ + "@echo off", + "setlocal", + f"{windows_cmd_quote(python_exe)} {windows_cmd_quote(helper_py)} %*", + "endlocal", + "", + ] + helper_cmd.write_text("\r\n".join(helper_cmd_lines), encoding="utf-8", newline="\r\n") + record_artifact("portable_build_helper_py", str(helper_py), helper_py.exists(), "Portable distribution builder script") + record_artifact("portable_build_helper", str(helper_cmd), helper_cmd.exists(), "Portable distribution builder launcher") + return helper_cmd + + +def query_python_runtime_info(msys_root: Path, preferred_python: Optional[Path] = None) -> Dict[str, object]: + probe = BUILD_ROOT / "query_python_runtime_info.py" + probe.write_text( + """import json +import site +import sys +import sysconfig + +data = { + 'executable': sys.executable, + 'version': sys.version, + 'sys_path': sys.path, + 'site_packages': site.getsitepackages() if hasattr(site, 'getsitepackages') else [], + 'user_site': site.getusersitepackages() if hasattr(site, 'getusersitepackages') else '', + 'sysconfig_paths': sysconfig.get_paths(), +} +print(json.dumps(data)) +""", + encoding="utf-8", + ) + python_cmd = msys_resolve_python(msys_root, preferred_python) + cp = msys_capture(msys_root, f"{python_cmd} {shell_quote(windows_to_msys_path(probe))}", env_name="ucrt64") + if cp.returncode != 0: + return { + "ok": False, + "detail": (cp.stdout + cp.stderr).strip(), + "site_packages": [], + "sys_path": [], + "user_site": "", + "sysconfig_paths": {}, + } + lines = [line.strip() for line in cp.stdout.splitlines() if line.strip()] + for line in reversed(lines): + try: + data = json.loads(line) + data["ok"] = True + data["detail"] = "runtime info loaded" + return data + except Exception: + continue + return { + "ok": False, + "detail": cp.stdout.strip() or cp.stderr.strip() or "Unable to parse python runtime info", + "site_packages": [], + "sys_path": [], + "user_site": "", + "sysconfig_paths": {}, + } + + +def msys_update(msys_root: Path) -> None: + info("Refreshing MSYS2 package database...") + msys_stream(msys_root, "pacman -Sy --noconfirm", env_name="ucrt64", check=True) + info("Upgrading MSYS2 packages (pass 1)...") + msys_stream(msys_root, "pacman -Syu --noconfirm --needed", env_name="ucrt64", check=True) + info("Upgrading MSYS2 packages (pass 2)...") + msys_stream(msys_root, "pacman -Su --noconfirm --needed", env_name="ucrt64", check=True) + + +def get_ucrt_repo_packages(msys_root: Path) -> List[str]: + cp = msys_capture(msys_root, "pacman -Sl ucrt64", env_name="ucrt64") + if cp.returncode != 0: + raise RuntimeError("Unable to query pacman package list for ucrt64") + packages = [] + for line in cp.stdout.splitlines(): + parts = line.strip().split() + if len(parts) >= 2 and parts[0] == "ucrt64": + packages.append(parts[1]) + if not packages: + raise RuntimeError("No package names were returned from pacman -Sl ucrt64") + ok(f"Loaded {len(packages)} live UCRT64 package names") + return packages + + +def normalize(text: str) -> str: + text = text.lower() + text = re.sub(r"^mingw-w64-(ucrt|clang|mingw32|mingw64|clangarm64)-x86_64-", "", text) + text = re.sub(r"^ucrt64/", "", text) + text = text.replace("python3", "python") + text = text.replace("cx_freeze", "cx freeze") + text = re.sub(r"[^a-z0-9]+", " ", text) + return " ".join(text.split()) + + +def token_set(text: str) -> List[str]: + return [t for t in normalize(text).split() if t] + + +def score_package(alias: str, package_name: str) -> float: + q = normalize(alias) + p = normalize(package_name) + if not q or not p: + return 0.0 + + q_tokens = set(token_set(q)) + p_tokens = set(token_set(p)) + + ratio = difflib.SequenceMatcher(None, q, p).ratio() + overlap = len(q_tokens & p_tokens) / max(1, len(q_tokens)) + containment = 1.0 if q in p else 0.0 + exact = 1.0 if q == p else 0.0 + + prefix_bonus = 0.0 + if p.startswith(q): + prefix_bonus += 0.15 + if q_tokens: + first = sorted(q_tokens)[0] + if first in p_tokens: + prefix_bonus += 0.05 + + penalty = 0.0 + penalty += 0.09 * len(NOISY_TOKENS & p_tokens) + + if "python" in q_tokens and "python" not in p_tokens: + penalty += 0.40 + if "qt5" in q_tokens and "qt5" not in p_tokens and "pyqt5" not in p_tokens: + penalty += 0.40 + if "zmq" in q_tokens and not ({"zmq", "zeromq"} & p_tokens): + penalty += 0.40 + if "cargo" in q_tokens and "cargo" not in p_tokens: + penalty += 0.40 + if "rust" in q_tokens and not ({"rust", "rustc", "cargo", "rustup"} & p_tokens): + penalty += 0.35 + + score = ratio * 0.45 + overlap * 0.35 + containment * 0.10 + exact * 0.20 + prefix_bonus - penalty + if package_name.startswith("mingw-w64-ucrt-x86_64-"): + score += 0.08 + if package_name.startswith("mingw-w64-clang") or package_name.startswith("mingw-w64-i686-"): + score -= 0.40 + return score + + +def rank_candidates(aliases: Sequence[str], package_names: Sequence[str], limit: int = 20) -> List[Tuple[str, float, str]]: + best: Dict[str, Tuple[float, str]] = {} + for alias in aliases: + for pkg in package_names: + s = score_package(alias, pkg) + prev = best.get(pkg) + if prev is None or s > prev[0]: + best[pkg] = (s, alias) + ranked = sorted(((pkg, score, alias) for pkg, (score, alias) in best.items()), key=lambda x: x[1], reverse=True) + return ranked[:limit] + + +def msys_command_exists(msys_root: Path, commands: Sequence[str]) -> bool: + for cmd in commands: + cp = msys_capture(msys_root, f"command -v {cmd}") + if cp.returncode == 0 and cp.stdout.strip(): + ok(f"MSYS capability present: {cmd} -> {cp.stdout.strip()}") + return True + return False + + +def pacman_installed(msys_root: Path, package_name: str) -> bool: + cp = msys_capture(msys_root, f"pacman -Q {package_name}") + return cp.returncode == 0 + + +def install_package(msys_root: Path, package_name: str) -> bool: + info(f"Installing package candidate: {package_name}") + rc = msys_stream(msys_root, f"pacman -S --needed --noconfirm --disable-download-timeout {package_name}") + return rc == 0 + + +def resolve_and_install_capability(msys_root: Path, capability: str, repo_packages: Sequence[str]) -> Optional[str]: + spec = CAPABILITIES[capability] + commands = list(spec.get("commands", [])) + aliases = list(spec.get("aliases", [])) + prefer = list(spec.get("prefer", [])) + required = bool(spec.get("required", False)) + + info(f"Resolving capability: {capability}") + if commands and msys_command_exists(msys_root, commands): + ok(f"Capability already satisfied: {capability}") + return None + + ranked = rank_candidates(aliases, repo_packages, limit=30) + if not ranked: + if required: + raise RuntimeError(f"No live UCRT64 package candidates found for capability '{capability}'") + warn(f"No package candidates found for optional capability '{capability}'") + return None + + reranked = [] + for pkg, score, alias in ranked: + bonus = 0.0 + norm_pkg = normalize(pkg) + for token in prefer: + if normalize(token) in norm_pkg: + bonus += 0.08 + reranked.append((pkg, score + bonus, alias)) + reranked.sort(key=lambda x: x[1], reverse=True) + + top_preview = ", ".join([f"{pkg}={score:.3f}" for pkg, score, _ in reranked[:10]]) + info(f"Top package guesses for {capability}: {top_preview}") + state = read_state() + state[f"candidates_{capability}"] = [{"package": pkg, "score": score, "alias": alias} for pkg, score, alias in reranked[:10]] + write_state(state) + + attempted = [] + for pkg, score, alias in reranked: + attempted.append((pkg, score)) + if pacman_installed(msys_root, pkg): + ok(f"{pkg} already installed for capability {capability}") + if not commands or msys_command_exists(msys_root, commands): + return pkg + + if install_package(msys_root, pkg): + if not commands or msys_command_exists(msys_root, commands): + ok(f"Capability {capability} satisfied by {pkg} (alias={alias}, score={score:.3f})") + return pkg + warn(f"{pkg} installed but capability {capability} still not verified; trying next guess...") + else: + warn(f"Install failed for candidate {pkg} (score={score:.3f}); trying next guess...") + + if required: + raise RuntimeError(f"Failed to satisfy required capability '{capability}'. Tried: {attempted[:8]}") + warn(f"Optional capability '{capability}' was not satisfied.") + return None + + +def ensure_capabilities(msys_root: Path, repo_packages: Sequence[str]) -> Dict[str, Optional[str]]: + installed = {} + for capability in CAPABILITIES: + installed[capability] = resolve_and_install_capability(msys_root, capability, repo_packages) + return installed + + +def ensure_python_support_env(msys_root: Path) -> Optional[Path]: + info("Preparing Python support environment for openshot-qt...") + venv_dir = BUILD_ROOT / "pyenv" + msys_venv = windows_to_msys_path(venv_dir) + base_python = msys_resolve_python(msys_root) + + script = f""" +rm -rf {shell_quote(msys_venv)} +{base_python} -m venv --system-site-packages {shell_quote(msys_venv)} +if [ -x {shell_quote(msys_venv + "/bin/python")} ]; then + VENV_PY={shell_quote(msys_venv + "/bin/python")} + VENV_PIP={shell_quote(msys_venv + "/bin/pip")} +else + VENV_PY={shell_quote(msys_venv + "/Scripts/python.exe")} + VENV_PIP={shell_quote(msys_venv + "/Scripts/pip.exe")} +fi +"$VENV_PY" -m pip install --upgrade pip setuptools wheel +"$VENV_PIP" install httplib2 tinys3 github3.py==0.9.6 requests sentry-sdk cx_Freeze +""" + rc = run_msys_script(msys_root, "prepare_python_env.sh", script, env_name="ucrt64") + venv_python_candidates = [ + venv_dir / "bin" / "python.exe", + venv_dir / "bin" / "python", + venv_dir / "Scripts" / "python.exe", + venv_dir / "Scripts" / "python", + ] + venv_python = next((p for p in venv_python_candidates if p.exists()), None) + ok_env = rc == 0 and venv_python is not None + record_artifact("python_venv", str(venv_python or (venv_dir / "bin" / "python")), ok_env, "venv extras ready" if ok_env else "venv extras unavailable") + if ok_env: + ok(f"Python support environment ready: {venv_python}") + return venv_python + warn("Python venv extras were not fully prepared. Falling back to system MSYS2 Python.") + return None + + +def git_clone_or_update(name: str, url: str, root: Path) -> Path: + dest = root / name + if (dest / ".git").exists(): + info(f"Updating existing repo: {name}") + run_stream(["git", "-C", str(dest), "fetch", "--all", "--tags"], check=True) + run_stream(["git", "-C", str(dest), "pull", "--ff-only"], check=True) + elif dest.exists(): + warn(f"{dest} exists but is not a git repo; leaving it untouched.") + else: + info(f"Cloning {name} from {url}") + run_stream(["git", "clone", url, str(dest)], check=True) + return dest + + +def clone_or_update_repos(root: Path) -> Dict[str, Path]: + root.mkdir(parents=True, exist_ok=True) + repos: Dict[str, Path] = {} + for name, url in REPOS.items(): + repos[name] = git_clone_or_update(name, url, root) + record_artifact(f"repo_{name}", str(repos[name]), repos[name].exists(), "repo ready") + return repos + + +def write_python_probe( + script_path: Path, + module_name: str, + extra_lines: Optional[List[str]] = None, + bootstrap_lines: Optional[List[str]] = None, +) -> Path: + script_path.parent.mkdir(parents=True, exist_ok=True) + lines = [ + "import importlib", + "import os", + "import sys", + ] + if bootstrap_lines: + lines.extend(bootstrap_lines) + lines.extend([ + f"name = {module_name!r}", + "mod = importlib.import_module(name)", + "print(f\"module={name}\")", + "print(f\"file={getattr(mod, '__file__', '')}\")", + "print(f\"sys_path_0={sys.path[0] if sys.path else ''}\")", + "print(f\"path={os.environ.get('PYTHONPATH', '')}\")", + ]) + if extra_lines: + lines.extend(extra_lines) + script_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return script_path + + +def verify_openshot_import(msys_root: Path, repo_dir: Path, preferred_python: Optional[Path] = None) -> Dict[str, object]: + probe = BUILD_ROOT / "verify_openshot_import.py" + bindings_dir = repo_dir / "build" / "bindings" / "python" + build_src_dir = repo_dir / "build" / "src" + ucrt_bin_dir = msys_root / "ucrt64" / "bin" + + runtime_info = query_python_runtime_info(msys_root, preferred_python) + site_packages: List[Path] = [] + for item in runtime_info.get("site_packages", []) or []: + try: + site_packages.append(Path(str(item))) + except Exception: + pass + user_site = runtime_info.get("user_site") + if user_site: + try: + site_packages.append(Path(str(user_site))) + except Exception: + pass + + bootstrap_common = [ + f"build_src_dir = {str(build_src_dir)!r}", + f"ucrt_bin_dir = {str(ucrt_bin_dir)!r}", + "path_parts = []", + "for part in (build_src_dir, ucrt_bin_dir):", + " if os.path.isdir(part):", + " path_parts.append(part)", + " add_dir = getattr(os, 'add_dll_directory', None)", + " if add_dir:", + " try:", + " add_dir(part)", + " except OSError:", + " pass", + "if path_parts:", + " os.environ['PATH'] = os.pathsep.join(path_parts + [os.environ.get('PATH', '')])", + "print(f'bootstrap_build_src={build_src_dir}')", + "print(f'bootstrap_ucrt_bin={ucrt_bin_dir}')", + ] + + installed_bootstrap = list(bootstrap_common) + installed_bootstrap.extend([ + "print('probe_mode=installed')", + "print(f'sys_path_0={sys.path[0] if sys.path else ''}')", + ]) + write_python_probe(probe, "openshot", [ + "version = getattr(mod, 'Version', None)", + "print(f'version={version}')", + ], bootstrap_lines=installed_bootstrap) + + python_cmd = msys_resolve_python(msys_root, preferred_python) + probe_cmd = f"{python_cmd} {shell_quote(windows_to_msys_path(probe))}" + installed_cp = msys_capture(msys_root, probe_cmd, env_name="ucrt64") + installed_ok = installed_cp.returncode == 0 and 'module=openshot' in installed_cp.stdout + + source_cp = installed_cp + source_ok = False + if not installed_ok: + source_bootstrap = list(bootstrap_common) + source_bootstrap.extend([ + f"bindings_dir = {str(bindings_dir)!r}", + "if os.path.isdir(bindings_dir) and bindings_dir not in sys.path:", + " sys.path.insert(0, bindings_dir)", + "print('probe_mode=source-build')", + "print(f'bootstrap_bindings={bindings_dir}')", + ]) + write_python_probe(probe, "openshot", [ + "version = getattr(mod, 'Version', None)", + "print(f'version={version}')", + ], bootstrap_lines=source_bootstrap) + source_cp = msys_capture(msys_root, probe_cmd, env_name="ucrt64") + source_ok = source_cp.returncode == 0 and 'module=openshot' in source_cp.stdout + + installed_detail = (installed_cp.stdout + installed_cp.stderr).strip() + source_detail = (source_cp.stdout + source_cp.stderr).strip() if source_cp is not installed_cp else "" + if installed_ok: + mode = "installed" + detail = installed_detail + elif source_ok: + mode = "source-build" + detail = source_detail + else: + mode = "failed" + detail = (installed_detail + "\n\n--- source fallback ---\n" + source_detail).strip() if source_detail else installed_detail + + return { + "ok": installed_ok or source_ok, + "detail": detail, + "mode": mode, + "installed_import_ok": installed_ok, + "source_import_ok": source_ok, + "runtime_info": runtime_info, + "site_packages": [str(p) for p in site_packages], + } + + +def build_libopenshot_audio(msys_root: Path, repo_dir: Path) -> bool: + info("Building libopenshot-audio...") + repo_msys = windows_to_msys_path(repo_dir) + rc = run_msys_script( + msys_root, + "build_libopenshot_audio.sh", + f""" +cd '{repo_msys}' +rm -rf build +cmake -S . -B build -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ucrt64 -DCMAKE_PREFIX_PATH=/ucrt64 +cmake --build build --parallel +cmake --install build +""", + env_name="ucrt64", + ) + install_manifest = repo_dir / "build" / "install_manifest.txt" + cache_file = repo_dir / "build" / "CMakeCache.txt" + build_outputs = list((repo_dir / "build").rglob("*openshot*audio*")) + list((repo_dir / "build").rglob("*OpenShotAudio*")) + installed_outputs = list((MSYS_ROOT_DEFAULT / "ucrt64").rglob("*openshot*audio*")) + list((MSYS_ROOT_DEFAULT / "ucrt64").rglob("*OpenShotAudio*")) + + record_artifact("audio_build_cache", str(cache_file), cache_file.exists(), "libopenshot-audio CMake cache") + record_artifact("audio_install_manifest", str(install_manifest), install_manifest.exists(), "libopenshot-audio install manifest") + record_artifact("audio_build_outputs", str(repo_dir / "build"), bool(build_outputs), f"{len(build_outputs)} libopenshot-audio build outputs found") + record_artifact("audio_installed_outputs", str(MSYS_ROOT_DEFAULT / "ucrt64"), bool(installed_outputs), f"{len(installed_outputs)} installed audio outputs found") + + ok_build = rc == 0 and cache_file.exists() and (install_manifest.exists() or bool(build_outputs) or bool(installed_outputs)) + return ok_build + + +def build_libopenshot(msys_root: Path, repo_dir: Path, preferred_python: Optional[Path] = None) -> Dict[str, object]: + info("Building libopenshot...") + repo_msys = windows_to_msys_path(repo_dir) + rc = run_msys_script( + msys_root, + "build_libopenshot.sh", + f""" +cd '{repo_msys}' +rm -rf build +export LIBOPENSHOT_AUDIO_DIR=/ucrt64 +cmake -S . -B build -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ucrt64 -DCMAKE_PREFIX_PATH=/ucrt64 +cmake --build build --parallel +cmake --install build || true +""", + env_name="ucrt64", + ) + cache_file = repo_dir / "build" / "CMakeCache.txt" + install_manifest = repo_dir / "build" / "install_manifest.txt" + bindings_dir = repo_dir / "build" / "bindings" / "python" + openshot_py = bindings_dir / "openshot.py" + openshot_pyd = next(iter(bindings_dir.glob("*_openshot*.pyd")), None) + if openshot_pyd is None: + openshot_pyd = next(iter(bindings_dir.glob("_openshot*.pyd")), None) + build_src = repo_dir / "build" / "src" + build_dll = next(iter(build_src.glob("libopenshot*.dll")), None) + build_import_lib = next(iter(build_src.glob("libopenshot*.a")), None) + installed_dll = next(iter((MSYS_ROOT_DEFAULT / "ucrt64" / "bin").glob("libopenshot.dll")), None) + + verify = verify_openshot_import(msys_root, repo_dir, preferred_python) + runtime_info = verify.get("runtime_info", {}) if isinstance(verify.get("runtime_info"), dict) else {} + site_package_dirs: List[Path] = [] + for item in verify.get("site_packages", []) or []: + try: + site_package_dirs.append(Path(str(item))) + except Exception: + pass + if not site_package_dirs: + for site_packages_dir in (MSYS_ROOT_DEFAULT / "ucrt64" / "lib").glob("python*/site-packages"): + site_package_dirs.append(site_packages_dir) + + installed_pyd = None + installed_py = None + for site_packages_dir in site_package_dirs: + candidate_py = site_packages_dir / "openshot.py" + candidate_pyd = next(iter(site_packages_dir.glob("_openshot*.pyd")), None) + if candidate_py.exists() and installed_py is None: + installed_py = candidate_py + if candidate_pyd and installed_pyd is None: + installed_pyd = candidate_pyd + if installed_py and installed_pyd: + break + + built_enough = cache_file.exists() and openshot_py.exists() and bool(openshot_pyd) and bool(build_dll) + source_ready = openshot_py.exists() and bool(openshot_pyd) and bindings_dir.exists() and bool(build_dll) + + record_artifact("python_runtime_info", None, bool(runtime_info.get("ok")), runtime_info.get("detail", ""), status="OK" if runtime_info.get("ok") else "WARN") + record_artifact("python_runtime_site_packages", "; ".join(str(p) for p in site_package_dirs) if site_package_dirs else None, bool(site_package_dirs), f"{len(site_package_dirs)} python site-packages path(s) discovered", status="OK" if site_package_dirs else "WARN") + record_artifact("libopenshot_build_cache", str(cache_file), cache_file.exists(), "libopenshot CMake cache") + record_artifact("libopenshot_install_manifest", str(install_manifest), install_manifest.exists(), "libopenshot install manifest", status="OK" if install_manifest.exists() else ("WARN" if source_ready else "MISS")) + record_artifact("libopenshot_bindings_dir", str(bindings_dir), bindings_dir.exists(), "libopenshot Python bindings dir") + record_artifact("libopenshot_py", str(openshot_py), openshot_py.exists(), "generated openshot.py binding") + record_artifact("libopenshot_pyd", str(openshot_pyd) if openshot_pyd else str(bindings_dir / "_openshot*.pyd"), bool(openshot_pyd), "compiled _openshot extension") + record_artifact("libopenshot_build_dll", str(build_dll) if build_dll else str(build_src / "libopenshot*.dll"), bool(build_dll), "built libopenshot DLL") + record_artifact("libopenshot_build_lib", str(build_import_lib) if build_import_lib else str(build_src / "libopenshot*.a"), bool(build_import_lib), "built libopenshot import/static library", status="OK" if build_import_lib else ("WARN" if bool(build_dll) else "MISS")) + record_artifact("libopenshot_installed_dll", str(installed_dll) if installed_dll else str(MSYS_ROOT_DEFAULT / "ucrt64" / "bin" / "libopenshot*.dll"), bool(installed_dll), "installed libopenshot DLL", status="OK" if installed_dll else ("WARN" if source_ready else "MISS")) + record_artifact("libopenshot_installed_py", str(installed_py) if installed_py else None, bool(installed_py), "installed openshot.py binding", status="OK" if installed_py else ("WARN" if verify.get("mode") == "source-build" else "MISS")) + record_artifact("libopenshot_installed_pyd", str(installed_pyd) if installed_pyd else None, bool(installed_pyd), "installed libopenshot Python extension", status="OK" if installed_pyd else ("WARN" if verify.get("mode") == "source-build" else "MISS")) + record_artifact("python_openshot_import", None, bool(verify.get("ok")), verify.get("detail", ""), status="OK" if verify.get("ok") else ("WARN" if source_ready else "MISS")) + record_artifact("python_openshot_import_mode", None, bool(verify.get("ok")), str(verify.get("mode", "failed")), status="OK" if verify.get("ok") else ("WARN" if source_ready else "MISS")) + record_artifact("python_openshot_import_installed", None, bool(verify.get("installed_import_ok")), "installed site-packages import probe", status="OK" if verify.get("installed_import_ok") else ("WARN" if verify.get("source_import_ok") else "MISS")) + record_artifact("python_openshot_import_source", None, bool(verify.get("source_import_ok")), "source-build import probe", status="OK" if verify.get("source_import_ok") else ("WARN" if verify.get("installed_import_ok") else "MISS")) + + compile_ok = rc == 0 and built_enough + if compile_ok and verify.get("ok"): + stage_status = "PASS" + if verify.get("mode") == "installed": + stage_detail = "Video library built and installed Python bindings import successfully" + else: + stage_detail = "Video library built; source-build Python bindings import successfully" + elif compile_ok: + stage_status = "WARN" + stage_detail = "Video library built, but Python import verification still needs runtime bootstrap" + else: + stage_status = "FAIL" + stage_detail = "Video build did not leave the expected binary and binding artifacts" + + return { + "ok": compile_ok, + "runtime_ok": bool(verify.get("ok")), + "stage_status": stage_status, + "stage_detail": stage_detail, + "compile_ok": compile_ok, + "source_ready": source_ready, + "verify": verify, + } + + +def create_launcher(root: Path, msys_root: Path, qt_repo: Path, preferred_python: Optional[Path]) -> Path: + bootstrap_path = create_runtime_bootstrap_script(root, msys_root, qt_repo) + create_distribution_helper(root, msys_root, qt_repo, preferred_python, bootstrap_path) + create_portable_distribution_helper(root, msys_root, qt_repo, preferred_python) + + launcher = root / "Launch-OpenShot-Qt.cmd" + lib_repo = root / "libopenshot" + bindings_dir = lib_repo / "build" / "bindings" / "python" + build_src_dir = lib_repo / "build" / "src" + qt_src_dir = qt_repo / "src" + python_exe = resolve_windows_python(preferred_python, msys_root) + python_cmd = windows_cmd_quote(python_exe) if python_exe else "python" + + lines = [ + "@echo off", + "setlocal", + f"set OPENSHOT_BUILD_ROOT={root}", + f"set OPENSHOT_QT_REPO={qt_repo}", + f"set OPENSHOT_BOOTSTRAP_PATHS={qt_src_dir};{bindings_dir}", + f"set PYTHONPATH={bindings_dir};{qt_src_dir};%PYTHONPATH%", + f"set PATH={build_src_dir};{msys_root / 'ucrt64' / 'bin'};%PATH%", + f'cd /d "{qt_repo}"', + f"{python_cmd} {windows_cmd_quote(bootstrap_path)} %*", + "endlocal", + "", + ] + launcher.write_text("\r\n".join(str(line) for line in lines), encoding="utf-8", newline="\r\n") + record_artifact("launcher_cmd", str(launcher), launcher.exists(), "Launch OpenShot .cmd") + ok(f"Created launcher: {launcher}") + return launcher + + +def verify_qt_launcher(msys_root: Path, qt_repo: Path, preferred_python: Optional[Path]) -> bool: + launch_py = qt_repo / "src" / "launch.py" + record_artifact("openshot_qt_launch_py", str(launch_py), launch_py.exists(), "openshot-qt launch script") + bootstrap = BUILD_ROOT / "Launch-OpenShot-Qt.py" + record_artifact("launcher_bootstrap_py", str(bootstrap), bootstrap.exists(), "Python runtime bootstrap launcher") + if not launch_py.exists() or not bootstrap.exists(): + return False + + python_exe = resolve_windows_python(preferred_python, msys_root) + if python_exe is None: + record_artifact("openshot_qt_runtime_smoke", None, False, "Unable to resolve Windows Python for runtime smoke test", status="WARN") + return False + + cp = run_capture([str(python_exe), str(bootstrap), "--installer-smoke-import"], cwd=qt_repo) + smoke_ok = cp.returncode == 0 and "SMOKE_IMPORT_OK" in cp.stdout + detail = (cp.stdout.strip() or cp.stderr.strip()) + record_artifact("openshot_qt_smoke_import", None, smoke_ok, detail, status="OK" if smoke_ok else "WARN") + record_artifact("openshot_qt_runtime_smoke", None, smoke_ok, detail, status="OK" if smoke_ok else "WARN") + return smoke_ok + + +def print_summary_from_state(state: dict) -> None: + print("\n" + "=" * 72) + print(cyan(APP_NAME)) + print("=" * 72) + stages = state.get("stages", {}) + for stage_name in [ + "Prerequisites", + "Dependencies", + "Repositories", + "Build libopenshot-audio", + "Build libopenshot", + "Prepare openshot-qt", + "Verification", + ]: + st = stages.get(stage_name, {}) + status = st.get("status", "SKIP") + detail = st.get("detail", "") + if status == "PASS": + marker = green("🟢 PASS") + elif status == "FAIL": + marker = red("🔴 FAIL") + elif status == "WARN": + marker = yellow("🟡 WARN") + else: + marker = cyan("🔵 " + status) + print(f"{marker:<12} {stage_name}: {detail}") + + print("\nArtifacts:") + for name, art in state.get("artifacts", {}).items(): + status = str(art.get("status") or ("OK" if art.get("exists") else "MISS")).upper() + if status == "OK": + marker = green("OK") + elif status == "WARN": + marker = yellow("WARN") + else: + marker = red("MISS") + path = art.get("path") or "" + detail = art.get("detail") or "" + print(f" {marker:>4} {name}: {path} {detail}".rstrip()) + + launcher = state.get("launcher_path") + run_command = state.get("run_command") + print("\nHow to run OpenShot:") + if launcher: + print(f" 1) Double-click: {launcher}") + print(f' 2) Or run: cmd /c "{launcher}"') + if run_command: + print(f" 3) Raw command: {run_command}") + print(f"\nLog file: {LOG_PATH}") + print("=" * 72 + "\n") + + +def maybe_prompt_run(state: dict) -> None: + launcher = state.get("launcher_path") + success = bool(state.get("success")) + if not success or not launcher or not Path(launcher).exists(): + return + try: + answer = input("OpenShot looks ready. Run it now? [Y/n]: ").strip().lower() + except EOFError: + return + if answer in ("", "y", "yes"): + info(f"Launching OpenShot via {launcher}") + subprocess.Popen(["cmd", "/c", launcher], cwd=str(Path(launcher).parent)) + else: + info("Run skipped by user.") + + +def do_install_work(prompt_at_end: bool) -> int: + reset_state_files() + enable_ansi_colors() + info(APP_FULL_NAME) + try: + info(f"Installer file md5={script_md5(SCRIPT_PATH)}") + info(f"Installer file lmd={script_lmd(SCRIPT_PATH)}") + except Exception as exc: + warn(f"Unable to fingerprint installer file: {exc}") + trace(f"argv={sys.argv!r}") + trace(f"cwd={Path.cwd()}") + trace(f"script={SCRIPT_PATH}") + + if not host_python_ok(): + raise RuntimeError(f"Host Python must be >= {SUPPORTED_HOST_PYTHON_MIN[0]}.{SUPPORTED_HOST_PYTHON_MIN[1]}") + + check_windows_support() + + record_stage("Prerequisites", "RUNNING", "Checking admin, winget, git, and MSYS2") + ensure_winget() + ensure_git() + msys_root = ensure_msys2() + ok("Prerequisites are ready") + update_state(msys_root=str(msys_root)) + record_stage("Prerequisites", "PASS", "Admin, winget, git, and MSYS2 verified") + + record_stage("Dependencies", "RUNNING", "Refreshing MSYS2 and resolving live packages") + msys_update(msys_root) + repo_packages = get_ucrt_repo_packages(msys_root) + update_state(repo_package_count=len(repo_packages)) + ensured = ensure_capabilities(msys_root, repo_packages) + update_state(capabilities=ensured) + preferred_python = ensure_python_support_env(msys_root) + record_stage("Dependencies", "PASS", f"Resolved {len(ensured)} capability entries against {len(repo_packages)} live packages") + + record_stage("Repositories", "RUNNING", "Cloning or updating OpenShot repositories") + repos = clone_or_update_repos(BUILD_ROOT) + update_state(repos={k: str(v) for k, v in repos.items()}) + record_stage("Repositories", "PASS", "OpenShot repositories are present") + + record_stage("Build libopenshot-audio", "RUNNING", "Building and installing audio library") + audio_ok = build_libopenshot_audio(msys_root, repos["libopenshot-audio"]) + if audio_ok: + record_stage("Build libopenshot-audio", "PASS", "Audio library built and install manifest found") + else: + record_stage("Build libopenshot-audio", "FAIL", "Audio build did not leave expected artifacts") + raise RuntimeError("libopenshot-audio build failed verification") + + record_stage("Build libopenshot", "RUNNING", "Building and installing video library and Python bindings") + lib_result = build_libopenshot(msys_root, repos["libopenshot"], preferred_python) + record_stage("Build libopenshot", lib_result["stage_status"], lib_result["stage_detail"]) + if not lib_result["ok"]: + raise RuntimeError("libopenshot build failed to produce the expected binary outputs") + + record_stage("Prepare openshot-qt", "RUNNING", "Patching launch.py, creating runtime bootstrap, and validating startup") + patched_launch = patch_openshot_qt_launch(repos["openshot-qt"]) + launcher = create_launcher(BUILD_ROOT, msys_root, repos["openshot-qt"], preferred_python) + qt_ok = verify_qt_launcher(msys_root, repos["openshot-qt"], preferred_python) + update_state( + launcher_path=str(launcher), + run_command=f'cmd /c "{launcher}"', + preferred_python=str(preferred_python) if preferred_python else "", + launch_patch_applied=patched_launch, + ) + if qt_ok: + record_stage("Prepare openshot-qt", "PASS", "launch.py patched, bootstrap launcher created, and runtime smoke import verified") + else: + record_stage("Prepare openshot-qt", "WARN", "Runtime launcher was created, but the final startup smoke test still needs adjustment") + + record_stage("Verification", "RUNNING", "Checking final expected files and launch entry points") + artifacts = read_state().get("artifacts", {}) + success = ( + artifacts.get("audio_install_manifest", {}).get("exists") and + artifacts.get("libopenshot_bindings_dir", {}).get("exists") and + artifacts.get("libopenshot_py", {}).get("exists") and + artifacts.get("libopenshot_pyd", {}).get("exists") and + artifacts.get("libopenshot_build_dll", {}).get("exists") and + artifacts.get("openshot_qt_launch_py", {}).get("exists") and + artifacts.get("openshot_qt_launch_patch", {}).get("exists") and + artifacts.get("launcher_bootstrap_py", {}).get("exists") and + artifacts.get("launcher_cmd", {}).get("exists") and + artifacts.get("openshot_qt_runtime_smoke", {}).get("exists") + ) + if success: + record_stage("Verification", "PASS", "Build artifacts exist and the real runtime bootstrap smoke test passed") + ok("OpenShot source build finished successfully") + else: + record_stage("Verification", "FAIL", "Build artifacts exist, but the real runtime bootstrap smoke test did not pass yet") + raise RuntimeError("Build artifacts were created, but the final runtime bootstrap smoke test failed") + + state = read_state() + state["completed"] = True + state["success"] = True + state["exit_code"] = 0 + state["finished"] = timestamp() + write_state(state) + + print_summary_from_state(state) + if prompt_at_end: + maybe_prompt_run(state) + return 0 + + +def normalize_cli_token(token: str) -> str: + return token.strip().lower() + + +def read_local_text(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except Exception: + return "" + + +def build_docs_index_text() -> str: + return textwrap.dedent(f"""\ + {APP_FULL_NAME} + {'=' * 72} + Documentation suite: + README.md Project overview, upstream context, and contribution framing + INSTALL.md Fast path, workflow, and stock-Windows bootstrap notes + MANUAL_INSTALL.md Manual package URLs, package roles, and direct install commands + LOG_GUIDE.md Successful log walkthrough and stage meanings + TROUBLESHOOTING.md Permission, install, runtime, and packaging fixes + RELEASE_GUIDE.md Release-candidate and ship checklist + CHANGELOG.md Release history + CONTRIBUTING.md Contribution rules and review expectations + SECURITY.md Helper-specific vulnerability reporting guidance + CODE_OF_CONDUCT.md Project behavior expectations + LICENSE.txt MIT license text + help.html Local browser help with OpenShot attribution, workflow notes, and installer comparison + + Project links: + Author site: {WEBSITE_URL} + BnR repo: {REPO_URL} + OpenShot site: {OPENSHOT_PROJECT_URL} + OpenShot repo: {OPENSHOT_REPO_URL} + OpenShot docs: {OPENSHOT_DOCS_URL} + """).strip() + + +def build_usage_text() -> str: + return textwrap.dedent(f"""\ + {APP_FULL_NAME} + {'=' * 72} + Usage: + py -3 {SCRIPT_PATH.name} Run the full install/build/launch-prep workflow + py -3 {SCRIPT_PATH.name} [info-switch] Print docs or diagnostics and exit + + Quick commands: + usage : --usage -usage /usage usage -u /u /U + help : --help -help /help help -h /h /? ? + man : man --man -man /man manual --manual -manual /manual + about : --about -about /about about + ver : --version -version /version version --ver -ver /ver ver -v /v + docs : --docs -docs /docs docs + debug : --debug -debug /debug debug + + Read next: + help.html, README.md, INSTALL.md + """).strip() + + +def build_help_text() -> str: + return textwrap.dedent(f"""\ + {APP_FULL_NAME} + {'=' * 72} + Help summary: + This script bootstraps, builds, verifies, launches, and prepares helper outputs + for OpenShot source builds on Windows. Use no switches to run the real workflow. Use the + switches below when you want docs, diagnostics, or project metadata without touching + the build. + + Information commands: + --usage -usage /usage usage -u /u /U + Quick command reminder. + + --help -help /help help -h /h /? ? + Detailed command guide. + + man --man -man /man manual --manual -manual /manual + Full manual-style reference. + + --about -about /about about + Project purpose, value, and contact info. + + --version -version /version version --ver -ver /ver ver -v /v + Version banner and project links. + + --docs -docs /docs docs + Print the local docs index. + + --readme -readme /readme readme + Print README.md. + + --install -install /install install + Print INSTALL.md. + + --manual-install -manual-install /manual-install manual-install manualinstall + Print MANUAL_INSTALL.md. + + --log-guide -log-guide /log-guide log-guide --logs -logs /logs logs + Print LOG_GUIDE.md. + + --troubleshoot -troubleshoot /troubleshoot troubleshoot + --troubleshooting -troubleshooting /troubleshooting troubleshooting + Print TROUBLESHOOTING.md. + + --release-guide -release-guide /release-guide release-guide + Print RELEASE_GUIDE.md. + + --changelog -changelog /changelog changelog + Print CHANGELOG.md. + + --contributing -contributing /contributing contributing + Print CONTRIBUTING.md. + + --security -security /security security + Print SECURITY.md. + + --code-of-conduct -code-of-conduct /code-of-conduct code-of-conduct + Print CODE_OF_CONDUCT.md. + + --license -license /license license + Print LICENSE.txt. + + --debug -debug /debug debug + Print local machine, tool, and file-discovery info. + + Common examples: + py -3 {SCRIPT_PATH.name} + py -3 {SCRIPT_PATH.name} --help + py -3 {SCRIPT_PATH.name} man + py -3 {SCRIPT_PATH.name} --debug + py -3 {SCRIPT_PATH.name} --docs + py -3 {SCRIPT_PATH.name} --install + py -3 {SCRIPT_PATH.name} --troubleshoot + """).strip() + + +def build_man_text() -> str: + return textwrap.dedent(f"""\ + NAME + {PRODUCT_NAME} - Windows bootstrap, build, and run helper for OpenShot source builds + + SYNOPSIS + py -3 {SCRIPT_PATH.name} + py -3 {SCRIPT_PATH.name} [information-switches] + + DESCRIPTION + {PRODUCT_NAME} turns a fragile Windows source-build process on stock Windows into a readable workflow. + It checks the machine, restores or installs toolchain pieces, resolves MSYS2 UCRT64 + packages against the live repo index, builds libopenshot-audio and libopenshot, verifies + Python bindings honestly, patches the launch path, and generates helper launchers. + + OpenShot already has upstream installer and packaging scripts for shipping builds. + This helper is aimed at a different problem: getting OpenShot building and launching + on a real stock Windows machine. + + Run it with no switches when you want the real build. Run it with an information switch + when you want documentation, diagnostics, version info, or local file help. + + INFORMATION SWITCHES + usage + --usage -usage /usage usage -u /u /U + help + --help -help /help help -h /h /? ? + man + man --man -man /man manual --manual -manual /manual + about + --about -about /about about + version + --version -version /version version --ver -ver /ver ver -v /v + docs + --docs -docs /docs docs + readme + --readme -readme /readme readme + install + --install -install /install install + manual install + --manual-install -manual-install /manual-install manual-install manualinstall + log guide + --log-guide -log-guide /log-guide log-guide --logs -logs /logs logs + troubleshooting + --troubleshoot -troubleshoot /troubleshoot troubleshoot + --troubleshooting -troubleshooting /troubleshooting troubleshooting + release guide + --release-guide -release-guide /release-guide release-guide + changelog + --changelog -changelog /changelog changelog + contributing + --contributing -contributing /contributing contributing + security + --security -security /security security + code of conduct + --code-of-conduct -code-of-conduct /code-of-conduct code-of-conduct + license + --license -license /license license + debug + --debug -debug /debug debug + + GENERATED OUTPUTS + C:\\OpenShotBuild\\Launch-OpenShot-Qt.cmd + C:\\OpenShotBuild\\Launch-OpenShot-Qt.py + C:\\OpenShotBuild\\Build-OpenShot-Frozen.cmd + C:\\OpenShotBuild\\Build-OpenShot-Portable.cmd + openshot-installer.log + openshot-installer-state.json + openshot-installer-relay.log + + SUCCESS LOOKS LIKE + 1. Prerequisites, Dependencies, Repositories, and build stages all pass. + 2. libopenshot bindings import in installed or source-build mode. + 3. Launch helpers are generated. + 4. OpenShot starts, initializes the UI, and ends the session cleanly. + + FILES + README.md, INSTALL.md, MANUAL_INSTALL.md, LOG_GUIDE.md, TROUBLESHOOTING.md, + RELEASE_GUIDE.md, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, + CODE_OF_CONDUCT.md, LICENSE.txt, help.html + + LINKS + Website: {WEBSITE_URL} + Repository: {REPO_URL} + OpenShot docs: {OPENSHOT_DOCS_URL} + """).strip() + + +def build_about_text() -> str: + return textwrap.dedent(f"""\ + {APP_FULL_NAME} + {'=' * 72} + OpenShot BnR is a Windows-first bootstrap, build, and launch helper for OpenShot + source builds. + + It exists for the painful part of the workflow: the stock machine, the stale machine, + the half-configured shell, the missing package, the almost-working launcher, and the + hours that disappear when none of those layers tell the truth. + + This helper is meant to support the OpenShot workflow, not replace the OpenShot + project's own installer or packaging work. + + OpenShot's upstream installer/build tooling packages finished builds. BnR focuses on + the earlier problem: getting OpenShot actually building and launching on stock Windows. + + OpenShot upstream: + Website: {OPENSHOT_PROJECT_URL} + Repository: {OPENSHOT_REPO_URL} + Docs: {OPENSHOT_DOCS_URL} + + What it does well: + - checks the machine before the costly steps begin + - restores or installs the Windows/MSYS2 tooling it needs + - builds the OpenShot native stack in order + - verifies Python bindings honestly instead of faking success + - repairs the runtime launch path so source-build mode actually launches + - leaves behind logs, state files, helper launchers, and docs you can inspect + + Benefit: + You spend less time guessing and more time shipping. + + Author: + Trenton Tompkins + {WEBSITE_URL} + + For a free consultation about Windows/OpenShot build automation: + (724) 431-5207 • trenttompkins@gmail.com + + Coded with ❤️ with ChatGPT. + """).strip() + + +def build_version_text() -> str: + return textwrap.dedent(f"""\ + {APP_FULL_NAME} + Version: {VERSION} + BnR repo: {REPO_URL} + Author site: {WEBSITE_URL} + OpenShot site: {OPENSHOT_PROJECT_URL} + OpenShot repo: {OPENSHOT_REPO_URL} + OpenShot docs: {OPENSHOT_DOCS_URL} + Coded with ❤️ with ChatGPT. + """).strip() + + +def print_document(title: str, path: Path, fallback: str = "") -> None: + print(title) + print("=" * max(len(title), 24)) + content = read_local_text(path).strip() + if content: + print(content) + elif fallback: + print(fallback.strip()) + else: + print(f"Document not found: {path}") + + +def build_debug_report() -> str: + tools = { + "python": sys.executable, + "git": shutil.which("git") or "not found", + "winget": shutil.which("winget") or "not found", + "cmake": shutil.which("cmake") or "not found", + "msys2_shell": str(MSYS_ROOT_DEFAULT / "msys2_shell.cmd") if (MSYS_ROOT_DEFAULT / "msys2_shell.cmd").exists() else "not found", + "pacman": str(MSYS_ROOT_DEFAULT / "usr" / "bin" / "pacman.exe") if (MSYS_ROOT_DEFAULT / "usr" / "bin" / "pacman.exe").exists() else "not found", + } + lines = [ + APP_FULL_NAME, + "=" * 72, + f"Script path: {SCRIPT_PATH}", + f"Script dir: {SCRIPT_DIR}", + f"Build root: {BUILD_ROOT}", + f"Windows: {platform.platform()}", + f"Python: {platform.python_version()} ({sys.executable})", + f"Admin: {'yes' if is_windows() and is_admin() else 'no'}", + f"README.md: {'found' if README_MD_PATH.exists() else 'missing'} -> {README_MD_PATH}", + f"INSTALL.md: {'found' if INSTALL_MD_PATH.exists() else 'missing'} -> {INSTALL_MD_PATH}", + f"MANUAL_INSTALL.md: {'found' if MANUAL_INSTALL_MD_PATH.exists() else 'missing'} -> {MANUAL_INSTALL_MD_PATH}", + f"LOG_GUIDE.md: {'found' if LOG_GUIDE_MD_PATH.exists() else 'missing'} -> {LOG_GUIDE_MD_PATH}", + f"TROUBLESHOOTING.md: {'found' if TROUBLESHOOTING_MD_PATH.exists() else 'missing'} -> {TROUBLESHOOTING_MD_PATH}", + f"RELEASE_GUIDE.md: {'found' if RELEASE_GUIDE_MD_PATH.exists() else 'missing'} -> {RELEASE_GUIDE_MD_PATH}", + f"CHANGELOG.md: {'found' if CHANGELOG_MD_PATH.exists() else 'missing'} -> {CHANGELOG_MD_PATH}", + f"CONTRIBUTING.md: {'found' if CONTRIBUTING_MD_PATH.exists() else 'missing'} -> {CONTRIBUTING_MD_PATH}", + f"SECURITY.md: {'found' if SECURITY_MD_PATH.exists() else 'missing'} -> {SECURITY_MD_PATH}", + f"CODE_OF_CONDUCT.md: {'found' if CODE_OF_CONDUCT_MD_PATH.exists() else 'missing'} -> {CODE_OF_CONDUCT_MD_PATH}", + f"LICENSE.txt: {'found' if LICENSE_TXT_PATH.exists() else 'missing'} -> {LICENSE_TXT_PATH}", + f"help.html: {'found' if HELP_HTML_PATH.exists() else 'missing'} -> {HELP_HTML_PATH}", + "", + "Tool discovery:", + ] + for name, value in tools.items(): + lines.append(f" - {name}: {value}") + lines.extend([ + "", + "Information command groups:", + " usage, help, man, about, version, docs, readme, install, manual-install, log-guide, troubleshooting, release-guide, changelog, contributing, security, code-of-conduct, license, debug", + ]) + return "\n".join(lines) + + +def try_handle_information_flags(argv: Sequence[str]) -> Optional[int]: + filtered = [arg for arg in argv[1:] if arg not in (CHILD_ARG, NO_PROMPT_ARG)] + if not filtered: + return None + + normalized = [normalize_cli_token(arg) for arg in filtered] + + alias_to_action = {} + for aliases, action in [ + (USAGE_ALIASES, "usage"), + (HELP_ALIASES, "help"), + (MAN_ALIASES, "man"), + (ABOUT_ALIASES, "about"), + (VERSION_ALIASES, "version"), + (DOCS_ALIASES, "docs"), + (README_ALIASES, "readme"), + (INSTALL_ALIASES, "install"), + (MANUAL_INSTALL_ALIASES, "manual-install"), + (LOG_GUIDE_ALIASES, "log-guide"), + (TROUBLESHOOTING_ALIASES, "troubleshooting"), + (RELEASE_GUIDE_ALIASES, "release-guide"), + (CHANGELOG_ALIASES, "changelog"), + (CONTRIBUTING_ALIASES, "contributing"), + (SECURITY_ALIASES, "security"), + (CODE_OF_CONDUCT_ALIASES, "code-of-conduct"), + (LICENSE_ALIASES, "license"), + (DEBUG_ALIASES, "debug"), + ]: + for alias in aliases: + alias_to_action[alias.lower()] = action + + if not all(token in alias_to_action for token in normalized): + return None + + def show(action: str) -> None: + if action == "usage": + print(build_usage_text()) + elif action == "help": + print(build_help_text()) + elif action == "man": + print(build_man_text()) + elif action == "about": + print(build_about_text()) + elif action == "version": + print(build_version_text()) + elif action == "docs": + print(build_docs_index_text()) + elif action == "readme": + print_document("README.md", README_MD_PATH, build_about_text()) + elif action == "install": + print_document("INSTALL.md", INSTALL_MD_PATH, "INSTALL.md is missing from this folder.") + elif action == "manual-install": + print_document("MANUAL_INSTALL.md", MANUAL_INSTALL_MD_PATH, "MANUAL_INSTALL.md is missing from this folder.") + elif action == "log-guide": + print_document("LOG_GUIDE.md", LOG_GUIDE_MD_PATH, "LOG_GUIDE.md is missing from this folder.") + elif action == "troubleshooting": + print_document("TROUBLESHOOTING.md", TROUBLESHOOTING_MD_PATH, "TROUBLESHOOTING.md is missing from this folder.") + elif action == "release-guide": + print_document("RELEASE_GUIDE.md", RELEASE_GUIDE_MD_PATH, "RELEASE_GUIDE.md is missing from this folder.") + elif action == "changelog": + print_document("CHANGELOG.md", CHANGELOG_MD_PATH, "CHANGELOG.md is missing from this folder.") + elif action == "contributing": + print_document("CONTRIBUTING.md", CONTRIBUTING_MD_PATH, "CONTRIBUTING.md is missing from this folder.") + elif action == "security": + print_document("SECURITY.md", SECURITY_MD_PATH, "SECURITY.md is missing from this folder.") + elif action == "code-of-conduct": + print_document("CODE_OF_CONDUCT.md", CODE_OF_CONDUCT_MD_PATH, "CODE_OF_CONDUCT.md is missing from this folder.") + elif action == "license": + print_document("LICENSE.txt", LICENSE_TXT_PATH, "MIT License file is missing from this folder.") + elif action == "debug": + print(build_debug_report()) + + shown = [] + seen = set() + for token in normalized: + action = alias_to_action[token] + if action in seen: + continue + seen.add(action) + shown.append(action) + + for index, action in enumerate(shown): + show(action) + if index < len(shown) - 1: + print("\n" + "-" * 72 + "\n") + + return 0 if shown else None + + +def main() -> int: + enable_ansi_colors() + info_result = try_handle_information_flags(sys.argv) + if info_result is not None: + return info_result + + child_mode = CHILD_ARG in sys.argv + no_prompt = NO_PROMPT_ARG in sys.argv + + if not is_windows(): + print(red("[FAIL]"), "This installer only supports Windows.", flush=True) + return 1 + + if child_mode: + try: + if not is_admin(): + raise RuntimeError("Elevated child started without admin rights.") + ok("Running elevated") + return do_install_work(prompt_at_end=not no_prompt) + except Exception as exc: + fail(str(exc)) + tb = traceback.format_exc() + for line in tb.splitlines(): + trace(line) + state = read_state() + state["completed"] = True + state["success"] = False + state["exit_code"] = 1 + state["failed"] = True + state["error"] = str(exc) + state["traceback"] = tb + state["finished"] = timestamp() + write_state(state) + print_summary_from_state(state) + return 1 + + reset_state_files() + rc = elevate_and_wait() + state = read_state() + print_summary_from_state(state) + if rc == 0: + maybe_prompt_run(state) + return rc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/installer/BnRinstall_script-win/README.md b/installer/BnRinstall_script-win/README.md new file mode 100644 index 0000000000..9c7a52b9b2 --- /dev/null +++ b/installer/BnRinstall_script-win/README.md @@ -0,0 +1,121 @@ +# OpenShot BnR 1.0.1 + +**OpenShot BnR** is a Windows-first bootstrap, build, and launch helper for **OpenShot** source builds. + +This pass is shaped to read more like a practical contribution around OpenShot and less like a standalone product bundle. The main job is simple: **get OpenShot working on a stock or half-broken Windows machine with fewer mystery failures**. + +## How this differs from OpenShot's existing installer + +OpenShot already has official installer and packaging infrastructure in its upstream `installer/` directory. That tooling is aimed at **packaging and shipping OpenShot builds**. + +OpenShot BnR aims at a different problem: **bootstrapping a real Windows machine from zero to a working source build**. + +| Tooling | Primary job | +|---|---| +| OpenShot upstream installer/build scripts | Package, launch, and release finished OpenShot builds | +| OpenShot BnR | Restore prerequisites, clone/update repos, build native dependencies, verify bindings, and generate launch helpers on stock Windows | + +So this helper is best described as a **Windows bootstrap and diagnostics helper**. It supports the upstream project workflow, but it does not try to replace the project's official installer or release pipeline. + +## Upstream context + +OpenShot itself is the work of the **OpenShot project and contributors**. + +- OpenShot website: https://www.openshot.org/ +- OpenShot source repository: https://github.com/OpenShot/openshot-qt +- OpenShot developer docs: https://www.openshot.org/static/files/user-guide/developers.html +- OpenShot contribution guide: https://github.com/OpenShot/openshot-qt/blob/develop/CONTRIBUTING.md + +This helper sits beside that work. Its job is the painful part on a fresh or half-broken Windows machine: verifying prerequisites, restoring toolchain pieces, building dependencies in the right order, checking bindings honestly, and leaving behind logs and launch helpers that are readable. + +## What this helper does + +- checks elevation, WinGet, Git, and MSYS2 +- refreshes MSYS2 and resolves live UCRT64 dependency choices +- clones or updates: + - `libopenshot-audio` + - `libopenshot` + - `openshot-qt` +- builds the native stack in order +- verifies Python bindings in installed and source-build modes +- repairs the runtime launch path +- generates launcher plus portable and frozen helper files +- exposes docs and debug output from the command line + +## Why it may still be useful + +The upstream installer helps ship OpenShot. This helper helps **prepare the machine, build the native stack, and prove that the source build actually launches**. + +That makes it more of a **bootstrap and diagnostics helper** than a replacement installer. + +## Quick start + +```bash +py -3 OpenShot_BnR_v1_0.py +``` + +After a successful run: + +```bash +cmd /c "C:\OpenShotBuild\Launch-OpenShot-Qt.cmd" +py -3 "C:\OpenShotBuild\Launch-OpenShot-Qt.py" +``` + +## Information commands + +```bash +py -3 OpenShot_BnR_v1_0.py --usage +py -3 OpenShot_BnR_v1_0.py --help +py -3 OpenShot_BnR_v1_0.py --about +py -3 OpenShot_BnR_v1_0.py --version +py -3 OpenShot_BnR_v1_0.py --docs +py -3 OpenShot_BnR_v1_0.py --install +py -3 OpenShot_BnR_v1_0.py --manual-install +py -3 OpenShot_BnR_v1_0.py --log-guide +py -3 OpenShot_BnR_v1_0.py --troubleshoot +py -3 OpenShot_BnR_v1_0.py --release-guide +py -3 OpenShot_BnR_v1_0.py --debug +``` + +## Docs included + +- [INSTALL.md](INSTALL.md) — install flow and result reading +- [MANUAL_INSTALL.md](MANUAL_INSTALL.md) — manual dependency references and upstream links +- [LOG_GUIDE.md](LOG_GUIDE.md) — successful log walkthrough +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — common failure paths and fixes +- [RELEASE_GUIDE.md](RELEASE_GUIDE.md) — release and review checklist +- [help.html](help.html) — local browser help page + +## OpenShot submission notes + +To align with the OpenShot contribution workflow, this helper is shaped as a **source-build bootstrap contribution**, not a replacement for the official installer. + +- branch from `develop` +- open a pull request against `develop` +- draft / WIP pull requests are acceptable when feedback is needed early +- the PR description should clearly explain the problem and the solution +- if reporting a bug instead of submitting code, include the operating system and attach relevant log files + +The cleanest framing is: **make OpenShot easier to build and launch from source on stock Windows**. + +## Contribution-shaped trim + +This pass removes a lot of the stuff that made the package feel more like a side product than an upstream-friendly helper: + +- no personal photos +- no decorative mascot assets +- no `.url` shortcuts +- no vendored jQuery source tree +- less self-promotional copy +- more direct attribution to OpenShot and its upstream repos + +## Links + +- BnR contribution repo: https://github.com/tibberous/BnRinstall_script-win +- OpenShot repo: https://github.com/OpenShot/openshot-qt +- OpenShot docs: https://www.openshot.org/static/files/user-guide/developers.html +- Author: https://www.trentontompkins.com + +For a free consultation about Windows/OpenShot build automation: **(724) 431-5207** • **trenttompkins@gmail.com** + +*Coded with ❤️ with ChatGPT.* diff --git a/installer/BnRinstall_script-win/README.txt b/installer/BnRinstall_script-win/README.txt new file mode 100644 index 0000000000..89c60f7e9e --- /dev/null +++ b/installer/BnRinstall_script-win/README.txt @@ -0,0 +1,32 @@ +OpenShot BnR 1.0.1 +=================== + +OpenShot BnR is a Windows-first bootstrap, build, and launch helper for OpenShot source builds. + +Difference from OpenShot's existing installer +--------------------------------------------- +OpenShot already has installer and packaging tooling aimed at building and shipping finished releases. +OpenShot BnR is different: it is focused on getting OpenShot building and launching from source on stock +or half-broken Windows machines. + +Contribution fit +---------------- +This helper is intended to fit OpenShot's normal contribution workflow: +- base work on the develop branch +- open a pull request against develop +- use draft / WIP status when feedback or more testing is needed +- describe the problem and solution clearly in the PR +- when reporting bugs, include the operating system and attach relevant log files + +Project links +------------- +OpenShot website: https://www.openshot.org/ +OpenShot repository: https://github.com/OpenShot/openshot-qt +OpenShot developer docs: https://www.openshot.org/static/files/user-guide/developers.html +BnR repo: https://github.com/tibberous/BnRinstall_script-win +Author: https://www.trentontompkins.com + +For a free consultation about Windows/OpenShot build automation: +(724) 431-5207 • trenttompkins@gmail.com + +Coded with ❤️ with ChatGPT. diff --git a/installer/BnRinstall_script-win/RELEASE_GUIDE.md b/installer/BnRinstall_script-win/RELEASE_GUIDE.md new file mode 100644 index 0000000000..da308ed6b5 --- /dev/null +++ b/installer/BnRinstall_script-win/RELEASE_GUIDE.md @@ -0,0 +1,49 @@ +# RELEASE_GUIDE.md + +## Purpose + +This checklist is for packaging this helper as a clean OpenShot contribution instead of a standalone side product. + +## Best upstream framing + +Describe the helper as: + +> a Windows bootstrap/build helper for OpenShot source builds on stock or partially configured Windows systems + +Do **not** describe it as a replacement for OpenShot's existing installer. The upstream installer and packaging scripts are for shipping finished builds. This helper is for restoring prerequisites, building dependencies, verifying bindings, and leaving behind readable launch helpers and logs. + +## OpenShot pull request checklist + +- branch from `develop` +- open the PR against `develop` +- explain the problem and the solution clearly +- mention how this differs from the upstream installer +- keep the work focused on OpenShot use, not personal branding +- use draft / WIP status if review or additional testing is still needed + +## Bug report / issue checklist + +If opening an issue instead of a PR: + +- search existing issues first +- include the operating system +- include clear reproduction steps +- attach the relevant log files + +On Windows, OpenShot log files are typically found in: + +- `%USERPROFILE%\.openshot_qt\openshot-qt.log` +- `%USERPROFILE%\.openshot_qt\libopenshot.log` + +## Local quality pass + +- `py -3 OpenShot_BnR_v1_0.py --help` runs cleanly +- `py -3 OpenShot_BnR_v1_0.py --about` runs cleanly +- `py -3 OpenShot_BnR_v1_0.py --version` runs cleanly +- `py -3 OpenShot_BnR_v1_0.py --debug` runs cleanly +- `help.html` opens without broken local assets +- no dead local file references remain in docs + +## Submission note + +The clearest summary is: this helper makes it easier to get OpenShot running from source on stock Windows. diff --git a/installer/BnRinstall_script-win/SECURITY.md b/installer/BnRinstall_script-win/SECURITY.md new file mode 100644 index 0000000000..d4e60d87ed --- /dev/null +++ b/installer/BnRinstall_script-win/SECURITY.md @@ -0,0 +1,5 @@ +# SECURITY.md + +This helper is meant to support the OpenShot project workflow. If you believe you have found a security issue in OpenShot itself, follow the upstream OpenShot security / reporting guidance and repository processes first. + +For issues specific to this helper package, prefer a private report before publishing sensitive details. Non-sensitive bugs and build failures can go through the normal issue or pull request flow. diff --git a/installer/BnRinstall_script-win/TROUBLESHOOTING.md b/installer/BnRinstall_script-win/TROUBLESHOOTING.md new file mode 100644 index 0000000000..e4bb96bf31 --- /dev/null +++ b/installer/BnRinstall_script-win/TROUBLESHOOTING.md @@ -0,0 +1,101 @@ +# Troubleshooting + +## Permission and elevation problems + +### Symptom +- the script cannot install tools +- `winget` recovery fails +- files under `C:\OpenShotBuild` do not update +- launchers are created but builds do not complete + +### What it usually means +You are not running with the permissions needed to install tools or write to the build location. + +### Fixes +- let the script elevate when Windows prompts you +- start PowerShell or Command Prompt **as Administrator** and rerun the script +- if company policy blocks elevation, use the **manual install** path first, then rerun the build + +## WinGet / App Installer problems + +### Symptom +- `winget` is not found +- the script says WinGet is unavailable +- Store/App Installer is stale or disabled + +### Fixes +- update or install App Installer using Microsoft’s official App Installer docs +- on Store-enabled machines, install/update **App Installer** from the Microsoft Store +- on locked-down machines, skip WinGet bootstrap and install Git + MSYS2 manually + +## Git problems + +### Symptom +- repo clone fails +- `git` not found + +### Fixes +- install Git for Windows manually +- reopen your terminal after install +- confirm `git --version` works before rerunning the script + +## MSYS2 problems + +### Symptom +- `msys2_shell.cmd` missing +- `pacman` missing +- UCRT64 package operations fail + +### Fixes +- install MSYS2 manually +- open the **UCRT64** shell, not a random MSYS shell +- run the update sequence manually before retrying + +```bash +pacman -Sy --noconfirm +pacman -Syu --noconfirm --needed +pacman -Su --noconfirm --needed +``` + +## Manual install fallback + +If the automatic path is fighting you, the shortest path is: + +1. install Git for Windows +2. install MSYS2 +3. install Python for Windows if needed +4. install the typical UCRT64 packages from `MANUAL_INSTALL.md` +5. rerun the script or build manually from there + +## Runtime launch problems + +### Symptom +- build is green but OpenShot does not open +- `ModuleNotFoundError: No module named 'openshot'` +- launcher exits quickly + +### Fixes +- use the generated launcher, not an ad-hoc shell command first +- try both: + - `cmd /c "C:\OpenShotBuild\Launch-OpenShot-Qt.cmd"` + - `py -3 "C:\OpenShotBuild\Launch-OpenShot-Qt.py"` +- compare the failing log to the successful log walkthrough in `LOG_GUIDE.md` + +## Package helper problems + +### Symptom +- portable/frozen helper fails + +### Fix +Do **not** package first. Get the normal source-build launch clean first. Packaging a broken runtime just gives you a shinier failure. + +## What to collect before asking for help + +- stage summary +- artifacts block +- `--debug` output +- `openshot-installer.log` +- the exact command you ran + + +See also: RELEASE_GUIDE.md for the public-release checklist and final QA pass. diff --git a/installer/BnRinstall_script-win/assets/css/help.css b/installer/BnRinstall_script-win/assets/css/help.css new file mode 100644 index 0000000000..2ca74ffa57 --- /dev/null +++ b/installer/BnRinstall_script-win/assets/css/help.css @@ -0,0 +1,69 @@ +:root{ + --bg:#06132a; + --bg2:#0a2049; + --panel:rgba(8,18,36,.78); + --panel-2:rgba(13,30,63,.72); + --primary:#135ee9; + --primary-2:#2ba6ff; + --gold:#ffd449; + --gold-2:#ffea94; + --ink:#07101d; + --text:#eef5ff; + --muted:#b8c7dd; + --line:rgba(255,255,255,.11); + --line-strong:rgba(255,230,120,.26); + --shadow:0 24px 60px rgba(0,0,0,.35); + --shadow-soft:0 12px 30px rgba(0,0,0,.22); + --radius:26px; + --mono:'Consolas','Cascadia Mono','Courier New',monospace; + --body:'HelveticaNeueEmbedded','Segoe UI',Arial,sans-serif; + --title:'ConstantiaEmbedded',Constantia,Georgia,serif; +} +*{box-sizing:border-box} +html{scroll-behavior:smooth} +body{margin:0;font-family:var(--body);color:var(--text);background:linear-gradient(180deg, #071224, #0a1730 40%, #0d2147 100%);min-height:100vh} +body::before{content:"";position:fixed;inset:0;background:radial-gradient(circle at 15% 20%, rgba(43,166,255,.18), transparent 22%),radial-gradient(circle at 82% 14%, rgba(255,212,73,.12), transparent 24%),linear-gradient(135deg, rgba(19,94,233,.06), transparent 45%);pointer-events:none} +a{color:#bfe0ff;text-decoration:none}a:hover{text-decoration:underline}img{max-width:100%;display:block} +code,kbd,.menu a,.pill,.badge,.kicker,.search-wrap input,.filters button,.toolbar,.logo-chip,.credit-badge{font-family:var(--mono)} +kbd{display:inline-block;padding:.15rem .42rem;border-radius:8px;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);font-size:.86em} +.skip-link{position:absolute;left:-9999px;top:8px;background:#fff;color:#000;padding:10px 14px;border-radius:10px;z-index:9999}.skip-link:focus{left:8px} +.layout{display:grid;grid-template-columns:360px minmax(0,1fr);gap:24px;width:min(1620px, calc(100% - 32px));margin:24px auto;position:relative;z-index:1} +.sidebar{position:sticky;top:24px;align-self:start;max-height:calc(100vh - 48px);overflow:auto;padding:18px;border:1px solid var(--line);border-radius:30px;background:linear-gradient(180deg, rgba(8,18,36,.92), rgba(6,14,28,.86));backdrop-filter:blur(16px);box-shadow:var(--shadow)} +.sidebar::-webkit-scrollbar{width:10px}.sidebar::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:999px} +.brand{display:block;padding:20px;border:1px solid rgba(255,255,255,.14);border-radius:24px;background:linear-gradient(135deg, rgba(12,31,65,.95), rgba(19,94,233,.72) 55%, rgba(255,212,73,.22)),rgba(8,18,36,.6);box-shadow:var(--shadow-soft);margin-bottom:16px} +.brand-mark{display:flex;align-items:center;gap:10px;margin-bottom:14px}.brand-mark img{width:32px;height:32px;object-fit:contain;filter:drop-shadow(0 6px 12px rgba(0,0,0,.25))}.brand-slash{font-size:18px;color:rgba(255,255,255,.62)} +.brand h1{margin:0 0 10px;font-family:var(--title);font-size:34px;line-height:1.02;letter-spacing:.01em}.brand p{margin:0;color:#edf4ff;line-height:1.6;font-size:15px} +.search-wrap,.sidebar-footer a,.menu a,.filters button{background:rgba(255,255,255,.04)}.search-wrap{margin:0 0 16px;padding:14px;border:1px solid var(--line);border-radius:18px} +.search-label{display:block;font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--gold-2);margin-bottom:8px}.search-wrap input{width:100%;padding:12px 14px;border-radius:14px;border:1px solid rgba(255,255,255,.10);background:rgba(4,11,24,.9);color:var(--text);outline:none}.search-wrap input:focus{border-color:var(--line-strong);box-shadow:0 0 0 3px rgba(255,212,73,.08)}.search-hint{margin-top:8px;color:var(--muted);font-size:12px} +.filters{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 14px}.filters button{border:1px solid rgba(255,255,255,.12);border-radius:999px;color:var(--text);padding:9px 12px;cursor:pointer;transition:.18s ease;font-size:12px;letter-spacing:.04em;text-transform:uppercase}.filters button:hover,.filters button.is-active{background:linear-gradient(135deg, rgba(19,94,233,.26), rgba(255,212,73,.18));border-color:rgba(255,212,73,.26)} +.menu{margin:0;padding:0;list-style:none;display:grid;gap:8px}.menu a{display:block;padding:11px 13px;border-radius:14px;color:var(--text);border:1px solid transparent;transition:transform .12s ease, background .18s ease, border-color .18s ease}.menu a:hover,.menu a.is-active{background:linear-gradient(135deg, rgba(19,94,233,.24), rgba(255,212,73,.12));border-color:rgba(255,255,255,.16);text-decoration:none;transform:translateX(2px)} +.badges{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}.badge{padding:8px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.13);background:linear-gradient(135deg, rgba(255,255,255,.08), rgba(255,255,255,.03));color:#eef7ff;font-size:12px} +.sidebar-footer{display:grid;gap:8px;margin-top:18px;padding-top:16px;border-top:1px solid var(--line)}.sidebar-footer a{padding:10px 12px;border-radius:12px;border:1px solid transparent}.sidebar-footer a:hover{text-decoration:none;border-color:rgba(255,255,255,.16);background:rgba(255,255,255,.08)} +.main{padding:0 0 28px}.utility-bar{position:sticky;top:24px;z-index:20;display:flex;justify-content:space-between;gap:16px;align-items:center;margin:0 0 18px;padding:12px 16px;border:1px solid var(--line);border-radius:22px;background:linear-gradient(180deg, rgba(7,16,31,.88), rgba(7,16,31,.76));backdrop-filter:blur(14px);box-shadow:var(--shadow-soft)} +.utility-links,.utility-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center}.pill,.utility-actions button{display:inline-flex;align-items:center;justify-content:center;gap:10px;padding:10px 14px;border-radius:999px;border:1px solid rgba(255,255,255,.11);background:rgba(255,255,255,.06);color:var(--text);cursor:pointer;font-size:12px;letter-spacing:.04em;text-transform:uppercase}.pill:hover,.utility-actions button:hover{text-decoration:none;background:rgba(255,255,255,.10)}.pill-strong{background:linear-gradient(135deg, rgba(19,94,233,.85), rgba(255,212,73,.45));border-color:rgba(255,212,73,.34)}.icon-pill img{width:18px;height:18px;object-fit:contain} +.hero,.card{border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg, rgba(8,18,36,.84), rgba(7,16,31,.72));backdrop-filter:blur(12px);box-shadow:var(--shadow)} +.hero{padding:28px;margin-bottom:24px;display:grid;grid-template-columns:minmax(0,1.2fr) minmax(280px,.8fr);gap:24px;overflow:hidden}.kicker{color:var(--gold-2);font-size:12px;letter-spacing:.18em;text-transform:uppercase;margin-bottom:12px}.hero h2,.section h3,.section h4,.docs-footer h4{font-family:var(--title)} +.hero h2{margin:0 0 14px;font-size:54px;line-height:1.02;max-width:820px}.hero p{color:#edf5ff;font-size:18px;line-height:1.72;max-width:880px}.hero-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-top:18px}.hero-grid .mini,.mini-card,.tip-card,.stat,.logo-chip{padding:16px;border-radius:22px;border:1px solid rgba(255,255,255,.1);background:linear-gradient(135deg, rgba(19,94,233,.17), rgba(255,212,73,.08))} +.hero-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:20px}.hero-button{display:inline-flex;align-items:center;justify-content:center;padding:12px 18px;border-radius:16px;border:1px solid rgba(255,255,255,.14);background:linear-gradient(135deg, rgba(19,94,233,.86), rgba(255,212,73,.44));color:#fff;text-transform:uppercase;letter-spacing:.05em;font-family:var(--mono);font-size:12px}.hero-button:hover{text-decoration:none;transform:translateY(-1px)}.hero-button.ghost{background:rgba(255,255,255,.06)} +.hero-art{position:relative;min-height:420px}.hero-bg-art{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:28px;opacity:.35;mix-blend-mode:screen;filter:saturate(1.1)}.hero-card{position:absolute;border-radius:26px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.14);box-shadow:var(--shadow-soft);overflow:hidden}.hero-portrait{right:18px;top:10px;width:170px}.hero-standing{left:0;right:35px;bottom:0;max-width:420px}.hero-portrait img,.hero-standing img{width:100%;height:auto} +.section{margin-bottom:22px}.card{padding:24px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.section h3{margin:0;font-size:34px}.section h4{margin:18px 0 10px;font-size:22px}.section p,.section li,.section summary{color:#eef5ff;line-height:1.75}.anchor-link{opacity:.35;font-size:22px;line-height:1;padding:2px 8px;border-radius:10px}.anchor-link:hover{opacity:1;text-decoration:none;background:rgba(255,255,255,.05)} +.grid,.release-grid,.stats-grid,.tips-grid,.inventory-stack{display:grid;gap:18px}.grid,.release-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.stats-grid{grid-template-columns:repeat(4,minmax(0,1fr));margin-top:18px}.tips-grid{grid-template-columns:repeat(3,minmax(0,1fr));margin-top:18px}.inventory-stack{grid-template-columns:repeat(2,minmax(0,1fr))}.stat{text-align:center}.stat strong{display:block;font-size:30px;margin-bottom:6px;font-family:var(--title)}.stat span{display:block;color:var(--muted)} +.note{border-left:4px solid var(--gold);padding:14px 16px;background:rgba(255,255,255,.05);border-radius:14px;margin-top:14px}.faq-list{display:grid;gap:12px}.faq-list details{border:1px solid var(--line);border-radius:16px;background:rgba(255,255,255,.03);padding:12px 14px}.faq-list summary{cursor:pointer;font-weight:700}.check-list{padding-left:20px}.definition-block{margin-top:18px}.definition-copy{color:var(--muted);margin:6px 0 10px}.inventory-group ul{margin:0;padding-left:18px}.inventory-group li{margin-bottom:4px} +table{width:100%;border-collapse:collapse;margin-top:8px}th,td{border-bottom:1px solid rgba(255,255,255,.08);padding:12px 10px;text-align:left;vertical-align:top}th{font-family:var(--mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:var(--gold-2)} +.about-card{overflow:hidden}.about-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(320px,.9fr);gap:22px;align-items:start}.about-copy p{margin-top:0}.about-credit{margin:18px 0 16px}.credit-badge{display:inline-flex;align-items:center;gap:10px;padding:12px 16px;border-radius:999px;border:1px solid rgba(255,255,255,.13);background:linear-gradient(135deg, rgba(19,94,233,.24), rgba(255,212,73,.18))}.about-links{display:flex;gap:12px;flex-wrap:wrap}.about-pill{display:inline-flex;align-items:center;gap:12px;padding:12px 16px;border-radius:18px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05)}.about-pill img{width:24px;height:24px;object-fit:contain}.about-pill:hover{text-decoration:none;background:rgba(255,255,255,.08)} +.about-media{display:grid;gap:16px}.repo-chip{color:var(--text)}.repo-chip:hover{text-decoration:none;background:linear-gradient(135deg, rgba(19,94,233,.22), rgba(255,212,73,.14))} +.line-meta{color:var(--muted);font-size:13px;font-weight:400}.code-toolbar{position:relative;margin:18px 0 22px}.code-toolbar .toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border:1px solid var(--line);border-bottom:none;border-radius:18px 18px 0 0;background:linear-gradient(180deg, rgba(11,23,46,.96), rgba(15,39,86,.92))}.code-toolbar .toolbar .toolbar-left{display:flex;align-items:center;gap:10px}.code-toolbar .toolbar .window-dots{display:flex;gap:8px}.code-toolbar .toolbar .window-dots span{width:11px;height:11px;border-radius:50%;background:linear-gradient(180deg,#ff7f7f,#c44f4f);box-shadow:0 0 0 1px rgba(0,0,0,.2) inset}.code-toolbar .toolbar .window-dots span:nth-child(2){background:linear-gradient(180deg,#ffe38b,#b9932d)}.code-toolbar .toolbar .window-dots span:nth-child(3){background:linear-gradient(180deg,#8bf3a4,#2d9d50)}.code-toolbar .toolbar .label{font-size:13px;letter-spacing:.05em;text-transform:uppercase;color:#f4f8ff}.code-toolbar .toolbar .toolbar-right{display:flex;gap:8px}.code-toolbar .toolbar button{appearance:none;border:1px solid rgba(255,255,255,.15);border-radius:12px;background:rgba(255,255,255,.08);color:#fff;padding:7px 12px;cursor:pointer;font-size:13px}.code-toolbar .toolbar button:hover{background:rgba(255,255,255,.13)}.code-toolbar pre{margin:0;border-radius:0 0 18px 18px;border-top:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.03), 0 12px 30px rgba(0,0,0,.22)}.code-toolbar.shell-window .toolbar{background:linear-gradient(180deg,#eef4fb,#d8e7f8 0%, #adc4de 52%, #87a6c6 100%)}.code-toolbar.shell-window .toolbar .label,.code-toolbar.shell-window .toolbar button{color:#102544}.code-toolbar.shell-window .toolbar button{background:linear-gradient(180deg,#ffffff,#d9e7f6);border-color:rgba(16,37,68,.18)}.code-toolbar.shell-window .toolbar button:hover{background:linear-gradient(180deg,#ffffff,#c7dbef)} +.footer.docs-footer{margin-top:22px;padding:24px;border-top:1px solid rgba(255,255,255,.1);display:grid;gap:10px;border-radius:26px;background:linear-gradient(180deg, rgba(8,18,36,.84), rgba(7,16,31,.72))}.docs-footer h4{margin:0 0 8px;font-size:26px}.footer-brand p{margin:0;color:var(--muted)}.footer-links{display:flex;gap:16px;flex-wrap:wrap}.footer-credit{color:var(--gold-2);font-family:var(--mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase} +.back-to-top{position:fixed;right:22px;bottom:22px;display:none;width:52px;height:52px;border:0;border-radius:50%;background:linear-gradient(135deg, rgba(19,94,233,.94), rgba(255,212,73,.54));color:#fff;cursor:pointer;box-shadow:var(--shadow)}.back-to-top.is-visible{display:block} +@media (max-width:1400px){.hero{grid-template-columns:1fr}.hero-art{min-height:340px}} +@media (max-width:1250px){.stats-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.inventory-stack{grid-template-columns:1fr}.about-layout{grid-template-columns:1fr}} +@media (max-width:1100px){.layout{grid-template-columns:1fr}.sidebar{position:relative;top:0;max-height:none}.utility-bar{top:0;position:relative;flex-direction:column;align-items:stretch}.hero-grid,.grid,.release-grid,.tips-grid,.logo-rail{grid-template-columns:1fr}.hero h2{font-size:38px}} +@media (max-width:780px){.main{padding:0 0 20px}.card,.hero{padding:18px}.stats-grid{grid-template-columns:1fr 1fr}.hero h2,.section h3{font-size:30px}.portrait-stack{grid-template-columns:1fr}.utility-links,.utility-actions{justify-content:flex-start}} +@media (max-width:560px){.layout{width:min(100% - 18px, 1620px);margin:10px auto}.stats-grid{grid-template-columns:1fr}.pill,.utility-actions button{width:100%;justify-content:center}} + +.docs-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.doc-card h4{margin:0 0 8px}.doc-card p{margin:0}.docs-links{margin-top:18px} +@media (max-width:1100px){.docs-grid{grid-template-columns:1fr}} + +.mark-dot{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:999px;background:linear-gradient(135deg, rgba(19,94,233,.86), rgba(255,212,73,.45));font-family:var(--mono);font-size:14px;color:#fff;box-shadow:var(--shadow-soft)} +.hero-note,.about-panel{padding:18px;border-radius:22px;border:1px solid rgba(255,255,255,.10);background:linear-gradient(135deg, rgba(19,94,233,.14), rgba(255,212,73,.08))} +.hero-art{position:relative;min-height:auto;display:grid;gap:14px;align-content:start}.hero-card{position:relative}.hero-bg-art{display:none}.about-layout{grid-template-columns:1fr}.about-panel p:last-child{margin-bottom:0}.small-list{margin:12px 0 0;padding-left:18px} diff --git a/installer/BnRinstall_script-win/assets/css/prism-tomorrow.min.css b/installer/BnRinstall_script-win/assets/css/prism-tomorrow.min.css new file mode 100644 index 0000000000..f7ebd00675 --- /dev/null +++ b/installer/BnRinstall_script-win/assets/css/prism-tomorrow.min.css @@ -0,0 +1,27 @@ +/* Prism placeholder theme hook. + This file exists so the repo has a local style entry point. + help.html also loads Prism from CDN for actual highlighting when online. */ +pre[class*="language-"], code[class*="language-"] { + color: #e6eef2; + background: #0b0f12; + text-shadow: none; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; + font-size: 14px; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.55; + tab-size: 4; + hyphens: none; +} +.token.comment,.token.prolog,.token.doctype,.token.cdata{color:#8a99a6} +.token.punctuation{color:#cbd6dc} +.token.property,.token.tag,.token.constant,.token.symbol,.token.deleted{color:#ff7c7c} +.token.boolean,.token.number{color:#f1b972} +.token.selector,.token.attr-name,.token.string,.token.char,.token.builtin,.token.inserted{color:#8bd49c} +.token.operator,.token.entity,.token.url,.language-css .token.string,.style .token.string{color:#7ac7ff} +.token.atrule,.token.attr-value,.token.keyword{color:#72a8ff} +.token.function,.token.class-name{color:#7fe7d8} +.token.regex,.token.important,.token.variable{color:#ffd479} diff --git a/installer/BnRinstall_script-win/assets/css/prism.css b/installer/BnRinstall_script-win/assets/css/prism.css new file mode 100644 index 0000000000..cf81f23902 --- /dev/null +++ b/installer/BnRinstall_script-win/assets/css/prism.css @@ -0,0 +1,71 @@ +/* PrismJS 1.30.0 +https://prismjs.com/download#themes=prism-dark&languages=markup+css+clike+javascript+bash+plsql+python+sql */ +code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-shadow:0 -.1em .2em #000;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red} + + +/* OpenShot BnR overrides for line numbers + toolbar + Windows 7 PowerShell shell blocks */ +pre[class*=language-]{ + position:relative; + margin:0; + padding:1.15em 1.2em 1.15em 4.75em; + border-radius:0 0 18px 18px; + border-top:none; + box-shadow:inset 0 1px 0 rgba(255,255,255,.04); +} +pre.line-numbers{padding-left:4.75em} +pre.line-numbers > code{position:relative;z-index:2;white-space:pre;word-break:normal;word-wrap:normal} +.line-numbers-rows{ + position:absolute;pointer-events:none;top:0;left:0;width:3.8em; + border-right:1px solid rgba(255,255,255,.12);padding:1.15em 0 1.15em; + background:linear-gradient(180deg, rgba(0,0,0,.18), rgba(255,255,255,.02)); + user-select:none; +} +.line-numbers-rows > span{display:block;counter-increment:linenumber} +.line-numbers-rows > span:before{ + content:counter(linenumber); + display:block;padding-right:.9em;text-align:right; + color:rgba(255,255,255,.42); +} +pre.language-bash, +pre.language-shell, +pre.language-powershell{ + background:linear-gradient(180deg,#063a82 0%, #032b65 4%, #012456 100%) !important; + border-color:#587aa1 !important; +} +pre.language-bash .line-numbers-rows, +pre.language-shell .line-numbers-rows, +pre.language-powershell .line-numbers-rows{ + background:linear-gradient(180deg,#052d66,#011d45); + border-right-color:rgba(255,255,255,.18); +} +pre.language-bash .line-numbers-rows > span:before, +pre.language-shell .line-numbers-rows > span:before, +pre.language-powershell .line-numbers-rows > span:before{ + color:rgba(255,255,255,.58); +} +pre.language-python{ + background:linear-gradient(180deg,#171d22,#101418 100%) !important; + border-color:#26495a !important; +} +pre.language-python .token.comment{color:#9db0bd} +pre.language-python .token.keyword{color:#7fd8ff} +pre.language-python .token.builtin{color:#c2ff8a} +pre.language-python .token.string{color:#ffd289} +pre.language-python .token.number{color:#ffb2c3} +pre.language-bash .token.comment{color:#d2dde8} +pre.language-bash .token.keyword{color:#9ce8ff} +pre.language-bash .token.string{color:#fff3b0} +pre.language-bash .token.number{color:#ffe0b0} +pre.language-bash .token.operator{color:#ffffff} +pre.language-bash .token.builtin{color:#c8f5ff} +code[class*=language-]::selection, pre[class*=language-]::selection, code[class*=language-] *::selection, pre[class*=language-] *::selection{ + background:rgba(255,255,255,.14); +} +@media print{ + body{background:#fff!important;color:#111!important} + .sidebar,.filters,.footer,.toolbar button{display:none!important} + .layout{display:block} + .main{padding:0} + .card,.hero,pre[class*=language-]{box-shadow:none!important} + pre[class*=language-]{border:1px solid #999!important} +} diff --git a/installer/BnRinstall_script-win/assets/js/prism.min.js b/installer/BnRinstall_script-win/assets/js/prism.min.js new file mode 100644 index 0000000000..ffddcd9b62 --- /dev/null +++ b/installer/BnRinstall_script-win/assets/js/prism.min.js @@ -0,0 +1,117 @@ + +(function () { + window.Prism = window.Prism || {}; + window.Prism.manual = true; + + function qsa(sel, ctx) { + return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); + } + + function getLineCount(text) { + if (!text) return 1; + var normalized = text.replace(/\n$/, ''); + var lines = normalized.split('\n').length; + return Math.max(lines, 1); + } + + function addLineNumbers(pre) { + if (!pre.classList.contains('line-numbers')) return; + if (pre.querySelector('.line-numbers-rows')) return; + var code = pre.querySelector('code'); + if (!code) return; + var rows = document.createElement('span'); + rows.className = 'line-numbers-rows'; + rows.setAttribute('aria-hidden', 'true'); + var count = getLineCount(code.textContent || ''); + var frag = document.createDocumentFragment(); + for (var i = 0; i < count; i++) { + frag.appendChild(document.createElement('span')); + } + rows.appendChild(frag); + pre.appendChild(rows); + } + + function makeButton(text, handler) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.textContent = text; + btn.addEventListener('click', handler); + return btn; + } + + function wrapCodeBlock(pre) { + if (pre.parentNode && pre.parentNode.classList && pre.parentNode.classList.contains('code-toolbar')) { + addLineNumbers(pre); + return; + } + var lang = (pre.getAttribute('class') || '').match(/language-([a-z0-9_-]+)/i); + var langName = pre.getAttribute('data-lang-label') || (lang ? lang[1].toUpperCase() : 'CODE'); + + var wrapper = document.createElement('div'); + wrapper.className = 'code-toolbar'; + if ((lang && /bash|shell|powershell/i.test(lang[1])) || /PowerShell|CMD/i.test(langName)) { + wrapper.className += ' shell-window'; + } + + var toolbar = document.createElement('div'); + toolbar.className = 'toolbar'; + + var left = document.createElement('div'); + left.className = 'toolbar-left'; + var dots = document.createElement('div'); + dots.className = 'window-dots'; + dots.innerHTML = ''; + var label = document.createElement('div'); + label.className = 'label'; + label.textContent = langName; + left.appendChild(dots); + left.appendChild(label); + + var right = document.createElement('div'); + right.className = 'toolbar-right'; + var code = pre.querySelector('code'); + right.appendChild(makeButton('Copy', function () { + var text = code ? code.textContent : pre.textContent; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + this.textContent = 'Copied'; + var self = this; setTimeout(function(){ self.textContent = 'Copy'; }, 1200); + }.bind(this)); + } else { + var ta = document.createElement('textarea'); + ta.value = text; + document.body.appendChild(ta); + ta.select(); + try { document.execCommand('copy'); } catch (e) {} + document.body.removeChild(ta); + } + })); + right.appendChild(makeButton('Print', function () { + var win = window.open('', '_blank', 'width=900,height=700'); + if (!win) return; + var text = code ? code.textContent : pre.textContent; + var escaped = text + .replace(/&/g, '&') + .replace(//g, '>'); + var labelText = langName.replace(//g, '>'); + win.document.write('' + labelText + '

' + labelText + '

' + escaped + '
'); + win.document.close(); + win.focus(); + setTimeout(function(){ win.print(); }, 150); + })); + + toolbar.appendChild(left); + toolbar.appendChild(right); + + pre.parentNode.insertBefore(wrapper, pre); + wrapper.appendChild(toolbar); + wrapper.appendChild(pre); + + addLineNumbers(pre); + } + + document.addEventListener('DOMContentLoaded', function () { + qsa('pre[class*="language-"]').forEach(wrapCodeBlock); + }); +}()); diff --git a/installer/BnRinstall_script-win/assets/js/smooth-scroll.js b/installer/BnRinstall_script-win/assets/js/smooth-scroll.js new file mode 100644 index 0000000000..188f1a082c --- /dev/null +++ b/installer/BnRinstall_script-win/assets/js/smooth-scroll.js @@ -0,0 +1,82 @@ +(function () { + function qs(sel, ctx){ return (ctx || document).querySelector(sel); } + function qsa(sel, ctx){ return Array.prototype.slice.call((ctx || document).querySelectorAll(sel)); } + function smoothTo(hash) { + if (!hash) return; + var target = qs(hash); + if (!target) return; + var top = target.getBoundingClientRect().top + window.pageYOffset - 88; + window.scrollTo({ top: top, behavior: 'smooth' }); + } + function setActiveFilter(button) { + qsa('[data-filter]').forEach(function (btn) { btn.classList.toggle('is-active', btn === button); }); + } + function applyBucketFilter(tag) { + qsa('.menu a').forEach(function (a) { + var bucket = a.getAttribute('data-bucket') || ''; + var visible = (tag === 'all' || bucket.indexOf(tag) !== -1); + a.parentElement.style.display = visible ? '' : 'none'; + }); + } + function applySearchFilter(query) { + var q = (query || '').trim().toLowerCase(); + qsa('.menu a').forEach(function (a) { + var hay = (a.textContent + ' ' + (a.getAttribute('data-search') || '')).toLowerCase(); + var hit = !q || hay.indexOf(q) !== -1; + if (a.parentElement.style.display !== 'none') a.parentElement.style.display = hit ? '' : 'none'; + }); + } + function refreshActiveLink() { + var sections = qsa('main .section[id]'); + var current = null; + sections.forEach(function (section) { + var rect = section.getBoundingClientRect(); + if (rect.top <= 140 && rect.bottom >= 140) current = section.id; + }); + qsa('.menu a').forEach(function (a) { a.classList.toggle('is-active', current && a.getAttribute('href') === '#' + current); }); + } + document.addEventListener('click', function (e) { + var link = e.target.closest('a[href^="#"]'); + if (link) { + var href = link.getAttribute('href'); + if (href && href !== '#') { + e.preventDefault(); + history.pushState(null, '', href); + smoothTo(href); + } + } + if (e.target && e.target.id === 'print-help') window.print(); + if (e.target && e.target.id === 'back-to-top') window.scrollTo({ top: 0, behavior: 'smooth' }); + }); + window.addEventListener('load', function () { + if (window.location.hash) smoothTo(window.location.hash); + refreshActiveLink(); + }); + window.addEventListener('scroll', function () { + refreshActiveLink(); + var btn = qs('#back-to-top'); + if (btn) btn.classList.toggle('is-visible', window.pageYOffset > 500); + }); + qsa('[data-filter]').forEach(function (button) { + button.addEventListener('click', function () { + var tag = button.getAttribute('data-filter'); + setActiveFilter(button); + applyBucketFilter(tag); + applySearchFilter(qs('#menu-search') ? qs('#menu-search').value : ''); + }); + }); + var search = qs('#menu-search'); + if (search) { + search.addEventListener('input', function () { + var active = qs('[data-filter].is-active'); + applyBucketFilter(active ? active.getAttribute('data-filter') : 'all'); + applySearchFilter(search.value); + }); + } + document.addEventListener('keydown', function (e) { + if (e.key === '/' && !/input|textarea/i.test((document.activeElement || {}).tagName || '')) { + e.preventDefault(); + if (search) search.focus(); + } + }); +}()); \ No newline at end of file diff --git a/installer/BnRinstall_script-win/help.html b/installer/BnRinstall_script-win/help.html new file mode 100644 index 0000000000..89fc9325ab --- /dev/null +++ b/installer/BnRinstall_script-win/help.html @@ -0,0 +1,689 @@ + + + + + + +OpenShot BnR 1.0.1 Help + + + + + + + + + + +
+ +
+
+ +
+ +Jump to quick start +
+
+
+
+
OpenShot BnR 1.0.1 • Contribution-Oriented Windows Bootstrap Helper
+

Support the OpenShot workflow without pretending to be the whole project.

+

This helper targets the ugly part of a Windows source build: prerequisites, toolchain recovery, dependency resolution, native build order, binding checks, launcher repair, and logs you can actually read afterward.

+
+
Upstream-aware
Built to sit beside the OpenShot project, not overwrite its identity.
+
Readable evidence
Logs, stage records, and launch helpers make failures inspectable.
+
Fresh-machine utility
Best when a Windows box is missing pieces and you need an honest bootstrap path.
+
+ +
+
Where it fits

OpenShot already has official installer and packaging infrastructure. BnR is more useful as a bootstrap, verification, and source-build helper.

Submission tone

This trimmed pass keeps the BnR name and author credit, but cuts the side-product vibe so it reads more like a team-minded contribution.

+
+
+
+

About

#
+

Author: Trenton Tompkins • trentontompkins.com

OpenShot BnR is an independent Windows bootstrap, build, and launch helper written around the OpenShot workflow. The intent is to be useful to the OpenShot project and its users, not to pretend this helper replaces the upstream project or its packaging decisions.

Difference from the official installer: OpenShot's upstream installer/build scripts package finished builds. BnR focuses on the earlier problem: getting OpenShot building and launching on a stock or half-broken Windows machine.

OpenShot upstream: openshot.orgopenshot-qtdeveloper docs

Coded with ❤️ with ChatGPT

For a free consultation about Windows/OpenShot build automation: (724) 431-5207 • trenttompkins@gmail.com

  • keeps BnR naming and author credit
  • adds clearer OpenShot attribution and upstream context
  • drops photos, mascot art, shortcut files, and vendored baggage
+
+

OpenShot contribution notes

#

To fit OpenShot's normal workflow, this helper should be treated as a source-build bootstrap contribution, not a replacement for the official installer.

  • base the work on develop
  • open the pull request against develop
  • use draft / WIP status when the work still needs review or testing
  • make the PR description explain the problem and the solution clearly
  • for issue reports, include OS details and attach the relevant log files
Best framing: this helps get OpenShot building and launching from source on stock Windows; it does not replace the upstream packaging installer.
+

Overview

#

OpenShot BnR 1.0.1 is a Windows-first bootstrap, build, and run helper for OpenShot source builds. It checks the machine, resolves dependencies against live MSYS2 package data, builds the native layers in order, verifies the Python bindings, repairs the launcher path, and leaves behind useful helper files instead of vague encouragement.

87Top-level functions
0Top-level classes
6Information switches
3Primary outputs
LayerWhat it doesWhy it matters
PrerequisitesChecks Windows support, elevation, WinGet, Git, and MSYS2If the foundation is wrong, the rest is theater.
DependenciesResolves packages against the live UCRT64 repoOld hardcoded package lists rot. Live checks bend less.
BuildBuilds libopenshot-audio and libopenshotThe editor cannot run without them.
BootstrapRepairs the runtime launch path and verifies import behaviorA build that cannot launch still costs you time.
Distro prepCreates portable and frozen helper filesManual launch tricks do not scale to real users.
+

How this differs from OpenShot's installer

#

OpenShot already has official installer and packaging tooling in its upstream installer/ directory. That tooling is for packaging, launching, and shipping finished builds.

ToolingMain job
OpenShot upstream installer/build scriptsPackage and release finished OpenShot builds
OpenShot BnRRestore prerequisites, clone or update repos, build native dependencies, verify bindings, and generate launch helpers on stock Windows
That is why BnR fits better as a Windows bootstrap and diagnostics helper than as a replacement installer.
+

Why this exists

#

The expensive part of a broken build is not the one command that failed. The expensive part is the loop: wrong shell, stale package, wrong Python, wrong path, wrong launcher, and then another hour gone. OpenShot BnR compresses that loop.

That is leverage. The script does the boring expensive part so your brain does not have to keep paying for the same mistakes twice.

OpenShot BnR is Open Source. You can inspect it, extend it, automate it, and fit it to the way you actually work instead of bending around a closed tool.
+

Quick start

#

Run the script. Let it elevate. Let it build. Launch OpenShot from the generated launcher. That is the shortest path from zero to a real launch.

py -3 OpenShot_BnR_v1_0.py
+
+# After a successful run:
+cmd /c "C:\OpenShotBuild\Launch-OpenShot-Qt.cmd"
+py -3 "C:\OpenShotBuild\Launch-OpenShot-Qt.py"
First goalGet all stage lines green.
Second goalSee OpenShot initialize the UI and end the session cleanly.
Third goalUse the distro helpers only after the source launch is clean.
+

Run examples

#

These are the commands worth memorizing. The build path has one obvious entry point. The docs and diagnostics are split into commands that match what users actually ask for.

py -3 OpenShot_BnR_v1_0.py
+py -3 OpenShot_BnR_v1_0.py --usage
+py -3 OpenShot_BnR_v1_0.py --help
+py -3 OpenShot_BnR_v1_0.py man
+py -3 OpenShot_BnR_v1_0.py --about
+py -3 OpenShot_BnR_v1_0.py --version
+py -3 OpenShot_BnR_v1_0.py --docs
+py -3 OpenShot_BnR_v1_0.py --readme
+py -3 OpenShot_BnR_v1_0.py --install
+py -3 OpenShot_BnR_v1_0.py --manual-install
+py -3 OpenShot_BnR_v1_0.py --log-guide
+py -3 OpenShot_BnR_v1_0.py --troubleshoot
+py -3 OpenShot_BnR_v1_0.py --release-guide
+py -3 OpenShot_BnR_v1_0.py --changelog
+py -3 OpenShot_BnR_v1_0.py --contributing
+py -3 OpenShot_BnR_v1_0.py --security
+py -3 OpenShot_BnR_v1_0.py --code-of-conduct
+py -3 OpenShot_BnR_v1_0.py --license
+py -3 OpenShot_BnR_v1_0.py --debug
+py -3 OpenShot_BnR_v1_0.py /usage /U /help /v /docs /debug
+cmd /c "C:\OpenShotBuild\Launch-OpenShot-Qt.cmd"
+py -3 "C:\OpenShotBuild\Launch-OpenShot-Qt.py"
First goalGet all stage lines green.
Second goalSee OpenShot initialize the UI and end the session cleanly.
Third goalUse the distro helpers only after the source launch is clean.
+

Manual install

#

Sometimes the right move is manual. Locked-down Windows image. Store disabled. Elevation blocked. Or you just want to know exactly what the script is using.

ComponentWhat it doesOfficial URLAttribution
WinGet / App InstallerBootstrap and recovery path for missing tools on supported Windows systems.Microsoft Learn: WinGetMicrosoft Learn
App Installer maintenanceUseful when winget is missing, stale, or not registered right.Install / update App InstallerMicrosoft Learn
Git for WindowsClones and updates the OpenShot repositories.Git for WindowsGit for Windows project
MSYS2 installerProvides UCRT64, pacman, Bash, and the Windows-native build environment.MSYS2 installerMSYS2 project
Python for WindowsRuns the script on Windows when you do not already have py.Python downloads for WindowsPython Software Foundation
OpenShot developer docsUpstream architecture and developer notes.OpenShot developers guideOpenShot project

See MANUAL_INSTALL.md for the full manual package guide with more explanation and URLs.

+

Package guide

#

The script scores live UCRT64 packages, so exact picks can shift a little. These are the typical package families behind the capabilities the script is trying to satisfy.

CapabilityTypical package pageWhat it does
GCC / G++mingw-w64-ucrt-x86_64-gccC and C++ compiler toolchain for the native OpenShot layers.
GNU Makemingw-w64-ucrt-x86_64-makeRuns Makefile-based builds.
CMakemingw-w64-ucrt-x86_64-cmakeConfigures the C and C++ build.
Ninja (optional)mingw-w64-ucrt-x86_64-ninjaFast alternative build runner.
FFmpegmingw-w64-ucrt-x86_64-ffmpegMedia libraries and tools for audio and video processing.
SWIGmingw-w64-ucrt-x86_64-swigGenerates the binding glue used by Python.
Doxygen (optional)mingw-w64-ucrt-x86_64-doxygenBuilds API documentation.
ZeroMQmingw-w64-ucrt-x86_64-zeromqMessaging layer used by parts of the stack.
Pythonmingw-w64-ucrt-x86_64-pythonPython runtime inside UCRT64.
pipmingw-w64-ucrt-x86_64-python-pipPython package installer used by the helper venv.
PyQt5mingw-w64-ucrt-x86_64-python-pyqt5Qt5 bindings that power the OpenShot UI.
pyzmqmingw-w64-ucrt-x86_64-python-pyzmqPython bindings for ZeroMQ.
cx_Freeze (optional)mingw-w64-ucrt-x86_64-python-cx-freezeUsed to create standalone packaged builds.
Rust (optional)mingw-w64-ucrt-x86_64-rustOptional systems toolchain some workflows may want.
Catch (optional)mingw-w64-ucrt-x86_64-catchC++ unit test framework.
Manual command examples and the full package table also live in MANUAL_INSTALL.md.
+

Command switches

#

The command surface is finalized around two ideas: one real build command with no switches, and a full set of information commands that print docs or diagnostics without touching the build. That means users can learn the project from the script itself instead of guessing from repo breadcrumbs.

GroupCanonicalAliasesWhat it does
Usage--usage-usage, /usage, usage, -u, /u, /UQuick command reminder.
Help--help-help, /help, help, -h, /h, /?, ?Detailed command guide.
Man--manman, -man, /man, manual, --manual, -manual, /manualManual-style reference.
About--about-about, /about, aboutProject overview and value.
Version--version-version, /version, version, --ver, -ver, /ver, ver, -v, /vVersion and project links.
Docs--docs-docs, /docs, docsPrint the local docs suite map.
README--readme-readme, /readme, readmePrint README.md.
Install--install-install, /install, installPrint INSTALL.md.
Manual install--manual-install-manual-install, /manual-install, manual-install, manualinstallPrint MANUAL_INSTALL.md.
Log guide--log-guide-log-guide, /log-guide, log-guide, --logs, -logs, /logs, logsPrint LOG_GUIDE.md.
Troubleshooting--troubleshoot-troubleshoot, /troubleshoot, troubleshoot, --troubleshooting, -troubleshooting, /troubleshooting, troubleshootingPrint TROUBLESHOOTING.md.
Release guide--release-guide-release-guide, /release-guide, release-guidePrint RELEASE_GUIDE.md.
Changelog--changelog-changelog, /changelog, changelogPrint CHANGELOG.md.
Contributing--contributing-contributing, /contributing, contributingPrint CONTRIBUTING.md.
Security--security-security, /security, securityPrint SECURITY.md.
Code of conduct--code-of-conduct-code-of-conduct, /code-of-conduct, code-of-conductPrint CODE_OF_CONDUCT.md.
License--license-license, /license, licensePrint LICENSE.txt.
Debug--debug-debug, /debug, debugPrint local machine and tool diagnostics.
py -3 OpenShot_BnR_v1_0.py --usage
+py -3 OpenShot_BnR_v1_0.py --help
+py -3 OpenShot_BnR_v1_0.py man
+py -3 OpenShot_BnR_v1_0.py --docs
+py -3 OpenShot_BnR_v1_0.py --manual-install
+py -3 OpenShot_BnR_v1_0.py --troubleshoot
+py -3 OpenShot_BnR_v1_0.py /usage /U /help /v /docs /debug
+

Stage guide

#

Prerequisites

Checks Windows support, elevation, WinGet, Git, and MSYS2. If this layer is wrong, the rest is just expensive pretending.

Dependencies

Refreshes MSYS2, reads the live UCRT64 package list, scores candidates, and installs what the build chain really needs.

Repositories

Clones or updates libopenshot-audio, libopenshot, and openshot-qt.

Build libopenshot-audio

Compiles and installs the audio layer.

Build libopenshot

Compiles native bindings, verifies import behavior, and distinguishes installed mode from source-build mode.

Prepare openshot-qt

Patches the launcher path, creates runtime helpers, and smoke-tests the launch route that real users will hit.

+

Logging

#

Logging is where the script proves it is working instead of just sounding confident. The log tells you what stage started, what command ran, what artifacts were found, and where the launch path broke.

Loaded modules from: C:\OpenShotBuild\openshot-qt\src
+INFO app: OpenShot (version 3.5.1)
+INFO app: libopenshot version: 0.7.0
+INFO ui_util: Initializing UI for MainWindow
+INFO preview_thread: QThread Start Method Invoked
+INFO app: OpenShot's session ended
If the UI initializes and the session ends cleanly, that is a real win. The machine stopped arguing and started doing work.
+

Log walkthrough

#

A good log saves you hours because it tells you which layer is alive. Here is the plain-English meaning of the successful runtime flow.

Log line or groupWhat it means
Loaded modules from ... openshot-qt\srcThe launcher found the OpenShot Python app tree and put it on the module path.
OpenShot (version 3.5.1)The Python application started for real.
libopenshot version: 0.7.0The Python app successfully talked to the native libopenshot binding.
project_data / profile linesDefault project data loaded.
logger_libopenshotThe Python side connected to the native logging/debug layer.
Initializing UI for MainWindowThe desktop UI is being assembled. This is one of the most valuable startup lines in the whole run.
thumbnail server / preview_threadBackground support services started.
Cannot add existing listenerNoisy warning, not a launch blocker.
ComfyUI check failed ... 127.0.0.1:8188A local AI service was not running. That does not stop OpenShot from launching.
Shutdown blockThe app exited cleanly and tore down its background pieces on purpose instead of just disappearing.

Need the longer version? Open LOG_GUIDE.md. It walks through the runtime flow and the installer stage summary step by step.

+

Generated files

#

Launchers

  • Launch-OpenShot-Qt.cmd — main Windows launcher
  • Launch-OpenShot-Qt.py — Python runtime bootstrap launcher

Distribution helpers

  • Build-OpenShot-Frozen.cmd — frozen build helper
  • Build-OpenShot-Portable.cmd — portable build helper
  • Build-OpenShot-Portable.py — portable assembly helper

Logs and state

  • openshot-installer.log — main log
  • openshot-installer-state.json — stage and artifact state
  • openshot-installer-relay.log — relay output while elevated

Distro commands

cmd /c "C:\OpenShotBuild\Build-OpenShot-Portable.cmd"
+cmd /c "C:\OpenShotBuild\Build-OpenShot-Frozen.cmd"
+

Docs suite

#

The local docs set is meant to make the helper reviewable without digging through the whole Python file first.

Core docs

  • README.md — project fit, installer difference, and quick start
  • INSTALL.md — normal usage path and expected outputs
  • MANUAL_INSTALL.md — prerequisite packages, upstream URLs, and manual setup notes

Support docs

  • LOG_GUIDE.md — how to read the logs and what each file means
  • TROUBLESHOOTING.md — common failure patterns and first fixes
  • README.txt — plain text summary for quick viewing in simple shells or editors

Project docs

  • CONTRIBUTING.md — contribution fit and PR expectations
  • CHANGELOG.md — version notes for the trimmed contribution pass
  • SECURITY.md and CODE_OF_CONDUCT.md — basic policy and reporting notes
+

Project files

#

The package is intentionally small. The script lives at the center, the docs explain how to use and review it, and the browser guide adds a friendlier front end.

Primary entry point

  • OpenShot_BnR_v1_0.py — bootstrap, build, verify, and launch workflow

Browser help assets

  • help.html
  • assets/css/help.css
  • assets/css/prism.css
  • assets/js/prism.min.js
  • assets/js/smooth-scroll.js

Text docs

  • README / install / troubleshooting docs sit beside the script so every information command has a real file behind it.
+

Contributing

#

This helper is shaped to follow the OpenShot contribution flow, not bypass it.

  • branch from develop
  • keep the change focused and explain the problem clearly
  • open the PR against develop
  • use Draft or WIP status when feedback or more testing is still needed
  • when reporting bugs, include OS details and attach relevant logs

Upstream guide: OpenShot CONTRIBUTING.md

+

Changelog

#

Version 1.0.1 is the trimmed contribution pass.

  • clarified how this helper differs from the official OpenShot installer
  • kept the BnR identity while toning down the standalone-product feel
  • removed decorative image assets from the help package
  • kept the docs and command surface aligned so the information commands point to real files
+

Security

#

If the issue is in OpenShot itself, use the upstream OpenShot security and reporting process first. If the issue is specific to this helper package, prefer a private report before publishing sensitive details. Ordinary build failures and non-sensitive bugs can go through the normal issue or pull-request path.

+

FAQ

#
Do I need to preinstall Git, WinGet, or MSYS2?

No. The script is built to detect and restore what it needs first. You still need permission to elevate and an internet connection.

What is the real milestone I should care about?

A clean launch. Green build stages matter because they buy you that launch, but the real finish line is OpenShot opening and logging a clean session.

Should I use the portable or frozen helper immediately?

Only after the source-build launch path is clean. Packaging broken runtime assumptions just gives you a prettier failure.

Why does the guide keep talking about logs?

Because logs reduce wasted time. A confident sentence is not evidence. A clean stage summary and readable log are evidence.

+

Troubleshooting

#

When a build fails, you do not need more vibes. You need the shortest honest answer to: what broke, where, and why.

  • Red on prerequisites — the machine is not ready yet. Fix the missing tool first.
  • Red on dependencies — MSYS2 package resolution or install failed. Read the package candidate log and retry from there.
  • Green build, bad launch — the build succeeded but runtime bootstrap is wrong. Check launcher generation, Python path, and DLL path behavior.
  • Portable or frozen helper fails — fix the clean source-build launch first, then package.

Permissions

If installs or file writes fail, rerun the script in an elevated terminal or accept the Windows UAC prompt. Locked-down machines often need the manual route first.

Manual install fallback

Use the official package pages in MANUAL_INSTALL.md when WinGet or the Store is blocked, stale, or disabled.

Need the deep dive?

TROUBLESHOOTING.md covers permissions, manual install, missing tools, bad launches, and packaging fallback in more detail.

Fastest truth path: read the stage summary, read the artifacts block, run --debug, then open openshot-installer.log.
+

Distribution polish

#

The build script already gets you far. Distribution polish is the part where you stop thinking like a developer who can improvise around the environment and start thinking like a user who expects the tool to simply open.

Good distro UX means

  • clear launch entry points
  • clean documentation beside the script
  • portable or frozen outputs that carry their own runtime
  • error messages that tell the truth

Bad distro UX means

  • mystery dependencies
  • paths that only work on the build box
  • launchers that rely on tribal knowledge
  • help files that feel like an afterthought
+

Release checklist

#

Script QA

  • run --usage, --about, --version, --readme, --license, and --debug
  • confirm the version says 1.0.1 everywhere visible
  • confirm the comment block and repo docs agree

Launch QA

  • launch from Launch-OpenShot-Qt.cmd
  • launch from Launch-OpenShot-Qt.py
  • confirm OpenShot reaches the UI and logs a clean session

Docs QA

  • open help.html in a browser
  • check search, nav, copy, and print buttons
  • proofread README, INSTALL, and help wording

Distro QA

  • run portable helper
  • run frozen helper
  • test outputs on the target Windows machine, not just the build box
+

Class definitions

#

The developer section should show the actual code shape, not a fictional architecture diagram. This block is driven from the current AST of the script in this repo pack.

AST result: this script currently has zero top-level classes. It is a procedural build tool. Instead of faking a class section, this page shows the most important top-level function definitions below.

do_install_work [1789-1889]

Key top-level function from the current release.
def do_install_work(prompt_at_end: bool) -> int:
+    reset_state_files()
+    enable_ansi_colors()
+    info(APP_FULL_NAME)
+    try:
+        info(f"Installer file md5={script_md5(SCRIPT_PATH)}")
+        info(f"Installer file lmd={script_lmd(SCRIPT_PATH)}")
+    except Exception as exc:
+        warn(f"Unable to fingerprint installer file: {exc}")
+    trace(f"argv={sys.argv!r}")
+    trace(f"cwd={Path.cwd()}")
+    trace(f"script={SCRIPT_PATH}")
+
+    if not host_python_ok():
+        raise RuntimeError(f"Host Python must be >= {SUPPORTED_HOST_PYTHON_MIN[0]}.{SUPPORTED_HOST_PYTHON_MIN[1]}")
+
+    check_windows_support()
+
+    record_stage("Prerequisites", "RUNNING", "Checking admin, winget, git, and MSYS2")
+    ensure_winget()
+    ensure_git()
+    msys_root = ensure_msys2()
+    ok("Prerequisites are ready")
+    update_state(msys_root=str(msys_root))
+    record_stage("Prerequisites", "PASS", "Admin, winget, git, and MSYS2 verified")
+
+    record_stage("Dependencies", "RUNNING", "Refreshing MSYS2 and resolving live packages")
+    msys_update(msys_root)
+    repo_packages = get_ucrt_repo_packages(msys_root)
+    update_state(repo_package_count=len(repo_packages))
+    ensured = ensure_capabilities(msys_root, repo_packages)
+    update_state(capabilities=ensured)
+    preferred_python = ensure_python_support_env(msys_root)
+    record_stage("Dependencies", "PASS", f"Resolved {len(ensured)} capability entries against {len(repo_packages)} live packages")
+
+    record_stage("Repositories", "RUNNING", "Cloning or updating OpenShot repositories")
+    repos = clone_or_update_repos(BUILD_ROOT)
+    update_state(repos={k: str(v) for k, v in repos.items()})
+    record_stage("Repositories", "PASS", "OpenShot repositories are present")
+
+    record_stage("Build libopenshot-audio", "RUNNING", "Building and installing audio library")
+    audio_ok = build_libopenshot_audio(msys_root, repos["libopenshot-audio"])
+    if audio_ok:
+        record_stage("Build libopenshot-audio", "PASS", "Audio library built and install manifest found")
+    else:
+        record_stage("Build libopenshot-audio", "FAIL", "Audio build did not leave expected artifacts")
+        raise RuntimeError("libopenshot-audio build failed verification")
+
+    record_stage("Build libopenshot", "RUNNING", "Building and installing video library and Python bindings")
+    lib_result = build_libopenshot(msys_root, repos["libopenshot"], preferred_python)
+    record_stage("Build libopenshot", lib_result["stage_status"], lib_result["stage_detail"])
+    if not lib_result["ok"]:
+        raise RuntimeError("libopenshot build failed to produce the expected binary outputs")
+
+    record_stage("Prepare openshot-qt", "RUNNING", "Patching launch.py, creating runtime bootstrap, and validating startup")
+    patched_launch = patch_openshot_qt_launch(repos["openshot-qt"])
+    launcher = create_launcher(BUILD_ROOT, msys_root, repos["openshot-qt"], preferred_python)
+    qt_ok = verify_qt_launcher(msys_root, repos["openshot-qt"], preferred_python)
+    update_state(
+        launcher_path=str(launcher),
+        run_command=f'cmd /c "{launcher}"',
+        preferred_python=str(preferred_python) if preferred_python else "",
+        launch_patch_applied=patched_launch,
+    )
+    if qt_ok:
+        record_stage("Prepare openshot-qt", "PASS", "launch.py patched, bootstrap launcher created, and runtime smoke import verified")
+    else:
+        record_stage("Prepare openshot-qt", "WARN", "Runtime launcher was created, but the final startup smoke test still needs adjustment")
+
+    record_stage("Verification", "RUNNING", "Checking final expected files and launch entry points")
+    artifacts = read_state().get("artifacts", {})
+    success = (
+        artifacts.get("audio_install_manifest", {}).get("exists") and
+        artifacts.get("libopenshot_bindings_dir", {}).get("exists") and
+        artifacts.get("libopenshot_py", {}).get("exists") and
+        artifacts.get("libopenshot_pyd", {}).get("exists") and
+        artifacts.get("libopenshot_build_dll", {}).get("exists") and
+        artifacts.get("openshot_qt_launch_py", {}).get("exists") and
+        artifacts.get("openshot_qt_launch_patch", {}).get("exists") and
+        artifacts.get("launcher_bootstrap_py", {}).get("exists") and
+        artifacts.get("launcher_cmd", {}).get("exists") and
+        artifacts.get("openshot_qt_runtime_smoke", {}).get("exists")
+    )
+    if success:
+        record_stage("Verification", "PASS", "Build artifacts exist and the real runtime bootstrap smoke test passed")
+        ok("OpenShot source build finished successfully")
+    else:
+        record_stage("Verification", "FAIL", "Build artifacts exist, but the real runtime bootstrap smoke test did not pass yet")
+        raise RuntimeError("Build artifacts were created, but the final runtime bootstrap smoke test failed")
+
+    state = read_state()
+    state["completed"] = True
+    state["success"] = True
+    state["exit_code"] = 0
+    state["finished"] = timestamp()
+    write_state(state)
+
+    print_summary_from_state(state)
+    if prompt_at_end:
+        maybe_prompt_run(state)
+    return 0

create_runtime_bootstrap_script [752-889]

Key top-level function from the current release.
def create_runtime_bootstrap_script(root: Path, msys_root: Path, qt_repo: Path) -> Path:
+    bootstrap = root / "Launch-OpenShot-Qt.py"
+    qt_src_dir = qt_repo / "src"
+    lib_repo = root / "libopenshot"
+    bindings_dir = lib_repo / "build" / "bindings" / "python"
+    build_src_dir = lib_repo / "build" / "src"
+    ucrt_bin_dir = msys_root / "ucrt64" / "bin"
+    freeze_script = qt_repo / "freeze.py"
+
+    bootstrap_text = f'''#!/usr/bin/env python3
+import importlib.util
+import os
+import runpy
+import sys
+import traceback
+from pathlib import Path
+
+QT_REPO = Path(r"{qt_repo}")
+QT_SRC_DIR = Path(r"{qt_src_dir}")
+BINDINGS_DIR = Path(r"{bindings_dir}")
+BUILD_SRC_DIR = Path(r"{build_src_dir}")
+UCRT_BIN_DIR = Path(r"{ucrt_bin_dir}")
+LAUNCH_PY = QT_SRC_DIR / "launch.py"
+FREEZE_PY = Path(r"{freeze_script}")
+SMOKE_FLAG = "--installer-smoke-import"
+FROZEN_FLAG = "--installer-build-frozen"
+
+def prepend_env(name, values):
+    existing = [item for item in os.environ.get(name, "").split(os.pathsep) if item]
+    merged = []
+    for value in values:
+        text = str(value)
+        if text and text not in merged:
+            merged.append(text)
+    for item in existing:
+        if item and item not in merged:
+            merged.append(item)
+    os.environ[name] = os.pathsep.join(merged)
+
+def configure_runtime():
+    os.chdir(str(QT_REPO))
+    bootstrap_paths = [QT_SRC_DIR, BINDINGS_DIR]
+    dll_paths = [BUILD_SRC_DIR, UCRT_BIN_DIR]
+    for entry in bootstrap_paths:
+        text = str(entry)
+        if entry.exists() and text not in sys.path:
+            sys.path.insert(0, text)
+    prepend_env("PYTHONPATH", bootstrap_paths)
+    prepend_env("OPENSHOT_BOOTSTRAP_PATHS", bootstrap_paths)
+    prepend_env("PATH", dll_paths)
+    os.environ.setdefault("OPENSHOT_INSTALLER_RUNTIME_MODE", "source-build")
+    add_dir = getattr(os, "add_dll_directory", None)
+    if add_dir:
+        for entry in dll_paths:
+            if entry.exists():
+                try:
+                    add_dir(str(entry))
+                except OSError:
+                    pass
+
+def inject_bootstrap_args(extra_args):
+    args = []
+    existing = list(extra_args)
+    for entry in (BINDINGS_DIR, QT_SRC_DIR):
+        args.extend(["--path", str(entry)])
+    args.extend(existing)
+    return args
+
+def smoke_import() -> int:
+    configure_runtime()
+    import openshot
+    from classes import settings, project_data, updates, sentry  # noqa: F401
+    from classes.app import OpenShotApp  # noqa: F401
+    print("SMOKE_IMPORT_OK")
+    print(f"openshot={{getattr(openshot, '__file__', '')}}")
+    print(f"settings={{getattr(settings, '__file__', '')}}")
+    print(f"project_data={{getattr(project_data, '__file__', '')}}")
+    print(f"updates={{getattr(updates, '__file__', '')}}")
+    return 0
+
+def run_frozen_build(extra_args):
+    configure_runtime()
+    if not FREEZE_PY.exists():
+        print(f"freeze.py not found at {{FREEZE_PY}}", file=sys.stderr)
+        return 1
+    sys.argv = [str(FREEZE_PY)] + list(extra_args or ["build"])
+    runpy.run_path(str(FREEZE_PY), run_name="__main__")
+    return 0
+
+def run_launch(extra_args):
+    configure_runtime()
+    spec = importlib.util.spec_from_file_location("openshot_runtime_launch", str(LAUNCH_PY))
+    if spec is None or spec.loader is None:
+        raise RuntimeError(f"Unable to load launch.py from {{LAUNCH_PY}}")
+    module = importlib.util.module_from_spec(spec)
+    sys.modules["openshot_runtime_launch"] = module
+    sys.argv = [str(LAUNCH_PY)] + inject_bootstrap_args(extra_args)
+    spec.loader.exec_module(module)
+    try:
+        result = module.main()
+        if isinstance(result, int):
+            return result
+        return 0
+    except SystemExit as exc:
+        code = exc.code
+        if code is None:
+            return 0
+        if isinstance(code, int):
+            return code
+        print(code)
+        return 1
+
+def main():
+    args = list(sys.argv[1:])
+    try:
+        if args and args[0] == SMOKE_FLAG:
+            return smoke_import()
+        if args and args[0] == FROZEN_FLAG:
+            return run_frozen_build(args[1:])
+        return run_launch(args)
+    except SystemExit as exc:
+        code = exc.code
+        if code is None:
+            return 0
+        if isinstance(code, int):
+            return code
+        print(code)
+        return 1
+    except Exception:
+        traceback.print_exc()
+        return 1
+
+if __name__ == "__main__":
+    raise SystemExit(main())
+'''
+    bootstrap.write_text(bootstrap_text, encoding="utf-8", newline="\n")
+    record_artifact("launcher_bootstrap_py", str(bootstrap), bootstrap.exists(), "Python runtime bootstrap launcher")
+    return bootstrap

build_libopenshot [1571-1665]

Key top-level function from the current release.
def build_libopenshot(msys_root: Path, repo_dir: Path, preferred_python: Optional[Path] = None) -> Dict[str, object]:
+    info("Building libopenshot...")
+    repo_msys = windows_to_msys_path(repo_dir)
+    rc = run_msys_script(
+        msys_root,
+        "build_libopenshot.sh",
+        f"""
+cd '{repo_msys}'
+rm -rf build
+export LIBOPENSHOT_AUDIO_DIR=/ucrt64
+cmake -S . -B build -G 'MSYS Makefiles' -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/ucrt64 -DCMAKE_PREFIX_PATH=/ucrt64
+cmake --build build --parallel
+cmake --install build || true
+""",
+        env_name="ucrt64",
+    )
+    cache_file = repo_dir / "build" / "CMakeCache.txt"
+    install_manifest = repo_dir / "build" / "install_manifest.txt"
+    bindings_dir = repo_dir / "build" / "bindings" / "python"
+    openshot_py = bindings_dir / "openshot.py"
+    openshot_pyd = next(iter(bindings_dir.glob("*_openshot*.pyd")), None)
+    if openshot_pyd is None:
+        openshot_pyd = next(iter(bindings_dir.glob("_openshot*.pyd")), None)
+    build_src = repo_dir / "build" / "src"
+    build_dll = next(iter(build_src.glob("libopenshot*.dll")), None)
+    build_import_lib = next(iter(build_src.glob("libopenshot*.a")), None)
+    installed_dll = next(iter((MSYS_ROOT_DEFAULT / "ucrt64" / "bin").glob("libopenshot.dll")), None)
+
+    verify = verify_openshot_import(msys_root, repo_dir, preferred_python)
+    runtime_info = verify.get("runtime_info", {}) if isinstance(verify.get("runtime_info"), dict) else {}
+    site_package_dirs: List[Path] = []
+    for item in verify.get("site_packages", []) or []:
+        try:
+            site_package_dirs.append(Path(str(item)))
+        except Exception:
+            pass
+    if not site_package_dirs:
+        for site_packages_dir in (MSYS_ROOT_DEFAULT / "ucrt64" / "lib").glob("python*/site-packages"):
+            site_package_dirs.append(site_packages_dir)
+
+    installed_pyd = None
+    installed_py = None
+    for site_packages_dir in site_package_dirs:
+        candidate_py = site_packages_dir / "openshot.py"
+        candidate_pyd = next(iter(site_packages_dir.glob("_openshot*.pyd")), None)
+        if candidate_py.exists() and installed_py is None:
+            installed_py = candidate_py
+        if candidate_pyd and installed_pyd is None:
+            installed_pyd = candidate_pyd
+        if installed_py and installed_pyd:
+            break
+
+    built_enough = cache_file.exists() and openshot_py.exists() and bool(openshot_pyd) and bool(build_dll)
+    source_ready = openshot_py.exists() and bool(openshot_pyd) and bindings_dir.exists() and bool(build_dll)
+
+    record_artifact("python_runtime_info", None, bool(runtime_info.get("ok")), runtime_info.get("detail", ""), status="OK" if runtime_info.get("ok") else "WARN")
+    record_artifact("python_runtime_site_packages", "; ".join(str(p) for p in site_package_dirs) if site_package_dirs else None, bool(site_package_dirs), f"{len(site_package_dirs)} python site-packages path(s) discovered", status="OK" if site_package_dirs else "WARN")
+    record_artifact("libopenshot_build_cache", str(cache_file), cache_file.exists(), "libopenshot CMake cache")
+    record_artifact("libopenshot_install_manifest", str(install_manifest), install_manifest.exists(), "libopenshot install manifest", status="OK" if install_manifest.exists() else ("WARN" if source_ready else "MISS"))
+    record_artifact("libopenshot_bindings_dir", str(bindings_dir), bindings_dir.exists(), "libopenshot Python bindings dir")
+    record_artifact("libopenshot_py", str(openshot_py), openshot_py.exists(), "generated openshot.py binding")
+    record_artifact("libopenshot_pyd", str(openshot_pyd) if openshot_pyd else str(bindings_dir / "_openshot*.pyd"), bool(openshot_pyd), "compiled _openshot extension")
+    record_artifact("libopenshot_build_dll", str(build_dll) if build_dll else str(build_src / "libopenshot*.dll"), bool(build_dll), "built libopenshot DLL")
+    record_artifact("libopenshot_build_lib", str(build_import_lib) if build_import_lib else str(build_src / "libopenshot*.a"), bool(build_import_lib), "built libopenshot import/static library", status="OK" if build_import_lib else ("WARN" if bool(build_dll) else "MISS"))
+    record_artifact("libopenshot_installed_dll", str(installed_dll) if installed_dll else str(MSYS_ROOT_DEFAULT / "ucrt64" / "bin" / "libopenshot*.dll"), bool(installed_dll), "installed libopenshot DLL", status="OK" if installed_dll else ("WARN" if source_ready else "MISS"))
+    record_artifact("libopenshot_installed_py", str(installed_py) if installed_py else None, bool(installed_py), "installed openshot.py binding", status="OK" if installed_py else ("WARN" if verify.get("mode") == "source-build" else "MISS"))
+    record_artifact("libopenshot_installed_pyd", str(installed_pyd) if installed_pyd else None, bool(installed_pyd), "installed libopenshot Python extension", status="OK" if installed_pyd else ("WARN" if verify.get("mode") == "source-build" else "MISS"))
+    record_artifact("python_openshot_import", None, bool(verify.get("ok")), verify.get("detail", ""), status="OK" if verify.get("ok") else ("WARN" if source_ready else "MISS"))
+    record_artifact("python_openshot_import_mode", None, bool(verify.get("ok")), str(verify.get("mode", "failed")), status="OK" if verify.get("ok") else ("WARN" if source_ready else "MISS"))
+    record_artifact("python_openshot_import_installed", None, bool(verify.get("installed_import_ok")), "installed site-packages import probe", status="OK" if verify.get("installed_import_ok") else ("WARN" if verify.get("source_import_ok") else "MISS"))
+    record_artifact("python_openshot_import_source", None, bool(verify.get("source_import_ok")), "source-build import probe", status="OK" if verify.get("source_import_ok") else ("WARN" if verify.get("installed_import_ok") else "MISS"))
+
+    compile_ok = rc == 0 and built_enough
+    if compile_ok and verify.get("ok"):
+        stage_status = "PASS"
+        if verify.get("mode") == "installed":
+            stage_detail = "Video library built and installed Python bindings import successfully"
+        else:
+            stage_detail = "Video library built; source-build Python bindings import successfully"
+    elif compile_ok:
+        stage_status = "WARN"
+        stage_detail = "Video library built, but Python import verification still needs runtime bootstrap"
+    else:
+        stage_status = "FAIL"
+        stage_detail = "Video build did not leave the expected binary and binding artifacts"
+
+    return {
+        "ok": compile_ok,
+        "runtime_ok": bool(verify.get("ok")),
+        "stage_status": stage_status,
+        "stage_detail": stage_detail,
+        "compile_ok": compile_ok,
+        "source_ready": source_ready,
+        "verify": verify,
+    }

build_usage_text [1903-1957]

Key top-level function from the current release.
def build_usage_text() -> str:
+    return textwrap.dedent(f"""\
+    {APP_FULL_NAME}
+    {'=' * 72}
+    Purpose:
+      Install, build, verify, launch, and prep distribution helpers for OpenShot 3.5.x on Windows.
+
+    Default behavior:
+      Run the script with no switches to start the full workflow.
+
+    Information switches:
+      --usage   -usage   -u   man   /u     Show this usage screen
+      --about   -about                     Show the project overview
+      --version --ver  -ver  -v  /v       Show version, repo, and website links
+      --license                            Print LICENSE.txt
+      --readme                             Print README.md
+      --debug                              Print a local machine and tool report
+
+    Primary files:
+      Script:   {SCRIPT_PATH.name}
+      Readme:   {README_MD_PATH.name}
+      Install:  {INSTALL_MD_PATH.name}
+      Help:     {HELP_HTML_PATH.name}
+      License:  {LICENSE_TXT_PATH.name}
+
+    Common commands:
+      py -3 {SCRIPT_PATH.name}
+      py -3 {SCRIPT_PATH.name} --usage
+      py -3 {SCRIPT_PATH.name} --about
+      py -3 {SCRIPT_PATH.name} --version
+      py -3 {SCRIPT_PATH.name} --readme
+      py -3 {SCRIPT_PATH.name} --license
+      py -3 {SCRIPT_PATH.name} --debug
+
+    Success looks like:
+      1. Prerequisites go green.
+      2. Dependencies resolve cleanly.
+      3. libopenshot-audio builds.
+      4. libopenshot builds and bindings import.
+      5. Launch-OpenShot-Qt helpers are created.
+      6. OpenShot opens and logs a clean session.
+
+    Generated helpers after a successful run:
+      C:\OpenShotBuild\Launch-OpenShot-Qt.cmd
+      C:\OpenShotBuild\Launch-OpenShot-Qt.py
+      C:\OpenShotBuild\Build-OpenShot-Frozen.cmd
+      C:\OpenShotBuild\Build-OpenShot-Portable.cmd
+
+    Repository:
+      {REPO_URL}
+    Website:
+      {WEBSITE_URL}
+    OpenShot docs:
+      {OPENSHOT_DOCS_URL}
+    """).strip()

main [2101-2143]

Key top-level function from the current release.
def main() -> int:
+    enable_ansi_colors()
+    info_result = try_handle_information_flags(sys.argv)
+    if info_result is not None:
+        return info_result
+
+    child_mode = CHILD_ARG in sys.argv
+    no_prompt = NO_PROMPT_ARG in sys.argv
+
+    if not is_windows():
+        print(red("[FAIL]"), "This installer only supports Windows.", flush=True)
+        return 1
+
+    if child_mode:
+        try:
+            if not is_admin():
+                raise RuntimeError("Elevated child started without admin rights.")
+            ok("Running elevated")
+            return do_install_work(prompt_at_end=not no_prompt)
+        except Exception as exc:
+            fail(str(exc))
+            tb = traceback.format_exc()
+            for line in tb.splitlines():
+                trace(line)
+            state = read_state()
+            state["completed"] = True
+            state["success"] = False
+            state["exit_code"] = 1
+            state["failed"] = True
+            state["error"] = str(exc)
+            state["traceback"] = tb
+            state["finished"] = timestamp()
+            write_state(state)
+            print_summary_from_state(state)
+            return 1
+
+    reset_state_files()
+    rc = elevate_and_wait()
+    state = read_state()
+    print_summary_from_state(state)
+    if rc == 0:
+        maybe_prompt_run(state)
+    return rc
+

Function inventory

#

This is the fast structural scan. If you are orienting yourself, start with do_install_work(), then work outward into the category that matches the problem in front of you.

MSYS2 and path handling

  • windows_to_msys_path [92-101]
  • run_msys_script [104-115]
  • msys_shell [683-684]
  • msys_capture [687-689]
  • msys_stream [692-694]
  • shell_quote [696-697]
  • msys_resolve_python [700-722]
  • msys_env_exports [725-731]
  • windows_cmd_quote [734-735]
  • resolve_windows_python [738-749]
  • msys_update [1182-1188]
  • msys_command_exists [1276-1282]

Other

  • enable_ansi_colors [246-259]
  • color [262-265]
  • green [268-269]
  • red [272-273]
  • yellow [276-277]
  • cyan [280-281]
  • windows_build_number [455-460]

Logging, state, and process execution

  • timestamp [284-285]
  • script_md5 [289-294]
  • script_lmd [296-297]
  • append_text [299-302]
  • log [305-310]
  • info [313-313]
  • trace [314-314]
  • ok [315-315]
  • warn [316-316]
  • fail [317-317]
  • read_state [320-326]
  • write_state [329-332]
  • update_state [335-338]
  • record_stage [341-351]
  • record_artifact [354-365]
  • reset_state_files [368-378]
  • run_capture [381-404]
  • run_stream [407-434]
  • which_any [437-442]

Prerequisites and environment

  • host_python_ok [445-448]
  • is_windows [451-452]
  • is_admin [463-467]
  • powershell_path [470-472]
  • common_paths [475-491]
  • find_existing_tool [494-503]
  • wait_for_elevated_child [506-545]
  • elevate_and_wait [548-563]
  • check_windows_support [566-575]
  • ensure_winget [578-620]
  • winget_has_package [623-626]
  • winget_install [629-641]
  • ensure_git [644-656]
  • ensure_msys2 [659-680]
  • ensure_capabilities [1353-1357]
  • ensure_python_support_env [1360-1393]

Build, verification, and launch

  • create_runtime_bootstrap_script [752-889]
  • patch_openshot_qt_launch [892-931]
  • create_distribution_helper [934-956]
  • create_portable_distribution_helper [959-1129]
  • verify_openshot_import [1447-1539]
  • build_libopenshot_audio [1542-1568]
  • build_libopenshot [1571-1665]
  • create_launcher [1668-1697]
  • verify_qt_launcher [1700-1718]

Workflow orchestration

  • query_python_runtime_info [1132-1179]
  • write_python_probe [1419-1444]
  • print_summary_from_state [1721-1770]
  • maybe_prompt_run [1773-1786]
  • do_install_work [1789-1889]
  • main [2101-2143]

Dependency resolution and repos

  • get_ucrt_repo_packages [1191-1203]
  • normalize [1206-1213]
  • token_set [1216-1217]
  • score_package [1220-1261]
  • rank_candidates [1264-1273]
  • pacman_installed [1285-1287]
  • install_package [1290-1293]
  • resolve_and_install_capability [1296-1350]
  • git_clone_or_update [1396-1407]
  • clone_or_update_repos [1410-1416]

CLI and documentation

  • normalize_cli_token [1892-1893]
  • read_local_text [1896-1900]
  • build_usage_text [1903-1957]
  • build_about_text [1960-1998]
  • build_version_text [2001-2006]
  • print_document [2009-2018]
  • build_debug_report [2021-2058]
  • try_handle_information_flags [2061-2098]
+

Developers quick start

#

Start with the information switches if you want the fast orientation. Then read do_install_work() if you want the real execution flow. That is the center of gravity. The rest of the file exists to make that path predictable and debuggable.

OpenShot_BnR_Module
+├── Version: 1.0.1
+├── Top-level class definitions: 0
+├── Top-level function definitions: 87
+├── Major flows
+│   ├── CLI information mode
+│   ├── Prerequisites and dependency resolution
+│   ├── Repository sync and native build
+│   ├── Python binding verification
+│   ├── Launcher/bootstrap generation
+│   └── Distribution helper generation
+└── Function groups
+    ├── MSYS2 and path handling
+    │   ├── windows_to_msys_path [92-101]
+    │   ├── run_msys_script [104-115]
+    │   ├── msys_shell [683-684]
+    │   ├── msys_capture [687-689]
+    │   └── msys_stream [692-694]
+    │   └── ... 7 more
+    ├── Other
+    │   ├── enable_ansi_colors [246-259]
+    │   ├── color [262-265]
+    │   ├── green [268-269]
+    │   ├── red [272-273]
+    │   └── yellow [276-277]
+    │   └── ... 2 more
+    ├── Logging, state, and process execution
+    │   ├── timestamp [284-285]
+    │   ├── script_md5 [289-294]
+    │   ├── script_lmd [296-297]
+    │   ├── append_text [299-302]
+    │   └── log [305-310]
+    │   └── ... 14 more
+    ├── Prerequisites and environment
+    │   ├── host_python_ok [445-448]
+    │   ├── is_windows [451-452]
+    │   ├── is_admin [463-467]
+    │   ├── powershell_path [470-472]
+    │   └── common_paths [475-491]
+    │   └── ... 11 more
+    ├── Build, verification, and launch
+    │   ├── create_runtime_bootstrap_script [752-889]
+    │   ├── patch_openshot_qt_launch [892-931]
+    │   ├── create_distribution_helper [934-956]
+    │   ├── create_portable_distribution_helper [959-1129]
+    │   └── verify_openshot_import [1447-1539]
+    │   └── ... 4 more
+    ├── Workflow orchestration
+    │   ├── query_python_runtime_info [1132-1179]
+    │   ├── write_python_probe [1419-1444]
+    │   ├── print_summary_from_state [1721-1770]
+    │   ├── maybe_prompt_run [1773-1786]
+    │   └── do_install_work [1789-1889]
+    │   └── ... 1 more
+    ├── Dependency resolution and repos
+    │   ├── get_ucrt_repo_packages [1191-1203]
+    │   ├── normalize [1206-1213]
+    │   ├── token_set [1216-1217]
+    │   ├── score_package [1220-1261]
+    │   └── rank_candidates [1264-1273]
+    │   └── ... 5 more
+    └── CLI and documentation
+        ├── normalize_cli_token [1892-1893]
+        ├── read_local_text [1896-1900]
+        ├── build_usage_text [1903-1957]
+        ├── build_about_text [1960-1998]
+        └── build_version_text [2001-2006]
+        └── ... 3 more
+
+ + + +
+
+
+ + + + + + \ No newline at end of file