commit db4c1b0f1e876dd9330d468f58bd040f0e134b30 Author: Simone Date: Wed Apr 29 23:19:34 2026 +0200 initial: nsSplashPNG plugin 1.0.0 diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9182a7d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{yml,yaml,json}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{cmd,bat,ps1}] +end_of_line = crlf + +[*.{nsi,nsh,sln,vcxproj,filters}] +end_of_line = crlf + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..de4a923 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,51 @@ +# Normalizzazione line endings +* text=auto eol=lf + +# File di codice +*.c text eol=lf +*.cpp text eol=lf +*.h text eol=lf +*.hpp text eol=lf +*.def text eol=lf +*.py text eol=lf +*.ps1 text eol=lf +*.cmd text eol=crlf +*.bat text eol=crlf + +# NSIS +*.nsi text eol=crlf +*.nsh text eol=crlf + +# Markdown / docs / config +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.txt text eol=lf +*.xml text eol=lf + +# Visual Studio (CRLF richiesto) +*.sln text eol=crlf +*.vcxproj text eol=crlf merge=union +*.filters text eol=crlf merge=union +*.rc text eol=crlf working-tree-encoding=UTF-16LE + +# Binari +*.dll binary +*.exe binary +*.lib binary +*.obj binary +*.pdb binary +*.zip binary +*.7z binary +*.png binary +*.ico binary +*.gif binary +*.jpg binary +*.jpeg binary +*.bmp binary +*.pdf binary + +# Linguistic detection (GitHub Linguist / Gitea analogo) +*.nsi linguist-language=NSIS +*.nsh linguist-language=NSIS diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b11a09f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,76 @@ +name: Bug Report +description: Segnala un bug riproducibile +title: "[Bug]: " +labels: ["bug", "triage"] +body: + - type: markdown + attributes: + value: | + Grazie per la segnalazione. Compila i campi sotto per aiutarci a riprodurre il problema. + + - type: input + id: plugin-version + attributes: + label: Versione plugin + description: Contenuto del file VERSION o tag della release scaricata + placeholder: "1.6.0" + validations: + required: true + + - type: dropdown + id: architecture + attributes: + label: Architettura DLL usata + options: + - x86-ansi + - x86-unicode + - amd64-unicode + - non so / multiple + validations: + required: true + + - type: input + id: nsis-version + attributes: + label: Versione NSIS + placeholder: "3.10" + validations: + required: true + + - type: input + id: os + attributes: + label: Sistema operativo + placeholder: "Windows 11 23H2 x64" + validations: + required: true + + - type: textarea + id: repro + attributes: + label: Passi per riprodurre + description: Script .nsi minimale che riproduce il problema + render: nsis + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Comportamento atteso + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Comportamento osservato + description: Includi messaggi di errore esatti, output del log NSIS, screenshot + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Note aggiuntive + description: Qualunque altra informazione utile (workaround tentati, ecc.) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..2126b20 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Domande generali + url: https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/issues?type=question + about: Per domande di utilizzo che non sono bug, apri una issue con label "question" + - name: Security + url: https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/security/advisories/new + about: Segnala vulnerabilità di security in privato (non aprire issue pubbliche) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..bc28f74 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature Request +description: Suggerisci una nuova funzionalità o miglioramento +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problema / Caso d'uso + description: Quale problema vorresti risolvere? Quale workflow vuoi abilitare? + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposta + description: Come dovrebbe funzionare? Includi esempio di API/uso negli script NSIS se possibile. + render: nsis + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternative considerate + description: Altri approcci che hai valutato e perché sono stati scartati + + - type: textarea + id: additional + attributes: + label: Note aggiuntive diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..3db4c88 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## Descrizione + + + +## Tipo di Modifica + +- [ ] Bug fix (modifica non breaking che risolve un problema) +- [ ] Feature (modifica non breaking che aggiunge funzionalità) +- [ ] Breaking change (fix o feature che rompe la compatibilità) +- [ ] Documentazione +- [ ] Build / CI + +## Architetture Testate + +- [ ] `x86-ansi` +- [ ] `x86-unicode` +- [ ] `amd64-unicode` + +## Checklist + +- [ ] Build completa senza warning: `python build_plugin.py` +- [ ] CHANGELOG.md aggiornato (sezione `[Unreleased]`) +- [ ] README / docs aggiornati se l'API è cambiata +- [ ] Esempi aggiornati se l'API è cambiata +- [ ] Conventional Commits rispettato +- [ ] Nessun file in `dist/` o `__pycache__/` committato +- [ ] Self-review completata + +## Note Aggiuntive + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4102e1e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,66 @@ +# Build (GitHub) — pipeline CI primaria +# +# Strategia hosting: +# - Repo primario su Gitea (push iniziale) +# - Push mirror automatico Gitea -> GitHub +# - GitHub esegue TUTTA la CI (Gitea non esegue Actions) +# +# Trigger: +# - push su main (incluso quello propagato dal mirror Gitea) +# - PR aperte su GitHub +# - run manuale via UI + +name: Build + +on: + push: + branches: [main] + paths-ignore: + - '**.md' + - 'docs/**' + - 'examples/**' + pull_request: + branches: [main] + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + config: [x86-ansi, x86-unicode, x64-unicode] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Read VERSION + id: version + shell: pwsh + run: | + $v = (Get-Content VERSION -Raw).Trim() + "version=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + - name: Build ${{ matrix.config }} + run: python build_plugin.py --config ${{ matrix.config }} --verbosity minimal + + - name: Upload DLL + uses: actions/upload-artifact@v4 + with: + name: ${{ github.event.repository.name }}-${{ steps.version.outputs.version }}-${{ matrix.config }} + path: dist/** + if-no-files-found: error + retention-days: 30 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e4cf67d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,99 @@ +# Release (GitHub) — pipeline release primaria +# +# Triggerata quando un tag v* viene propagato dal mirror Gitea. +# Costruisce gli artifact per tutte le architetture e crea la GitHub Release. + +name: Release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + tag: + description: 'Tag versione (es. v1.7.0)' + required: true + +permissions: + contents: write + +jobs: + build: + runs-on: windows-latest + strategy: + fail-fast: true + matrix: + config: [x86-ansi, x86-unicode, x64-unicode] + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - uses: microsoft/setup-msbuild@v2 + + - name: Build ${{ matrix.config }} + run: python build_plugin.py --config ${{ matrix.config }} + + - name: Determine version & arch + id: meta + shell: pwsh + run: | + $tag = "${{ github.ref_name }}" + if ([string]::IsNullOrEmpty($tag) -or $tag -eq "main") { $tag = "${{ inputs.tag }}" } + $v = $tag -replace '^v','' + $map = @{ 'x86-ansi' = 'x86-ansi'; 'x86-unicode' = 'x86-unicode'; 'x64-unicode' = 'amd64-unicode' } + $dir = $map['${{ matrix.config }}'] + echo "version=$v" >> $env:GITHUB_OUTPUT + echo "tag=$tag" >> $env:GITHUB_OUTPUT + echo "dir=$dir" >> $env:GITHUB_OUTPUT + + - name: Package + shell: pwsh + run: | + $name = "${{ github.event.repository.name }}-${{ steps.meta.outputs.version }}-${{ steps.meta.outputs.dir }}" + $stage = "stage/$name" + New-Item -ItemType Directory -Force -Path $stage | Out-Null + Copy-Item "dist/${{ steps.meta.outputs.dir }}/*" $stage -Recurse + Copy-Item README.md, LICENSE, VERSION, CHANGELOG.md $stage + if (Test-Path "include") { Copy-Item "include" $stage -Recurse } + if (Test-Path "examples") { Copy-Item "examples" $stage -Recurse } + Compress-Archive -Path "$stage/*" -DestinationPath "$name.zip" -Force + + - uses: actions/upload-artifact@v4 + with: + name: pkg-${{ matrix.config }} + path: '*.zip' + + publish: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: SHA256SUMS + run: | + cd artifacts + sha256sum *.zip > SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: | + artifacts/*.zip + artifacts/SHA256SUMS.txt + generate_release_notes: true + body: | + **Repository primario**: vedi link Gitea nel README. + + Il mirror GitHub è generato automaticamente dal repo Gitea (sorgente di verità). + Per issue e PR aprire ticket sul repository primario Gitea. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76d88c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Build output (src/ subdirs only — dist/ is tracked, contains pre-built DLLs) +bin/ +obj/ +Build/ +build/ + +# Visual Studio +*.user +*.suo +*.ncb +*.sdf +.vs/ +ipch/ +*.VC.db +*.VC.VC.opendb +*.opendb +*.aps +*.tlog +*.idb +*.pdb +*.ilk +*.exp +*.lib +*.obj + +# Python +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +*.egg-info/ +.venv/ +venv/ + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +Thumbs.db +desktop.ini +.DS_Store + +# NSIS +*.nsh.bak + +# Logs +*.log +.logs/ + +# Sensitive +.env +.env.local +*.pem +*.key + +# Migration/legacy scripts (temporary, not part of the plugin) +tools/t*.py +tools/check_*.py +tools/legacy/ + +# MSBuild output directories in src/ (build artifacts, not source) +src/**/Release/ +src/**/Release Unicode/ +src/**/x64/ +*.recipe +*.iobj +*.ipdb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0249157 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +Tutte le modifiche rilevanti a questo progetto sono documentate in questo file. + +Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.0.0/), +e il progetto aderisce al [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0] — 2026-04-29 + +### Added +- Prima release come repository indipendente +- Build script unificato (`build_plugin.py`) con CLI canonica +- CI/CD via GitHub Actions (mirror automatico Gitea → GitHub) +- Documentazione completa (README, CONTRIBUTING, SECURITY) + +[Unreleased]: https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/compare/v1.0.0...HEAD +[1.0.0]: https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a1f95df --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,137 @@ +# Contribuire a nsSplashPNG + +Grazie per il tuo interesse a contribuire! + +> Repository primario: [https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng](https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng) +> Mirror GitHub: [https://github.com/systempal/nsis-plugin-nssplashpng](https://github.com/systempal/nsis-plugin-nssplashpng) (read-only — apri PR su Gitea) + +## Prerequisiti + +| Strumento | Versione | Note | +|-----------|----------|------| +| Visual Studio | 2022 / 2026 | Workload: Desktop development with C++ (toolset v143/v145) | +| Python | 3.7+ | Per `build_plugin.py` | +| Git | 2.30+ | Con supporto submoduli se applicabile | + +## Quick Start + +```powershell +git clone https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng.git +cd nsis-plugin-nssplashpng +python build_plugin.py +``` + +Output in `dist/{x86-ansi,x86-unicode,amd64-unicode}/`. + +## Workflow Sviluppo + +### 1. Branch + +```powershell +git checkout -b feat/nome-feature # nuova feature +git checkout -b fix/descrizione-bug # bugfix +git checkout -b docs/aggiornamento # solo doc +git checkout -b chore/manutenzione # build, dipendenze +``` + +### 2. Commit + +Segui [Conventional Commits](https://www.conventionalcommits.org/it/v1.0.0/): + +``` +feat: aggiunge supporto per formato XYZ +fix: corregge crash su Windows 11 ARM +docs: aggiorna esempio di utilizzo +chore: aggiorna toolset MSVC +refactor: estrae logica parsing in funzione separata +test: aggiunge test per edge case +build: aggiorna script di build +ci: aggiorna workflow GitHub Actions +``` + +Breaking changes: + +``` +feat!: rinomina API Foo() in Bar() + +BREAKING CHANGE: Foo() rinominata in Bar(); aggiornare tutti i chiamanti. +``` + +### 3. Pull Request + +PR aperte verso il branch `main` su Gitea. Verifica: + +- [ ] Build completa senza warning per tutte le architetture: `python build_plugin.py` +- [ ] Esempi aggiornati se cambia l'API +- [ ] `CHANGELOG.md` aggiornato (sezione `[Unreleased]`) +- [ ] Documentazione aggiornata (README, docs/API.md se esiste) +- [ ] Conventional Commits rispettato +- [ ] Nessun file in `dist/` o `__pycache__/` committato + +## Build Script + +``` +python build_plugin.py [opzioni] +``` + +| Opzione | Default | Descrizione | +|---------|---------|-------------| +| `--config` | `all` | Architettura: `x86-ansi`, `x86-unicode`, `x64-unicode`, `all` | +| `--toolset` | `auto` | Visual Studio: `2022`, `2026`, `auto` (rileva il più recente) | +| `--jobs` | numero CPU | Thread paralleli per la build | +| `--clean` | false | Pulisce `dist/` prima di compilare | +| `--install-dir` | none | Copia DLL anche in `/{arch}/` (es. NSIS plugin folder) | +| `--verbose` | false | Output dettagliato (mostra comandi MSBuild) | + +Exit codes: +- `0` — Successo +- `1` — Errore di build +- `2` — Prerequisiti mancanti +- `3` — Argomenti invalidi + +## Release Workflow + +Le release sono automatiche via tag Git: + +```powershell +# 1. Aggiorna VERSION e CHANGELOG.md +echo "1.7.0" > VERSION +# (modifica CHANGELOG.md spostando entry da [Unreleased] a [1.7.0]) + +# 2. Commit +git add VERSION CHANGELOG.md +git commit -m "chore(release): v1.7.0" + +# 3. Tag annotato +git tag -a v1.7.0 -m "Release v1.7.0" + +# 4. Push +git push origin main +git push origin v1.7.0 +``` + +Il workflow `.github/workflows/release.yml` compilerà tutte le architetture +e creerà la release su Gitea con gli artifact ZIP allegati. Il mirror GitHub +riceverà automaticamente il tag. + +## Code Style + +- C/C++: stile coerente con il file modificato +- Python: PEP 8 (4 spazi indent, line length flessibile) +- Header file: `#include` ordinati: stdlib → Win32 → progetto +- Encoding: UTF-8 senza BOM (eccetto `.rc` che richiede UTF-16 LE) +- Line endings: LF in repo (`.gitattributes` normalizza) + +## Reporting Bug + +Apri una [issue](https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/issues/new) usando il template "Bug Report". +Includi: +- Versione plugin e architettura DLL usata +- Versione NSIS +- Sistema operativo +- Script `.nsi` minimale che riproduce il problema +- Output / errore esatto + +## Domande + +Per domande generali, apri una [issue](https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/issues) con label `question`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2056c23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Simone + +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/README.md b/README.md new file mode 100644 index 0000000..da181fa --- /dev/null +++ b/README.md @@ -0,0 +1,348 @@ +# nsSplashPNG - PNG Splash Screen Plugin for NSIS + +A modern NSIS plugin that displays splash screens with full alpha transparency support using WIC (Windows Imaging Component). + +## Features + +- **Multiple Image Formats**: PNG, JPEG, BMP, GIF, TIFF, ICO via WIC decoder +- **Full Alpha Transparency**: Proper PNG/GIF alpha channel support via WIC and layered windows +- **Configurable Fade Speed**: Customizable fade in/out speed (1-255 steps, 30ms-7650ms duration) +- **Non-Blocking Execution**: Script continues after fade-in while splash stays visible +- **Auto-Close Timer**: Configurable automatic close after specified milliseconds +- **Click-to-Close**: Optional user interaction to close splash +- **No Cancel Option**: Prevent users from closing splash during critical operations +- **Manual Control**: Programmatic show/stop with fade effects +- **Multi-Monitor Support**: Center splash on primary, current (mouse), specific index, or nearest to a screen coordinate +- **Zero Warnings**: Clean compilation with Visual Studio 2022 + +## Installation + +1. Copy `nssplashpng.dll` to your NSIS plugins directory: + - `x86-unicode`: For 32-bit NSIS Unicode builds + - `x64-unicode`: For 64-bit NSIS Unicode builds + - `x86-ansi`: For 32-bit NSIS ANSI builds + +2. Include the plugin directory in your script: + ```nsis + !addplugindir "path\to\plugins\x86-unicode" + ``` + +## Building from Source + +### Prerequisites +- Visual Studio 2022 or later with C++ build tools +- Python 3.x + +### Build Steps +```powershell +cd nsSplashPNG +python build_plugin.py # Build all architectures +python build_plugin.py --config x86-unicode # Single architecture (x86-ansi|x86-unicode|x64-unicode|all) +python build_plugin.py --vs-version 2026 # Specific VS version (2022|2026|auto) +python build_plugin.py --clean # Clean dist/ before build +python build_plugin.py --install-dir "C:\NSIS\Plugins" # Copy to additional NSIS directory +python build_plugin.py --verbosity minimal # MSBuild verbosity (quiet|minimal|normal|detailed|diagnostic) +``` + +The script will: +- Build for x86-unicode, x64-unicode, and x86-ansi configurations +- Copy DLLs to `dist/{x86-unicode|amd64-unicode|x86-ansi}/` directories +- Clean up intermediate build artifacts from `src/Build/` +- Supports parallel builds with automatic CPU optimization + +## Usage + +### Important: Plugin Memory Management + +**CRITICAL**: Always use `/NOUNLOAD` flag on the first `show` call to keep the plugin loaded in memory. Without this, NSIS unloads the plugin after each call, losing all state (window handles, threads, etc.), causing `stop` to fail. + +```nsis +; Correct - keeps plugin loaded +nssplashpng::show /NOUNLOAD 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +nssplashpng::stop /FADEOUT + +; Wrong - plugin unloaded after show, stop will fail +nssplashpng::show 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +nssplashpng::stop /FADEOUT +``` + +### Syntax + +```nsis +nsSplashPNG::show [/NOUNLOAD] [/FADEIN [step]] [/FADEOUT [step]] [/NOCANCEL] [/MONITOR ] +nsSplashPNG::stop [/FADEOUT [step]] +``` + +**Note**: The timeout must be specified immediately after `/NOUNLOAD` (if present), followed by optional flags, with the image path as the last parameter. + +### Parameters + +#### show function +- `/NOUNLOAD` - **Required for stop() to work**: Keeps plugin in memory between calls +- `/FADEIN [step]` - Enable fade in effect. Optional step (1-255) controls speed (default: 15) + - Higher step = faster fade (255=instant, 51=fast, 15=normal, 5=slow) + - Duration formula: `(255 / step) × 30ms` +- `/FADEOUT [step]` - Enable fade out effect when auto-closing via timer (optional step) +- `/NOCANCEL` - Disable click-to-close (splash can only be closed programmatically or by timer) +- `/MONITOR ` - Select the monitor on which to center the splash (default: primary monitor) + - `PRIMARY` - Primary monitor (default) + - `CURRENT` or `MOUSE` - Monitor where the mouse cursor is + - `1`, `2`, `3`, ... - Monitor by 1-based index (enumeration order) + - `POINT x y` - Monitor nearest to the given screen coordinates (uses `MONITOR_DEFAULTTONEAREST`); useful to target the monitor where an application window was last open +- `` - Auto-close timer in milliseconds (0 = manual close only) +- `` - Path to image file (PNG, JPEG, BMP, GIF, TIFF, ICO) + +#### stop function +- `/FADEOUT [step]` - Enable fade out effect when closing. Optional step (1-255) controls speed (default: 15) + +### Examples + +#### Basic Usage +```nsis +; Show splash for 3 seconds (PNG with alpha) - no /NOUNLOAD needed for auto-close only +nsSplashPNG::show 3000 "$EXEDIR\splash.png" + +; Show JPEG splash (opaque, no transparency) +nsSplashPNG::show 3000 "$EXEDIR\splash.jpg" +``` + +#### Fade Effects +```nsis +; Default fade speed (step 15, ~510ms) +nsSplashPNG::show 3000 /FADEIN /FADEOUT "$EXEDIR\splash.png" + +; Fast fade (step 51, ~150ms) +nsSplashPNG::show 3000 /FADEIN 51 /FADEOUT 51 "$EXEDIR\splash.png" + +; Slow fade (step 5, ~1530ms) +nsSplashPNG::show 5000 /FADEIN 5 /FADEOUT 5 "$EXEDIR\splash.png" + +; Instant (step 255, ~30ms) +nsSplashPNG::show 3000 /FADEIN 255 /FADEOUT 255 "$EXEDIR\splash.png" + +; Different speeds for fade-in and fade-out +nsSplashPNG::show 3000 /FADEIN 51 /FADEOUT 5 "$EXEDIR\splash.png" +``` + +#### Manual Control +```nsis +; CRITICAL: Use /NOUNLOAD when using stop() to close manually +nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL "$EXEDIR\splash.png" + +; ... do installation work ... + +; Close with fade out +nsSplashPNG::stop /FADEOUT + +; Or close with custom fade speed +nsSplashPNG::stop /FADEOUT 51 +``` + +#### Real Installer Example +```nsis +Name "My Application" +OutFile "setup.exe" +InstallDir "$PROGRAMFILES\MyApp" + +!addplugindir "plugins\x86-unicode" + +Page instfiles + +Section "Main Installation" + ; Show splash with fade in, prevent user closing, no auto-close + ; CRITICAL: /NOUNLOAD required for stop() to work + nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL /FADEIN "$EXEDIR\splash.png" + + ; Install files + SetOutPath "$INSTDIR" + File /r "app\*.*" + + ; Create shortcuts + CreateDirectory "$SMPROGRAMS\MyApp" + CreateShortcut "$SMPROGRAMS\MyApp\MyApp.lnk" "$INSTDIR\myapp.exe" + + ; Close splash with fade out + nsSplashPNG::stop /FADEOUT +SectionEnd +``` + +#### Monitor Selection +```nsis +; Show on primary monitor (default) +nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL "$EXEDIR\splash.png" + +; Show on the monitor where the mouse cursor is +nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL /MONITOR CURRENT "$EXEDIR\splash.png" + +; Show on the second monitor (by 1-based index) +nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL /MONITOR 2 "$EXEDIR\splash.png" + +; Show on the monitor nearest to a known screen coordinate +; (e.g. where an application window was previously displayed) +nsSplashPNG::show /NOUNLOAD 0 /MONITOR POINT -1906 200 /FADEIN "$EXEDIR\splash.png" + +nsSplashPNG::stop /FADEOUT +``` + +> **Note on parameter order**: `/MONITOR POINT x y` must be placed **before** `/FADEIN`/`/FADEOUT`. +> The `/FADEIN` parser reads ahead one token to check for a custom step; if that token +> starts with `/`, it is requeued — but a subsequent `popstring()` in the loop will +> overwrite it before `/MONITOR` can be processed. + +#### Additional Examples + +See the `examples/` folder for more usage demonstrations: +- **example_basic.nsi** - Simple splash with auto-close timer +- **example_advanced.nsi** - Manual control with fade effects +- **example_formats.nsi** - Multi-format support (PNG, JPEG, BMP, GIF) +- **example_fade_speed.nsi** - Customizable fade speed demonstrations +- **example_nonblocking.nsi** - Non-blocking behavior with background processing + +## Technical Details + +### Implementation +- **Graphics Library**: WIC (Windows Imaging Component) via windowscodecs.lib +- **Image Format**: 32bppPBGRA (pre-multiplied alpha) for proper transparency +- **Window Type**: Layered windows with `WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW` +- **Transparency**: `UpdateLayeredWindow` with per-pixel alpha blending +- **Rendering**: Hardware-accelerated via `UpdateLayeredWindow` with `ULW_ALPHA` +- **Animation**: 30ms timer intervals for smooth fade (0-255 alpha range) + - Configurable step size (default: 15 steps = ~510ms duration) + - Duration formula: `(255 / step) × 30ms` +- **Threading**: Background window thread for non-blocking execution + - `show()` returns after fade-in completes + - Window stays open in background + - `stop()` waits for fade-out to complete before returning +- **Plugin State**: Requires `/NOUNLOAD` to maintain global variables (window handles, threads) between calls + +### Comparison with nsAdvsplash + +| Feature | nsAdvsplash | nsSplashPNG | +|----------------------|-------------|-----------------| +| PNG Alpha Support | Partial | Full | +| Fade Effects | Yes | Yes | +| Transparency Method | Color key | Per-pixel alpha | +| Graphics API | GDI | WIC | +| Window Type | Standard | Layered | +| Click-to-Close | Yes | Yes | +| Compilation Warnings | Some | None | + +### Supported Image Formats + +WIC automatically detects and decodes the following formats: +- **PNG** - Full alpha transparency support +- **JPEG/JPG** - No transparency (opaque) +- **BMP** - Alpha transparency if 32-bit, otherwise opaque +- **GIF** - Basic transparency support +- **TIFF** - Alpha transparency if supported by variant +- **ICO** - Icon format with alpha +- **WMP** - Windows Media Photo + +**Note**: Formats without alpha channels (JPEG, 24-bit BMP) will display as opaque images. + +### System Requirements +- Windows Vista or later (for layered window support) +- WIC - Windows Imaging Component (included with Windows Vista+) + +## API Reference + +### Exported Functions + +#### `show()` +Creates and displays a splash screen window in a background thread. + +**Parameters** (parsed from NSIS stack in reverse order): +1. Timeout in milliseconds (integer, 0 = manual close only) +2. `/FADEIN [step]` - Optional fade in with configurable step (1-255, default: 15) +3. `/FADEOUT [step]` - Optional auto fade out with configurable step (1-255, default: 15) +4. `/NOCANCEL` - Disable click-to-close +5. `/MONITOR ` - Monitor selection (see parameter docs); must appear **before** `/FADEIN` +6. `/NOUNLOAD` - **Required for stop() to work**: Keeps plugin in memory +7. Image path (string) - last parameter + +**Behavior**: +- Cleans up any previous window/thread +- Loads image with WIC (`IWICImagingFactory`, `IWICBitmapDecoder`) +- Supports PNG, JPEG, BMP, GIF, TIFF, ICO, WMP formats +- Converts to 32bppPBGRA format for proper alpha handling +- Creates centered topmost layered window on the selected monitor: + - `MONITOR_MODE_PRIMARY` (default) — `GetSystemMetrics(SM_CX/CYSCREEN)` + - `MONITOR_MODE_MOUSE` — `GetCursorPos` + `MonitorFromPoint` + - `MONITOR_MODE_INDEX` — `EnumDisplayMonitors` by 0-based index + - `MONITOR_MODE_POINT` — `MonitorFromPoint(x, y, MONITOR_DEFAULTTONEAREST)` +- Uses `UpdateLayeredWindow` for per-pixel alpha transparency +- Starts background thread with window message loop +- Applies fade in effect if `/FADEIN` specified (30ms timer with configurable steps) +- Sets auto-close timer if timeout > 0 +- Enables click-to-close unless `/NOCANCEL` specified +- **Returns control after fade-in completes** (non-blocking) +- Without `/NOUNLOAD`, plugin unloads and loses all state + +**Return value**: Pushes "success" to NSIS stack + +#### `stop()` +Closes the splash screen window with optional fade-out. + +**Parameters** (parsed from NSIS stack): +1. `/FADEOUT [step]` - Optional fade out with configurable step (1-255, default: 15) + +**Behavior**: +- If `/FADEOUT` specified: + - Starts fade-out timer (30ms intervals) + - Waits for fade to complete: `(255 / step) × 30ms` + - Destroys window after fade completes +- Otherwise: + - Immediately destroys window +- **Requires show() to have used /NOUNLOAD** or window handle will be NULL + +**Return value**: Pushes "success" to NSIS stack + +**Behavior**: +- Applies fade out effect if `/FADEOUT` specified (30ms timer, 15 steps) +- Destroys window and releases WIC resources +- Terminates splash thread and cleans up handles + +## Troubleshooting + +### Image not showing +- Verify image file path is correct and accessible +- Check that image file is in a supported format (PNG, JPEG, BMP, GIF, TIFF, ICO) +- Ensure NSIS can read the file at install time +- Verify WIC can decode the specific image file variant + +### No transparency +- Confirm PNG has alpha channel (not just RGB) +- Verify you're using the correct plugin DLL for your NSIS version +- Check Windows version supports layered windows (Vista+) + +### Fade effects not smooth +- Normal behavior - fade uses 30ms timer with 15-step alpha increments +- Total fade duration ≈ 450-500ms + +## License + +Created for NSIS installer system. + +## Credits + +- NSIS Plugin API +- WIC - Windows Imaging Component +- Windows Layered Windows API (`UpdateLayeredWindow`) +- Build system inspired by nsProcess plugin + +--- + +*See [README_IT.md](README_IT.md) for the Italian version.* + +## Version History + +### 1.0.0 (Current) +- Full PNG alpha transparency support via WIC +- Per-pixel alpha blending with `UpdateLayeredWindow` +- Fade in/out effects (30ms, 15 steps) +- Auto-close timer with configurable timeout +- Click-to-close with optional disable (`/NOCANCEL`) +- Blocks installer until splash closes +- x86-unicode, x64-unicode, and x86-ansi builds +- Zero compilation warnings with guarded macro definitions +- Optimized build system with parallel compilation support diff --git a/README_IT.md b/README_IT.md new file mode 100644 index 0000000..ac2bf8f --- /dev/null +++ b/README_IT.md @@ -0,0 +1,211 @@ +# nsSplashPNG - Plugin NSIS per Splash Screen PNG + +Plugin NSIS moderno che visualizza splash screen con supporto completo alla trasparenza alfa tramite WIC (Windows Imaging Component). + +## Funzionalità + +- **Formati Immagine Multipli**: PNG, JPEG, BMP, GIF, TIFF, ICO tramite WIC decoder +- **Trasparenza Alfa Completa**: Supporto canale alfa per PNG/GIF tramite WIC e layered windows +- **Velocità Fade Configurabile**: Velocità fade in/out personalizzabile (1-255 step, durata 30ms-7650ms) +- **Esecuzione Non Bloccante**: Lo script continua dopo il fade-in mentre lo splash rimane visibile +- **Timer Auto-Close**: Chiusura automatica configurabile dopo un numero specificato di millisecondi +- **Click per Chiudere**: Interazione utente opzionale per chiudere lo splash +- **No Cancel**: Impedisce all'utente di chiudere lo splash durante operazioni critiche +- **Controllo Manuale**: Show/stop programmabile con effetti fade +- **Supporto Multi-Monitor**: Centra lo splash sul monitor primario, corrente (mouse), per indice, o il più vicino a una coordinata +- **Zero Warning**: Compilazione pulita con Visual Studio 2022 + +## Installazione + +1. Copia `nssplashpng.dll` nella cartella dei plugin NSIS: + - `x86-unicode`: Per build NSIS Unicode a 32-bit + - `x64-unicode`: Per build NSIS Unicode a 64-bit + - `x86-ansi`: Per build NSIS ANSI a 32-bit + +2. Includi la cartella del plugin nello script: + ```nsis + !addplugindir "path\to\plugins\x86-unicode" + ``` + +## Compilazione dai Sorgenti + +### Prerequisiti +- Visual Studio 2022 o successivo con strumenti C++ +- Python 3.x + +### Passaggi + +```powershell +cd nsSplashPNG +python build_plugin.py # Compila tutte le architetture +python build_plugin.py --config x86-unicode # Solo un'architettura (x86-ansi|x86-unicode|x64-unicode|all) +python build_plugin.py --vs-version 2026 # Versione VS specifica (2022|2026|auto) +python build_plugin.py --clean # Pulizia dist/ prima della build +python build_plugin.py --install-dir "C:\NSIS\Plugins" # Copia in directory NSIS aggiuntiva +python build_plugin.py --verbosity minimal # Verbosita MSBuild (quiet|minimal|normal|detailed|diagnostic) +``` + +Lo script: +- Compila per x86-unicode, x64-unicode e x86-ansi +- Copia le DLL in `dist/{x86-unicode|amd64-unicode|x86-ansi}/` +- Pulisce i file intermedi dalla cartella `src/Build/` +- Supporta build parallele con ottimizzazione automatica CPU + +## Utilizzo + +### Importante: Gestione Memoria del Plugin + +**CRITICO**: Usare sempre il flag `/NOUNLOAD` alla prima chiamata a `show` per mantenere il plugin caricato in memoria. Senza questo, NSIS scarica il plugin dopo ogni chiamata, perdendo tutto lo stato (handle finestra, thread, ecc.), causando il fallimento di `stop`. + +```nsis +; Corretto - mantiene il plugin caricato +nssplashpng::show /NOUNLOAD 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +nssplashpng::stop /FADEOUT + +; Sbagliato - plugin scaricato dopo show, stop fallirà +nssplashpng::show 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +nssplashpng::stop /FADEOUT +``` + +### Sintassi + +```nsis +nsSplashPNG::show [/NOUNLOAD] [/FADEIN [step]] [/FADEOUT [step]] [/NOCANCEL] [/MONITOR ] +nsSplashPNG::stop [/FADEOUT [step]] +``` + +**Nota**: Il timeout deve essere specificato subito dopo `/NOUNLOAD` (se presente), seguito da flag opzionali, con il percorso immagine come ultimo parametro. + +### Parametri + +#### Funzione show +- `/NOUNLOAD` - **Richiesto perché stop() funzioni**: Mantiene il plugin in memoria tra le chiamate +- `/FADEIN [step]` - Abilita effetto fade in. Step opzionale (1-255) controlla la velocità (default: 15) + - Step più alto = fade più veloce (255=istantaneo, 51=veloce, 15=normale, 5=lento) + - Formula durata: `(255 / step) × 30ms` +- `/FADEOUT [step]` - Abilita effetto fade out alla chiusura automatica tramite timer (step opzionale) +- `/NOCANCEL` - Disabilita click per chiudere (lo splash può essere chiuso solo programmaticamente o dal timer) +- `/MONITOR ` - Seleziona il monitor su cui centrare lo splash (default: monitor primario) + - `PRIMARY` - Monitor primario (default) + - `CURRENT` o `MOUSE` - Monitor dove si trova il cursore del mouse + - `1`, `2`, `3`, ... - Monitor per indice 1-based (ordine di enumerazione) + - `POINT x y` - Monitor più vicino alle coordinate schermo fornite +- `` - Timer auto-close in millisecondi (0 = solo chiusura manuale) +- `` - Percorso al file immagine (PNG, JPEG, BMP, GIF, TIFF, ICO) + +#### Funzione stop +- `/FADEOUT [step]` - Abilita effetto fade out alla chiusura. Step opzionale (1-255) controlla la velocità (default: 15) + +### Esempi + +#### Utilizzo Base +```nsis +; Mostra splash per 3 secondi (PNG con alfa) - /NOUNLOAD non necessario per solo auto-close +nsSplashPNG::show 3000 "$EXEDIR\splash.png" + +; Mostra splash JPEG (opaco, senza trasparenza) +nsSplashPNG::show 3000 "$EXEDIR\splash.jpg" +``` + +#### Effetti Fade +```nsis +; Velocità fade default (step 15, ~510ms) +nsSplashPNG::show 3000 /FADEIN /FADEOUT "$EXEDIR\splash.png" + +; Fade veloce (step 51, ~150ms) +nsSplashPNG::show 3000 /FADEIN 51 /FADEOUT 51 "$EXEDIR\splash.png" + +; Fade lento (step 5, ~1530ms) +nsSplashPNG::show 5000 /FADEIN 5 /FADEOUT 5 "$EXEDIR\splash.png" +``` + +#### Controllo Manuale +```nsis +; CRITICO: Usare /NOUNLOAD quando si usa stop() per chiudere manualmente +nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL "$EXEDIR\splash.png" + +; ... operazioni di installazione ... + +; Chiudi con fade out +nsSplashPNG::stop /FADEOUT +``` + +## Dettagli Tecnici + +### Implementazione +- **Libreria Grafica**: WIC (Windows Imaging Component) tramite windowscodecs.lib +- **Formato Immagine**: 32bppPBGRA (alfa pre-moltiplicato) per corretta trasparenza +- **Tipo Finestra**: Layered window con `WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW` +- **Trasparenza**: `UpdateLayeredWindow` con blending alfa per pixel +- **Rendering**: Accelerato hardware tramite `UpdateLayeredWindow` con `ULW_ALPHA` +- **Animazione**: Timer da 30ms per fade fluido (range alfa 0-255) +- **Threading**: Thread finestra in background per esecuzione non bloccante + +### Confronto con nsAdvsplash + +| Funzionalità | nsAdvsplash | nsSplashPNG | +|--------------|-------------|-------------| +| Supporto Alfa PNG | Parziale | Completo | +| Effetti Fade | Sì | Sì | +| Metodo Trasparenza | Color key | Alfa per pixel | +| API Grafica | GDI | WIC | +| Tipo Finestra | Standard | Layered | +| Click per Chiudere | Sì | Sì | +| Warning Compilazione | Alcuni | Nessuno | + +### Formati Immagine Supportati + +WIC rileva e decodifica automaticamente i seguenti formati: +- **PNG** - Supporto completo trasparenza alfa +- **JPEG/JPG** - Senza trasparenza (opaco) +- **BMP** - Trasparenza alfa se a 32-bit, altrimenti opaco +- **GIF** - Supporto trasparenza base +- **TIFF** - Trasparenza alfa se supportata dalla variante +- **ICO** - Formato icona con alfa +- **WMP** - Windows Media Photo + +### Requisiti di Sistema +- Windows Vista o successivo (per supporto layered window) +- WIC - Windows Imaging Component (incluso da Windows Vista in poi) + +## Risoluzione Problemi + +### Immagine non visibile +- Verificare che il percorso del file immagine sia corretto e accessibile +- Controllare che il file sia in un formato supportato +- Verificare che WIC possa decodificare il file immagine specifico + +### Nessuna trasparenza +- Confermare che il PNG abbia canale alfa (non solo RGB) +- Verificare di usare la DLL plugin corretta per la versione NSIS + +### Effetti fade non fluidi +- Comportamento normale - il fade usa timer 30ms con incrementi alfa di 15 step +- Durata totale fade ≈ 450-500ms + +## Licenza + +Creato per il sistema di installazione NSIS. + +## Crediti + +- NSIS Plugin API +- WIC - Windows Imaging Component +- Windows Layered Windows API (`UpdateLayeredWindow`) +- Sistema di build ispirato al plugin nsProcess + +## Cronologia Versioni + +### 1.0.0 (Corrente) +- Supporto trasparenza alfa PNG completo tramite WIC +- Blending alfa per pixel con `UpdateLayeredWindow` +- Effetti fade in/out (30ms, 15 step) +- Timer auto-close con timeout configurabile +- Click per chiudere con disabilitazione opzionale (`/NOCANCEL`) +- Build x86-unicode, x64-unicode e x86-ansi +- Zero warning di compilazione +- Sistema di build ottimizzato con compilazione parallela + +--- + +*See [README.md](README.md) for the English version.* diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7c4588c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policy + +## Versioni Supportate + +Solo l'ultima versione minor riceve fix di security. + +| Versione | Supporto | +|----------|----------| +| Latest minor (es. 1.0.0) | ✅ | +| Versioni precedenti | ❌ | + +## Reporting di una Vulnerabilità + +**Non aprire issue pubbliche per vulnerabilità di security.** + +Per segnalare una vulnerabilità: + +1. **Preferito**: usa la funzione [Security Advisory privato](https://gitea.emulab.it/Simone/nsis-plugin-nssplashpng/security/advisories/new) di Gitea +2. **Alternativa**: invia email a `security@example.com` (sostituire con email reale prima del primo release) + +Includi: +- Descrizione della vulnerabilità +- Passi per riprodurla +- Versione/i affette +- Possibile mitigazione (se nota) + +## Tempo di Risposta + +- Acknowledgment iniziale: entro 7 giorni +- Valutazione: entro 14 giorni +- Patch (se confermata): tempistiche dipendono dalla severità + +## Hardening + +Questo plugin gira con i permessi del processo NSIS chiamante (tipicamente +l'utente che esegue l'installer). Non richiede privilegi elevati per la +compilazione né per l'esecuzione, salvo dove esplicitamente documentato. + +DLL distribuite tramite Releases sono compilate da CI GitHub Actions con +toolchain pinnata. Verificare hash SHA256 pubblicati nelle release. diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..5e3cadf --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/build_plugin.py b/build_plugin.py new file mode 100644 index 0000000..857efbe --- /dev/null +++ b/build_plugin.py @@ -0,0 +1,679 @@ +#!/usr/bin/env python3 +""" +Build script for nsSplashPNG plugin - All configurations +Supports multiple build configurations with flexible parameters +""" + +import argparse +import errno +import multiprocessing +import os +import shutil +import stat +import subprocess +import sys +import threading +import time +import xml.etree.ElementTree as ET +from concurrent.futures import ThreadPoolExecutor, as_completed +from pathlib import Path +from typing import List, Optional, Tuple + +# Ensure stdout/stderr handle Unicode on Windows CI (cp1252 default breaks non-ASCII) +if sys.stdout.encoding and sys.stdout.encoding.lower() not in ('utf-8', 'utf_8'): + import io + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + + +# --------------------------------------------------------------------------- +# Filesystem helpers +# --------------------------------------------------------------------------- + +def _on_rmtree_error(func, path, exc_info): + if func in (os.unlink, os.rmdir) and exc_info[1].errno == errno.EACCES: + try: + os.chmod(path, stat.S_IWRITE) + func(path) + return + except Exception: + pass + for i in range(5): + try: + time.sleep(0.1 * (i + 1)) + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.unlink(path) + return + except Exception: + pass + raise exc_info[1] + + +def robust_rmtree(path): + if not os.path.exists(path): + return + try: + shutil.rmtree(path, onexc=_on_rmtree_error) + except TypeError: + try: + shutil.rmtree(path, onerror=_on_rmtree_error) + except Exception: + pass + except Exception: + try: + shutil.rmtree(path, ignore_errors=True) + except Exception: + pass + + +# --------------------------------------------------------------------------- +# Colors & Spinner +# --------------------------------------------------------------------------- + +class Colors: + CYAN = "\033[36m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + RED = "\033[31m" + GRAY = "\033[90m" + BLUE = "\033[34m" + RESET = "\033[0m" + BOLD = "\033[1m" + BRIGHT_GREEN = "\033[92m" + BRIGHT_CYAN = "\033[96m" + BRIGHT_WHITE = "\033[97m" + BRIGHT_YELLOW = "\033[93m" + BRIGHT_RED = "\033[91m" + + +class Spinner: + def __init__(self, message: str = "Building...", delay: float = 0.1, total: int = 0): + self.spinner = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f'] + self.delay = delay + self.message = message + self.running = False + self.thread = None + self.start_time = time.time() + + def update(self, current=None): + pass + + def _spin(self): + idx = 0 + _block = '\u28ff' + while self.running: + elapsed = time.time() - self.start_time + n_blocks = int(elapsed // 2) + time_blocks = f"{Colors.YELLOW}{_block * n_blocks}{Colors.RESET}" + spin_char = f"{Colors.YELLOW}{self.spinner[idx % len(self.spinner)]}{Colors.RESET}" + msg = f"{Colors.BOLD}{Colors.CYAN}{self.message}{Colors.RESET}" + time_str = f"{Colors.GREEN}{int(elapsed)}s{Colors.RESET}" + sys.stdout.write(f"\r{msg} {time_str} {time_blocks}{spin_char} ") + sys.stdout.flush() + idx += 1 + time.sleep(self.delay) + sys.stdout.write("\r" + " " * (len(self.message) + 80) + "\r") + sys.stdout.flush() + + def __enter__(self): + if sys.stdout.isatty(): + self.running = True + self.thread = threading.Thread(target=self._spin, daemon=True) + self.thread.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.running: + self.running = False + if self.thread: + self.thread.join() + + +# --------------------------------------------------------------------------- +# Build configuration +# --------------------------------------------------------------------------- + +class BuildConfig: + def __init__(self, name: str, config: str, platform: str, dest_dir: str): + self.name = name + self.config = config + self.platform = platform + self.dest_dir = dest_dir + + +CONFIGS = { + 'x86-ansi': BuildConfig('x86-ansi', 'Release', 'Win32', 'x86-ansi'), + 'x86-unicode': BuildConfig('x86-unicode', 'Release Unicode', 'Win32', 'x86-unicode'), + 'x64-unicode': BuildConfig('x64-unicode', 'Release Unicode', 'x64', 'amd64-unicode'), +} + +DLL_NAME = 'nsSplashPNG.dll' +_VSWHERE = Path(r'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe') +_VS_RANGES = {'2026': '[18.0,19.0)', '2022': '[17.0,18.0)'} +_VS_TOOLSETS = {'2026': 'v145', '2022': 'v143'} + + +# --------------------------------------------------------------------------- +# MSBuild discovery +# --------------------------------------------------------------------------- + +def _find_msbuild_vswhere(vs_version: str) -> Optional[Tuple[Path, str, str]]: + if not _VSWHERE.exists(): + return None + to_try = ['2026', '2022'] if vs_version == 'auto' else [vs_version] + for ver in to_try: + if ver not in _VS_RANGES: + continue + try: + result = subprocess.run( + [str(_VSWHERE), '-version', _VS_RANGES[ver], '-latest', + '-requires', 'Microsoft.Component.MSBuild', + '-find', r'MSBuild\**\Bin\MSBuild.exe'], + capture_output=True, text=True, timeout=15, + ) + if result.returncode == 0: + lines = [ln.strip() for ln in result.stdout.splitlines() if ln.strip()] + if lines: + p = Path(lines[0]) + if p.exists(): + return p, _VS_TOOLSETS[ver], ver + except Exception: + pass + return None + + +def find_msbuild(vs_version: str = 'auto') -> Optional[Tuple[Path, str, str]]: + result = _find_msbuild_vswhere(vs_version) + if result: + return result + # Fallback: well-known paths + to_try = ['2026', '2022'] if vs_version == 'auto' else [vs_version] + for ver in to_try: + for edition in ('Community', 'Professional', 'Enterprise', 'BuildTools'): + p = Path(rf'C:\Program Files\Microsoft Visual Studio\{ver}\{edition}\MSBuild\Current\Bin\MSBuild.exe') + if p.exists(): + return p, _VS_TOOLSETS[ver], ver + return None + + +# --------------------------------------------------------------------------- +# Project paths & version +# --------------------------------------------------------------------------- + +def get_project_paths() -> Tuple[Path, Path, Path]: + script_dir = Path(__file__).parent.absolute() + project_dir = script_dir / 'src' + project_file = project_dir / 'nsSplashPNG.vcxproj' + dist_dir = script_dir / 'dist' + return project_dir, project_file, dist_dir + + +def get_plugins_dir() -> Optional[Path]: + """Return workspace-level plugins/ dir (4 levels above src/modules//).""" + p = Path(__file__).parent.parent.parent.parent / 'plugins' + return p if p.is_dir() else None + + +def read_version() -> str: + vf = Path(__file__).parent / 'VERSION' + try: + return vf.read_text(encoding='utf-8-sig').strip() + except Exception: + return '0.0.0' + + +# --------------------------------------------------------------------------- +# CPU info & build optimizations +# --------------------------------------------------------------------------- + +def get_cpu_info() -> dict: + try: + import psutil + logical = psutil.cpu_count(logical=True) + physical = psutil.cpu_count(logical=False) + return {'logical_cores': logical, 'physical_cores': physical, + 'has_hyperthreading': logical > physical, 'has_psutil': True} + except ImportError: + n = multiprocessing.cpu_count() + return {'logical_cores': n, 'physical_cores': n, + 'has_hyperthreading': False, 'has_psutil': False} + + +def get_optimal_threads() -> int: + info = get_cpu_info() + return info['physical_cores'] if info['has_hyperthreading'] and info['physical_cores'] > 1 else info['logical_cores'] + + +def get_build_optimizations() -> List[str]: + return [ + '/p:BuildInParallel=true', + '/p:MultiProcessorCompilation=true', + '/p:PreferredToolArchitecture=x64', + '/p:UseSharedCompilation=true', + '/nodeReuse:true', + '/p:GenerateResourceUsePreserializedResources=true', + ] + + +def get_memory_optimizations() -> List[str]: + try: + import psutil + gb = psutil.virtual_memory().total / (1024 ** 3) + if gb >= 16: + return ['/p:DisableFastUpToDateCheck=false', '/p:BuildProjectReferences=true', + '/p:UseCommonOutputDirectory=false'] + elif gb >= 8: + return ['/p:DisableFastUpToDateCheck=false'] + return [] + except ImportError: + return ['/p:DisableFastUpToDateCheck=false'] + + +def print_cpu_info(use_parallel: bool, use_optimizations: bool = True) -> None: + if not use_parallel: + print(f"{Colors.CYAN}Build mode: {Colors.RESET} {Colors.BOLD}Single-threaded{Colors.RESET}") + if use_optimizations: + print(f"{Colors.CYAN}Optimizations: {Colors.RESET} {Colors.GREEN}ENABLED{Colors.RESET} {Colors.GRAY}(memory, caching){Colors.RESET}") + return + + info = get_cpu_info() + optimal = get_optimal_threads() + print(f"{Colors.CYAN}Build mode: {Colors.RESET} {Colors.BRIGHT_WHITE}{'Parallel' if use_parallel else 'Sequential'}{Colors.RESET}") + print(f"{Colors.CYAN}Logical cores: {Colors.RESET} {Colors.BRIGHT_WHITE}{info['logical_cores']}{Colors.RESET}") + print(f"{Colors.CYAN}Physical cores: {Colors.RESET} {Colors.BRIGHT_WHITE}{info['physical_cores']}{Colors.RESET}") + if info['has_hyperthreading']: + print(f"{Colors.CYAN}Hyperthreading: {Colors.RESET} {Colors.BRIGHT_GREEN}ENABLED{Colors.RESET}") + print(f"{Colors.CYAN}Optimal threads: {Colors.RESET} {Colors.BRIGHT_WHITE}{optimal}{Colors.RESET} {Colors.GRAY}(using physical cores){Colors.RESET}") + else: + print(f"{Colors.CYAN}Hyperthreading: {Colors.RESET} {Colors.GRAY}NOT AVAILABLE{Colors.RESET}") + print(f"{Colors.CYAN}Optimal threads: {Colors.RESET} {Colors.BRIGHT_WHITE}{optimal}{Colors.RESET}") + print(f"{Colors.CYAN}MSBuild threads: {Colors.RESET} {Colors.BRIGHT_WHITE}{optimal}{Colors.RESET}") + if use_optimizations: + print(f"{Colors.CYAN}Optimizations: {Colors.RESET} {Colors.BRIGHT_GREEN}ENABLED{Colors.RESET} {Colors.GRAY}(parallel, memory, caching){Colors.RESET}") + try: + import psutil + gb = psutil.virtual_memory().total / (1024 ** 3) + print(f"{Colors.CYAN}Available memory: {Colors.RESET} {Colors.BRIGHT_WHITE}{gb:.1f} GB{Colors.RESET}") + except ImportError: + print(f"{Colors.CYAN}Available memory: {Colors.RESET} {Colors.GRAY}Unknown (install psutil){Colors.RESET}") + else: + print(f"{Colors.CYAN}Optimizations: {Colors.RESET} {Colors.BRIGHT_RED}DISABLED{Colors.RESET}") + if not info['has_psutil']: + print(f"{Colors.GRAY}Note: Install 'psutil' for detailed CPU/memory info (pip install psutil){Colors.RESET}") + print() + + +# --------------------------------------------------------------------------- +# Build helpers +# --------------------------------------------------------------------------- + +def format_time(seconds: float) -> str: + if seconds < 60: + return f"{seconds:.2f}s" + elif seconds < 3600: + return f"{int(seconds // 60)}m {seconds % 60:.1f}s" + return f"{int(seconds // 3600)}h {int((seconds % 3600) // 60)}m {seconds % 60:.0f}s" + + +def build_configuration( + msbuild_path: Path, + project_file: Path, + config: BuildConfig, + platform_toolset: str, + *, + rebuild: bool = True, + verbosity: str = 'quiet', + parallel: bool = True, + optimizations: bool = True, + counter: str = "", + capture_output: bool = False, +) -> Tuple[bool, float, str]: + optimal = get_optimal_threads() + cmd = [ + str(msbuild_path), + str(project_file), + f'/t:{"Rebuild" if rebuild else "Build"}', + f'/p:Configuration={config.config}', + f'/p:Platform={config.platform}', + f'/p:OutDir=Build\\{config.name}\\', + f'/p:IntDir=Build\\{config.name}\\obj\\', + '/p:WindowsTargetPlatformVersion=10.0', + f'/p:PlatformToolset={platform_toolset}', + f'/v:{verbosity}', + ] + if parallel: + cmd += [f'/maxcpucount:{optimal}', '/p:UseMultiToolTask=true', f'/p:CL_MPCount={optimal}'] + cmd += ['/p:Optimization=MaxSpeed', '/p:ExceptionHandling=Sync'] + if optimizations: + cmd += get_build_optimizations() + get_memory_optimizations() + + if not capture_output: + color = Colors.BRIGHT_YELLOW + print(f"\n{color}{'='*60}{Colors.RESET}") + label = f"[{counter}] " if counter else "" + print(f"{color}Building {Colors.BRIGHT_WHITE}{config.name:15s}{Colors.RESET} {color}{label}({'rebuild' if rebuild else 'incremental'}){Colors.RESET}") + print(f"{color}{'='*60}{Colors.RESET}") + + start = time.time() + try: + if capture_output: + result = subprocess.run(cmd, check=False, capture_output=True, text=True) + return result.returncode == 0, time.time() - start, result.stdout + result.stderr + else: + result = subprocess.run(cmd, check=False) + return result.returncode == 0, time.time() - start, "" + except Exception as e: + elapsed = time.time() - start + msg = f"ERROR: Build failed with exception: {e}" + if capture_output: + return False, elapsed, msg + print(msg) + return False, elapsed, "" + + +def copy_output(project_dir: Path, dist_dir: Path, config: BuildConfig) -> Tuple[bool, int, Optional[Path]]: + output_file = project_dir / 'Build' / config.name / DLL_NAME + if not output_file.exists(): + print(f"ERROR: DLL not found: {output_file}") + return False, 0, None + dest = dist_dir / config.dest_dir + dest.mkdir(parents=True, exist_ok=True) + try: + dst = dest / DLL_NAME + shutil.copy2(output_file, dst) + return True, output_file.stat().st_size, dst + except Exception as e: + print(f"ERROR: Failed to copy {config.name}: {e}") + return False, 0, None + + +def copy_to_plugins(dist_dir: Path, config: BuildConfig, plugins_dir: Path) -> Optional[Path]: + """Copy DLL from dist// to plugins//.""" + src = dist_dir / config.dest_dir / DLL_NAME + if not src.exists(): + return None + dst_dir = plugins_dir / config.dest_dir + dst_dir.mkdir(parents=True, exist_ok=True) + dst = dst_dir / DLL_NAME + shutil.copy2(src, dst) + return dst + + +def clean_build_artifacts(project_dir: Path, configs: List[BuildConfig]) -> None: + print(f"\n{Colors.CYAN}Cleaning build artifacts...{Colors.RESET}") + build_base = project_dir / 'Build' + if not build_base.exists(): + return + for cfg in configs: + d = build_base / cfg.name + if d.exists(): + try: + robust_rmtree(d) + print(f" {Colors.GRAY}- Cleaned:{Colors.RESET} {cfg.name}") + except Exception as e: + print(f" {Colors.RED}- Failed to clean {cfg.name}: {e}{Colors.RESET}") + try: + if build_base.exists() and not any(build_base.iterdir()): + build_base.rmdir() + except Exception: + pass + print(f"{Colors.BRIGHT_GREEN}Build artifacts cleaned successfully.{Colors.RESET}") + + +def print_available_configurations(project_file: Path) -> None: + try: + tree = ET.parse(project_file) + root = tree.getroot() + ns = {'ms': 'http://schemas.microsoft.com/developer/msbuild/2003'} + configs = [] + for ig in root.findall('.//ms:ItemGroup', ns): + for pc in ig.findall('ms:ProjectConfiguration', ns): + inc = pc.get('Include', '') + parts = inc.split('|') + if len(parts) == 2: + configs.append(tuple(parts)) + print("Available configurations in project:") + print("-" * 60) + from collections import defaultdict + grouped: dict = defaultdict(list) + for cfg, plat in configs: + grouped[cfg].append(plat) + for cfg_name in sorted(grouped): + print(f" {cfg_name:25s} - {', '.join(sorted(grouped[cfg_name]))}") + print(f"\nTotal: {len(configs)} configuration(s)\n") + except Exception as e: + print(f"Could not parse project file: {e}") + + +_print_lock = threading.Lock() + + +def _build_configs_parallel( + msbuild_path: Path, + project_file: Path, + configs: List[BuildConfig], + platform_toolset: str, + *, + rebuild: bool, + verbosity: str, + parallel: bool, + optimizations: bool, + project_dir: Path, + dist_dir: Path, +) -> list: + n = len(configs) + print(f"\nParallel: launching {n} builds simultaneously...") + print("=" * 50) + total_start = time.time() + results_by_idx: dict = {} + idx_lock = threading.Lock() + + def _build_one(idx: int, config: BuildConfig): + import contextlib + import io as _io + success, build_time, captured = build_configuration( + msbuild_path, project_file, config, platform_toolset, + rebuild=rebuild, verbosity=verbosity, + parallel=parallel, optimizations=optimizations, + capture_output=True, + ) + copy_buf = _io.StringIO() + if success: + with contextlib.redirect_stdout(copy_buf): + copy_ok, file_size, dest_path = copy_output(project_dir, dist_dir, config) + else: + copy_ok, file_size, dest_path = False, 0, None + + with _print_lock: + sys.stdout.write("\r" + " " * 80 + "\r") + sys.stdout.flush() + tag = "OK" if (success and copy_ok) else "FAILED" + size_str = f"{file_size:,} bytes" if file_size > 0 else "N/A" + copy_out = copy_buf.getvalue() + if copy_out.strip(): + print(copy_out.rstrip()) + if captured.strip(): + lines = [ln for ln in captured.splitlines() if "MSBuild" not in ln and "Copyright" not in ln] + filtered = "\n".join(lines).strip() + if filtered: + print(filtered) + color = Colors.BRIGHT_GREEN if (success and copy_ok) else Colors.RED + print(f"{color}[{tag}]{Colors.RESET} {config.name} ({format_time(build_time)}) {size_str}") + if dest_path: + print(f" -> {dest_path}") + + with idx_lock: + results_by_idx[idx] = (config, success and copy_ok, build_time, file_size, dest_path) + + with Spinner("Building configurations...", total=n) as s: + with ThreadPoolExecutor(max_workers=n) as executor: + futures = {executor.submit(_build_one, i, cfg): i for i, cfg in enumerate(configs)} + for fut in as_completed(futures): + s.update() + exc = fut.exception() + if exc: + idx = futures[fut] + with _print_lock: + print(f"ERROR: worker for config index {idx} raised: {exc}") + with idx_lock: + results_by_idx[idx] = (configs[idx], False, 0.0, 0, None) + + wall = time.time() - total_start + print(f"\nAll {n} configs finished in {format_time(wall)} (wall clock)") + return [results_by_idx[i] for i in range(n)] + + +# --------------------------------------------------------------------------- +# main +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser(description='Build nsSplashPNG plugin for NSIS') + parser.add_argument('--configs', nargs='+', + choices=list(CONFIGS.keys()) + ['all'], default=None) + parser.add_argument('--config', # singular alias used by CI + choices=list(CONFIGS.keys()) + ['all'], default=None) + parser.add_argument('--rebuild', action='store_true', default=True) + parser.add_argument('--no-rebuild', action='store_false', dest='rebuild') + parser.add_argument('--parallel', action='store_true', default=True) + parser.add_argument('--no-parallel', action='store_false', dest='parallel') + parser.add_argument('--verbosity', + choices=['quiet', 'minimal', 'normal', 'detailed', 'diagnostic'], + default='quiet') + parser.add_argument('--clean', action='store_true', default=False) + parser.add_argument('--list', action='store_true') + parser.add_argument('--list-project', action='store_true') + parser.add_argument('--no-optimizations', action='store_true') + parser.add_argument('--vs-version', choices=['auto', '2026', '2022'], default='auto') + parser.add_argument('--final', action='store_true', default=False, + help='Force rebuild + clean (pre-release build)') + parser.add_argument('--install-dir', type=Path, default=None, + help='Copy built DLLs to this directory (in / subdirs)') + args = parser.parse_args() + + # Merge --config (singular) into --configs list + if args.config and not args.configs: + args.configs = [args.config] + + if args.final: + args.rebuild = True + args.clean = True + + project_dir, project_file, dist_dir = get_project_paths() + + if args.list_project: + print_available_configurations(project_file) + return 0 + + if args.list: + for name, config in CONFIGS.items(): + print(f" {name:15s} -> {config.config} / {config.platform}") + return 0 + + if not project_file.exists(): + print(f"ERROR: project file not found: {project_file}", file=sys.stderr) + return 3 + + msbuild_result = find_msbuild(args.vs_version) + if not msbuild_result: + print("ERROR: MSBuild not found. Install Visual Studio 2022 or 2026.", file=sys.stderr) + return 2 + msbuild_path, platform_toolset, vs_version_name = msbuild_result + + version = read_version() + + if not args.configs: + configs_to_build = list(CONFIGS.values()) + elif 'all' in args.configs: + configs_to_build = list(CONFIGS.values()) + else: + configs_to_build = [CONFIGS[n] for n in args.configs] + + # Banner + print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}{'='*60}") + print(f"Building nsSplashPNG plugin v{version} - {Colors.BRIGHT_WHITE}{len(configs_to_build)}{Colors.RESET} {Colors.BOLD}{Colors.BRIGHT_CYAN}configuration(s)") + print(f"{'='*60}{Colors.RESET}") + print(f"{Colors.CYAN}VS: {Colors.RESET} {Colors.BRIGHT_WHITE}{vs_version_name}{Colors.RESET} {Colors.GRAY}({platform_toolset}){Colors.RESET}") + print(f"{Colors.CYAN}MSBuild: {Colors.RESET} {Colors.BRIGHT_WHITE}{msbuild_path}{Colors.RESET}") + print(f"{Colors.CYAN}Project: {Colors.RESET} {Colors.BRIGHT_WHITE}{project_file}{Colors.RESET}") + print(f"{Colors.CYAN}Dist: {Colors.RESET} {Colors.BRIGHT_WHITE}{dist_dir}{Colors.RESET}") + print(f"{Colors.CYAN}Rebuild: {Colors.RESET} {Colors.BRIGHT_WHITE}{args.rebuild}{Colors.RESET}") + print(f"{Colors.CYAN}Verbosity: {Colors.RESET} {Colors.BRIGHT_WHITE}{args.verbosity}{Colors.RESET}") + print() + + use_optimizations = not args.no_optimizations + print_cpu_info(args.parallel, use_optimizations) + + build_results = [] + total_start = time.time() + + if args.parallel and len(configs_to_build) > 1: + build_results = _build_configs_parallel( + msbuild_path, project_file, configs_to_build, platform_toolset, + rebuild=args.rebuild, verbosity=args.verbosity, + parallel=True, optimizations=use_optimizations, + project_dir=project_dir, dist_dir=dist_dir, + ) + else: + for i, config in enumerate(configs_to_build, 1): + success, b_time, _ = build_configuration( + msbuild_path, project_file, config, platform_toolset, + rebuild=args.rebuild, verbosity=args.verbosity, + parallel=args.parallel, optimizations=use_optimizations, + counter=f"{i}/{len(configs_to_build)}", + ) + if success: + ok, size, path = copy_output(project_dir, dist_dir, config) + build_results.append((config, ok, b_time, size, path)) + else: + build_results.append((config, False, b_time, 0, None)) + + total_time = time.time() - total_start + all_success = all(res[1] for res in build_results) + + print(f"\n{Colors.BOLD}{Colors.BRIGHT_GREEN if all_success else Colors.RED}{'='*50}") + print("ALL BUILDS SUCCESSFUL!" if all_success else "SOME BUILDS FAILED!") + print(f"{'='*50}{Colors.RESET}") + + # Copy to workspace plugins/ dir if present + plugins_dir = get_plugins_dir() + plugins_copied = [] + if plugins_dir and all_success: + for cfg, ok, _, _, _ in build_results: + if ok: + dst = copy_to_plugins(dist_dir, cfg, plugins_dir) + if dst: + plugins_copied.append(str(dst)) + if plugins_copied: + print(f"\n{Colors.CYAN}Installed to plugins/:{Colors.RESET}") + for p in plugins_copied: + print(f" {Colors.GRAY}->{Colors.RESET} {p}") + + if args.install_dir and all_success: + print(f"\n{Colors.CYAN}Installing to {args.install_dir}...{Colors.RESET}") + for cfg, ok, _, _, path in build_results: + if ok and path: + dest = args.install_dir / cfg.dest_dir + dest.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, dest / DLL_NAME) + print(f" {Colors.GRAY}- Installed:{Colors.RESET} {dest / DLL_NAME}") + + print(f"\n{Colors.BOLD}{Colors.BRIGHT_CYAN}{'-'*50}") + print(f"Build Summary - VS {vs_version_name} ({platform_toolset}):") + for cfg, ok, b_time, size, _ in build_results: + color = Colors.GREEN if ok else Colors.RED + tag = "OK " if ok else "FAIL" + print(f" {color}{tag}{Colors.RESET} {cfg.name:15s} - {format_time(b_time):8s} - {size:11,d} bytes") + print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}{'-'*50}{Colors.RESET}") + print(f"Total time: {format_time(total_time)}\n") + + if args.clean: + clean_build_artifacts(project_dir, configs_to_build) + + return 0 if all_success else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/dist/amd64-unicode/nsSplashPNG.dll b/dist/amd64-unicode/nsSplashPNG.dll new file mode 100644 index 0000000..d13557d Binary files /dev/null and b/dist/amd64-unicode/nsSplashPNG.dll differ diff --git a/dist/x86-ansi/nsSplashPNG.dll b/dist/x86-ansi/nsSplashPNG.dll new file mode 100644 index 0000000..6143782 Binary files /dev/null and b/dist/x86-ansi/nsSplashPNG.dll differ diff --git a/dist/x86-unicode/nsSplashPNG.dll b/dist/x86-unicode/nsSplashPNG.dll new file mode 100644 index 0000000..0cf668f Binary files /dev/null and b/dist/x86-unicode/nsSplashPNG.dll differ diff --git a/docs/QUICK_REFERENCE.md b/docs/QUICK_REFERENCE.md new file mode 100644 index 0000000..35ccd51 --- /dev/null +++ b/docs/QUICK_REFERENCE.md @@ -0,0 +1,157 @@ +# nsSplashPNG Quick Reference + +## Basic Syntax + +```nsis +nsSplashPNG::show [/NOUNLOAD] [/FADEIN [step]] [/FADEOUT [step]] [/NOCANCEL] +nsSplashPNG::stop [/FADEOUT [step]] +``` + +## Critical Requirements + +⚠️ **ALWAYS use `/NOUNLOAD` when calling `stop()` manually** + +Without `/NOUNLOAD`, the plugin unloads after `show()` and loses all state, causing `stop()` to fail. + +## Common Use Cases + +### Auto-Close Only (No /NOUNLOAD needed) +```nsis +; Splash closes automatically after 3 seconds +nssplashpng::show 3000 "$TEMP\splash.png" +``` + +### Manual Close (Requires /NOUNLOAD) +```nsis +; Show splash, no auto-close +nssplashpng::show /NOUNLOAD 0 "$TEMP\splash.png" + +; ... do work ... + +; Close manually +nssplashpng::stop +``` + +### With Fade Effects (Auto-close) +```nsis +; Fade in, wait 3 seconds, fade out +nssplashpng::show 3000 /FADEIN /FADEOUT "$TEMP\splash.png" +``` + +### With Fade Effects (Manual close - Requires /NOUNLOAD) +```nsis +; Fade in, no auto-close +nssplashpng::show /NOUNLOAD 0 /FADEIN "$TEMP\splash.png" + +; ... do work ... + +; Fade out when closing +nssplashpng::stop /FADEOUT +``` + +### Prevent User Closing +```nsis +; User cannot click to close, must wait for timer or stop() +nssplashpng::show /NOUNLOAD 0 /NOCANCEL "$TEMP\splash.png" +nssplashpng::stop +``` + +### Custom Fade Speed +```nsis +; Fast fade (step 51, ~150ms) +nssplashpng::show /NOUNLOAD 0 /FADEIN 51 "$TEMP\splash.png" +nssplashpng::stop /FADEOUT 51 + +; Slow fade (step 5, ~1530ms) +nssplashpng::show /NOUNLOAD 0 /FADEIN 5 "$TEMP\splash.png" +nssplashpng::stop /FADEOUT 5 + +; Instant (step 255, ~30ms) +nssplashpng::show /NOUNLOAD 0 /FADEIN 255 "$TEMP\splash.png" +nssplashpng::stop /FADEOUT 255 +``` + +## Fade Speed Reference + +| Step Value | Duration | Description | +|------------|----------|-------------| +| 1 | ~7650ms | Very slow | +| 5 | ~1530ms | Slow | +| 15 | ~510ms | **Default**, smooth | +| 51 | ~150ms | Fast | +| 85 | ~90ms | Very fast | +| 255 | ~30ms | Instant | + +**Formula**: Duration = (255 / step) × 30ms + +## Parameter Order + +The order matters! Parameters must appear in this sequence: + +```nsis +nssplashpng::show [flags] [more_flags] +``` + +**Correct**: +```nsis +nssplashpng::show /NOUNLOAD 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +``` + +**Wrong** (image_path must be last): +```nsis +nssplashpng::show /FADEIN "$TEMP\splash.png" 5000 /FADEOUT +``` + +## Supported Image Formats + +- **PNG** - Full alpha transparency ✅ +- **JPEG/JPG** - Opaque (no transparency) +- **BMP** - Alpha if 32-bit, otherwise opaque +- **GIF** - Basic transparency support +- **TIFF** - Alpha if supported by variant +- **ICO** - Icon format with alpha +- **WMP** - Windows Media Photo + +## Troubleshooting + +### Problem: stop() doesn't close the splash +**Solution**: Add `/NOUNLOAD` to the `show()` call + +### Problem: Splash closes too quickly/slowly +**Solution**: Adjust the step parameter: +- Faster: Increase step value (e.g., 51, 85, 255) +- Slower: Decrease step value (e.g., 5, 3, 1) + +### Problem: User can close splash during installation +**Solution**: Add `/NOCANCEL` flag + +### Problem: Script waits at show() forever +**Solution**: Either: +- Use a timeout value > 0 for auto-close, OR +- Use timeout = 0 and call `stop()` manually (requires `/NOUNLOAD`) + +## Non-Blocking Behavior + +The `show()` function returns control to your script **after the fade-in completes**, allowing you to: +1. Show the splash +2. Continue executing your script +3. Close the splash when ready + +```nsis +nssplashpng::show /NOUNLOAD 0 /FADEIN "$TEMP\splash.png" +; Control returns here after fade-in +DetailPrint "Installing..." +; Splash is still visible while we work +Sleep 2000 +nssplashpng::stop /FADEOUT +; Control returns here after fade-out +``` + +## Examples + +See the `examples/` directory for complete working examples: +- `example_basic.nsi` - Simple usage patterns +- `example_advanced.nsi` - Real installer with progress +- `example_formats.nsi` - Multi-format demonstrations +- `example_fade_speed.nsi` - All fade speed options +- `example_nonblocking.nsi` - Non-blocking execution pattern diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..7347e2d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,306 @@ +# nsSplashPNG - PNG Splash Screen Plugin for NSIS + +A modern NSIS plugin that displays splash screens with full alpha transparency support using WIC (Windows Imaging Component). + +## Features + +- **Multiple Image Formats**: PNG, JPEG, BMP, GIF, TIFF, ICO via WIC decoder +- **Full Alpha Transparency**: Proper PNG/GIF alpha channel support via WIC and layered windows +- **Configurable Fade Speed**: Customizable fade in/out speed (1-255 steps, 30ms-7650ms duration) +- **Non-Blocking Execution**: Script continues after fade-in while splash stays visible +- **Auto-Close Timer**: Configurable automatic close after specified milliseconds +- **Click-to-Close**: Optional user interaction to close splash +- **No Cancel Option**: Prevent users from closing splash during critical operations +- **Manual Control**: Programmatic show/stop with fade effects +- **Zero Warnings**: Clean compilation with Visual Studio 2022 + +## Installation + +1. Copy `nssplashpng.dll` to your NSIS plugins directory: + - `x86-unicode`: For 32-bit NSIS Unicode builds + - `x64-unicode`: For 64-bit NSIS Unicode builds + - `x86-ansi`: For 32-bit NSIS ANSI builds + +2. Include the plugin directory in your script: + ```nsis + !addplugindir "path\to\plugins\x86-unicode" + ``` + +## Building from Source + +### Prerequisites +- Visual Studio 2022 or later with C++ build tools +- Python 3.x + +### Build Steps +```bash +cd nsSplashPNG +python build_plugin.py # Builds all configurations +python build_plugin.py --configs x86-unicode # Build specific config +``` + +The script will: +- Build for x86-unicode, x64-unicode, and x86-ansi configurations +- Copy DLLs to `../plugins/{x86-unicode|amd64-unicode|x86-ansi}/` directories +- Clean up intermediate build artifacts from `src/Build/` +- Supports parallel builds with automatic CPU optimization + +## Usage + +### Important: Plugin Memory Management + +**CRITICAL**: Always use `/NOUNLOAD` flag on the first `show` call to keep the plugin loaded in memory. Without this, NSIS unloads the plugin after each call, losing all state (window handles, threads, etc.), causing `stop` to fail. + +```nsis +; Correct - keeps plugin loaded +nssplashpng::show /NOUNLOAD 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +nssplashpng::stop /FADEOUT + +; Wrong - plugin unloaded after show, stop will fail +nssplashpng::show 5000 /FADEIN /FADEOUT "$TEMP\splash.png" +nssplashpng::stop /FADEOUT +``` + +### Syntax + +```nsis +nsSplashPNG::show [/NOUNLOAD] [/FADEIN [step]] [/FADEOUT [step]] [/NOCANCEL] +nsSplashPNG::stop [/FADEOUT [step]] +``` + +**Note**: The timeout must be specified immediately after `/NOUNLOAD` (if present), followed by optional flags, with the image path as the last parameter. + +### Parameters + +#### show function +- `/NOUNLOAD` - **Required for stop() to work**: Keeps plugin in memory between calls +- `/FADEIN [step]` - Enable fade in effect. Optional step (1-255) controls speed (default: 15) + - Higher step = faster fade (255=instant, 51=fast, 15=normal, 5=slow) + - Duration formula: `(255 / step) × 30ms` +- `/FADEOUT [step]` - Enable fade out effect when auto-closing via timer (optional step) +- `/NOCANCEL` - Disable click-to-close (splash can only be closed programmatically or by timer) +- `` - Auto-close timer in milliseconds (0 = manual close only) +- `` - Path to image file (PNG, JPEG, BMP, GIF, TIFF, ICO) + +#### stop function +- `/FADEOUT [step]` - Enable fade out effect when closing. Optional step (1-255) controls speed (default: 15) + +### Examples + +#### Basic Usage +```nsis +; Show splash for 3 seconds (PNG with alpha) - no /NOUNLOAD needed for auto-close only +nsSplashPNG::show 3000 "$EXEDIR\splash.png" + +; Show JPEG splash (opaque, no transparency) +nsSplashPNG::show 3000 "$EXEDIR\splash.jpg" +``` + +#### Fade Effects +```nsis +; Default fade speed (step 15, ~510ms) +nsSplashPNG::show 3000 /FADEIN /FADEOUT "$EXEDIR\splash.png" + +; Fast fade (step 51, ~150ms) +nsSplashPNG::show 3000 /FADEIN 51 /FADEOUT 51 "$EXEDIR\splash.png" + +; Slow fade (step 5, ~1530ms) +nsSplashPNG::show 5000 /FADEIN 5 /FADEOUT 5 "$EXEDIR\splash.png" + +; Instant (step 255, ~30ms) +nsSplashPNG::show 3000 /FADEIN 255 /FADEOUT 255 "$EXEDIR\splash.png" + +; Different speeds for fade-in and fade-out +nsSplashPNG::show 3000 /FADEIN 51 /FADEOUT 5 "$EXEDIR\splash.png" +``` + +#### Manual Control +```nsis +; CRITICAL: Use /NOUNLOAD when using stop() to close manually +nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL "$EXEDIR\splash.png" + +; ... do installation work ... + +; Close with fade out +nsSplashPNG::stop /FADEOUT + +; Or close with custom fade speed +nsSplashPNG::stop /FADEOUT 51 +``` + +#### Real Installer Example +```nsis +Name "My Application" +OutFile "setup.exe" +InstallDir "$PROGRAMFILES\MyApp" + +!addplugindir "plugins\x86-unicode" + +Page instfiles + +Section "Main Installation" + ; Show splash with fade in, prevent user closing, no auto-close + ; CRITICAL: /NOUNLOAD required for stop() to work + nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL /FADEIN "$EXEDIR\splash.png" + + ; Install files + SetOutPath "$INSTDIR" + File /r "app\*.*" + + ; Create shortcuts + CreateDirectory "$SMPROGRAMS\MyApp" + CreateShortcut "$SMPROGRAMS\MyApp\MyApp.lnk" "$INSTDIR\myapp.exe" + + ; Close splash with fade out + nsSplashPNG::stop /FADEOUT +SectionEnd +``` + +#### Additional Examples + +See the `examples/` folder for more usage demonstrations: +- **example_basic.nsi** - Simple splash with auto-close timer +- **example_advanced.nsi** - Manual control with fade effects +- **example_formats.nsi** - Multi-format support (PNG, JPEG, BMP, GIF) +- **example_fade_speed.nsi** - Customizable fade speed demonstrations +- **example_nonblocking.nsi** - Non-blocking behavior with background processing + +## Technical Details + +### Implementation +- **Graphics Library**: WIC (Windows Imaging Component) via windowscodecs.lib +- **Image Format**: 32bppPBGRA (pre-multiplied alpha) for proper transparency +- **Window Type**: Layered windows with `WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW` +- **Transparency**: `UpdateLayeredWindow` with per-pixel alpha blending +- **Rendering**: Hardware-accelerated via `UpdateLayeredWindow` with `ULW_ALPHA` +- **Animation**: 30ms timer intervals for smooth fade (0-255 alpha range) + - Configurable step size (default: 15 steps = ~510ms duration) + - Duration formula: `(255 / step) × 30ms` +- **Threading**: Background window thread for non-blocking execution + - `show()` returns after fade-in completes + - Window stays open in background + - `stop()` waits for fade-out to complete before returning +- **Plugin State**: Requires `/NOUNLOAD` to maintain global variables (window handles, threads) between calls + +### Comparison with nsAdvsplash + +| Feature | nsAdvsplash | nsSplashPNG | +|----------------------|-------------|-----------------| +| PNG Alpha Support | Partial | Full | +| Fade Effects | Yes | Yes | +| Transparency Method | Color key | Per-pixel alpha | +| Graphics API | GDI | WIC | +| Window Type | Standard | Layered | +| Click-to-Close | Yes | Yes | +| Compilation Warnings | Some | None | + +### Supported Image Formats + +WIC automatically detects and decodes the following formats: +- **PNG** - Full alpha transparency support +- **JPEG/JPG** - No transparency (opaque) +- **BMP** - Alpha transparency if 32-bit, otherwise opaque +- **GIF** - Basic transparency support +- **TIFF** - Alpha transparency if supported by variant +- **ICO** - Icon format with alpha +- **WMP** - Windows Media Photo + +**Note**: Formats without alpha channels (JPEG, 24-bit BMP) will display as opaque images. + +### System Requirements +- Windows Vista or later (for layered window support) +- WIC - Windows Imaging Component (included with Windows Vista+) + +## API Reference + +### Exported Functions + +#### `show()` +Creates and displays a splash screen window in a background thread. + +**Parameters** (parsed from NSIS stack in reverse order): +1. Timeout in milliseconds (integer, 0 = manual close only) +2. `/FADEIN [step]` - Optional fade in with configurable step (1-255, default: 15) +3. `/FADEOUT [step]` - Optional auto fade out with configurable step (1-255, default: 15) +4. `/NOCANCEL` - Disable click-to-close +5. `/NOUNLOAD` - **Required for stop() to work**: Keeps plugin in memory +6. Image path (string) - last parameter + +**Behavior**: +- Cleans up any previous window/thread +- Loads image with WIC (`IWICImagingFactory`, `IWICBitmapDecoder`) +- Supports PNG, JPEG, BMP, GIF, TIFF, ICO, WMP formats +- Converts to 32bppPBGRA format for proper alpha handling +- Creates centered topmost layered window +- Uses `UpdateLayeredWindow` for per-pixel alpha transparency +- Starts background thread with window message loop +- Applies fade in effect if `/FADEIN` specified (30ms timer with configurable steps) +- Sets auto-close timer if timeout > 0 +- Enables click-to-close unless `/NOCANCEL` specified +- **Returns control after fade-in completes** (non-blocking) +- Without `/NOUNLOAD`, plugin unloads and loses all state + +**Return value**: Pushes "success" to NSIS stack + +#### `stop()` +Closes the splash screen window with optional fade-out. + +**Parameters** (parsed from NSIS stack): +1. `/FADEOUT [step]` - Optional fade out with configurable step (1-255, default: 15) + +**Behavior**: +- If `/FADEOUT` specified: + - Starts fade-out timer (30ms intervals) + - Waits for fade to complete: `(255 / step) × 30ms` + - Destroys window after fade completes +- Otherwise: + - Immediately destroys window +- **Requires show() to have used /NOUNLOAD** or window handle will be NULL + +**Return value**: Pushes "success" to NSIS stack + +**Behavior**: +- Applies fade out effect if `/FADEOUT` specified (30ms timer, 15 steps) +- Destroys window and releases WIC resources +- Terminates splash thread and cleans up handles + +## Troubleshooting + +### Image not showing +- Verify image file path is correct and accessible +- Check that image file is in a supported format (PNG, JPEG, BMP, GIF, TIFF, ICO) +- Ensure NSIS can read the file at install time +- Verify WIC can decode the specific image file variant + +### No transparency +- Confirm PNG has alpha channel (not just RGB) +- Verify you're using the correct plugin DLL for your NSIS version +- Check Windows version supports layered windows (Vista+) + +### Fade effects not smooth +- Normal behavior - fade uses 30ms timer with 15-step alpha increments +- Total fade duration ≈ 450-500ms + +## License + +Created for NSIS installer system. + +## Credits + +- NSIS Plugin API +- WIC - Windows Imaging Component +- Windows Layered Windows API (`UpdateLayeredWindow`) +- Build system inspired by nsProcess plugin + +## Version History + +### 1.0.0 (Current) +- Full PNG alpha transparency support via WIC +- Per-pixel alpha blending with `UpdateLayeredWindow` +- Fade in/out effects (30ms, 15 steps) +- Auto-close timer with configurable timeout +- Click-to-close with optional disable (`/NOCANCEL`) +- Blocks installer until splash closes +- x86-unicode, x64-unicode, and x86-ansi builds +- Zero compilation warnings with guarded macro definitions +- Optimized build system with parallel compilation support diff --git a/examples/example_advanced.nsi b/examples/example_advanced.nsi new file mode 100644 index 0000000..961fd46 --- /dev/null +++ b/examples/example_advanced.nsi @@ -0,0 +1,47 @@ +; nsSplashPNG Advanced Example +; Shows how to use nsSplashPNG in a real installer with progress updates + +Name "nsSplashPNG Advanced Test" +OutFile "nsSplashPNG_Advanced.exe" +InstallDir "$PROGRAMFILES\TestApp" +RequestExecutionLevel admin + +!addplugindir "..\plugins\x86-unicode" + +Page instfiles + +Section "Main Installation" + ; Copy splash image to temp directory + SetOutPath $TEMP + File "images\splash.png" + + ; Show splash with fade in, no cancel button + ; CRITICAL: /NOUNLOAD required since we call stop() later + nsSplashPNG::show /NOUNLOAD 0 /NOCANCEL /FADEIN "$TEMP\splash.png" + + ; Simulate installation steps + DetailPrint "Installing files..." + SetOutPath "$INSTDIR" + Sleep 1000 + + DetailPrint "Configuring application..." + Sleep 1000 + + DetailPrint "Creating shortcuts..." + Sleep 1000 + + DetailPrint "Finalizing installation..." + Sleep 1000 + + ; Close splash with fade out + nsSplashPNG::stop /FADEOUT + + ; Clean up + Delete "$TEMP\splash.png" + + DetailPrint "Installation complete!" +SectionEnd + +Section "Uninstall" + RMDir /r "$INSTDIR" +SectionEnd diff --git a/examples/example_basic.nsi b/examples/example_basic.nsi new file mode 100644 index 0000000..878881c --- /dev/null +++ b/examples/example_basic.nsi @@ -0,0 +1,39 @@ +; nsSplashPNG Example Script +; Demonstrates the usage of the nsSplashPNG plugin with various options + +Name "nsSplashPNG Test" +OutFile "nsSplashPNG_Example.exe" +SilentInstall silent +RequestExecutionLevel user + +!addplugindir "..\plugins\x86-unicode" + +Section + ; Copy splash image to temp directory + SetOutPath $TEMP + File "images\splash.png" + + ; Example 1: Simple splash with auto-close after 3 seconds + ; nsSplashPNG::show 3000 "test_image.png" + + ; Example 2: Splash with fade in effect, auto-close after 3 seconds + ; nsSplashPNG::show /FADEIN 3000 "test_image.png" + + ; Example 3: Splash with fade out effect, auto-close after 3 seconds + ; nsSplashPNG::show /FADEOUT 3000 "test_image.png" + + ; Example 4: Splash with fade in, manual close (no auto-close) + ; CRITICAL: /NOUNLOAD required since we call stop() later + nsSplashPNG::show /NOUNLOAD 0 /FADEIN "$TEMP\splash.png" + + ; Simulate some installation work + Sleep 2000 + + ; Example 5: Manual close with fade out + nsSplashPNG::stop /FADEOUT + + ; Clean up + Delete "$TEMP\splash.png" + + MessageBox MB_OK "Installation complete!" +SectionEnd diff --git a/examples/example_fade_speed.nsi b/examples/example_fade_speed.nsi new file mode 100644 index 0000000..303d63b --- /dev/null +++ b/examples/example_fade_speed.nsi @@ -0,0 +1,50 @@ +; Test fade speed customization +Unicode true +!addplugindir "N:\Code\Workspace\Launchers\plugins\x86-unicode" + +Name "Fade Speed Test" +OutFile "test_fade_speed.exe" +SilentInstall silent +RequestExecutionLevel user + +Function .onInit + SetOutPath $TEMP + File "images\splash.png" + + ; Test 1: Default speed (step 15, ~510ms fade) + MessageBox MB_OK "Test 1: Default fade speed (step 15)$\n$\nFade duration: ~510ms" + nssplashpng::show /NOUNLOAD 60000 /FADEIN "$TEMP\splash.png" + Pop $0 + Sleep 3000 + nssplashpng::stop /FADEOUT + ; No need to sleep - stop now waits for fade-out internally + + ; Test 2: Fast fade (step 51, ~150ms fade) + MessageBox MB_OK "Test 2: Fast fade (step 51)$\n$\nFade duration: ~150ms" + nssplashpng::show /NOUNLOAD 60000 /FADEIN 51 "$TEMP\splash.png" + Pop $0 + Sleep 3000 + nssplashpng::stop /FADEOUT 51 + ; No need to sleep - stop now waits for fade-out internally + + ; Test 3: Slow fade (step 5, ~1530ms fade) + MessageBox MB_OK "Test 3: Slow fade (step 5)$\n$\nFade duration: ~1530ms" + nssplashpng::show /NOUNLOAD 60000 /FADEIN 5 "$TEMP\splash.png" + Pop $0 + Sleep 5000 + nssplashpng::stop /FADEOUT 5 + ; No need to sleep - stop now waits for fade-out internally + + ; Test 4: Instant (step 255, ~30ms fade) + MessageBox MB_OK "Test 4: Instant fade (step 255)$\n$\nFade duration: ~30ms" + nssplashpng::show /NOUNLOAD 60000 /FADEIN 255 "$TEMP\splash.png" + Pop $0 + Sleep 2000 + nssplashpng::stop /FADEOUT 255 + ; No need to sleep - stop now waits for fade-out internally Delete "$TEMP\splash.png" + MessageBox MB_OK "All fade speed tests completed!" + Quit +FunctionEnd + +Section +SectionEnd diff --git a/examples/example_formats.nsi b/examples/example_formats.nsi new file mode 100644 index 0000000..aba4ea5 --- /dev/null +++ b/examples/example_formats.nsi @@ -0,0 +1,43 @@ +; Test different image formats with nsSplashPNG +Unicode true + +Name "Format Test" +OutFile "test_formats.exe" +InstallDir "$TEMP\test" + +!addplugindir "N:\Code\Workspace\Launchers\plugins\x86-unicode" + +Section + SetOutPath $TEMP + File "images\splash.png" + File "images\splash.jpg" + File "images\splash.bmp" + File "images\splash.gif" + + ; Test PNG (with alpha) + MessageBox MB_OK "Testing PNG format (with alpha transparency)" + nssplashpng::show /NOUNLOAD 2000 /FADEIN /FADEOUT "$TEMP\splash.png" + Pop $0 + + ; Test JPEG (no alpha) + MessageBox MB_OK "Testing JPEG format (no transparency)" + nssplashpng::show /NOUNLOAD 2000 /FADEIN /FADEOUT "$TEMP\splash.jpg" + Pop $0 + + ; Test BMP (no alpha) + MessageBox MB_OK "Testing BMP format (no transparency)" + nssplashpng::show /NOUNLOAD 2000 /FADEIN /FADEOUT "$TEMP\splash.bmp" + Pop $0 + + ; Test GIF (with transparency) + MessageBox MB_OK "Testing GIF format (basic transparency)" + nssplashpng::show /NOUNLOAD 2000 /FADEIN /FADEOUT "$TEMP\splash.gif" + Pop $0 + + Delete "$TEMP\splash.png" + Delete "$TEMP\splash.jpg" + Delete "$TEMP\splash.bmp" + Delete "$TEMP\splash.gif" + + MessageBox MB_OK "All format tests completed!" +SectionEnd diff --git a/examples/example_nonblocking.nsi b/examples/example_nonblocking.nsi new file mode 100644 index 0000000..77bedbb --- /dev/null +++ b/examples/example_nonblocking.nsi @@ -0,0 +1,34 @@ +; Test non-blocking behavior +Unicode true +!addplugindir "N:\Code\Workspace\Launchers\plugins\x86-unicode" + +Name "Non-Blocking Test" +OutFile "test_nonblocking.exe" +SilentInstall silent +RequestExecutionLevel user + +Function .onInit + SetOutPath $TEMP + File "images\splash.png" + + ; Show splash with fade-in, manual close (0 timeout) + ; CRITICAL: /NOUNLOAD required since we call stop() later + MessageBox MB_OK "Showing splash with fade-in. Script should continue immediately after fade-in completes." + nssplashpng::show /NOUNLOAD 0 /FADEIN "$TEMP\splash.png" + Pop $0 + + ; This should appear right after fade-in, while splash is still visible + MessageBox MB_OK "Splash is visible! Result: $0$\n$\nNow doing some work..." + Sleep 2000 + + MessageBox MB_OK "Work done. Now closing splash with fade-out..." + nssplashpng::stop /FADEOUT + Pop $0 + + MessageBox MB_OK "Splash closed! Stop result: $0" + Delete "$TEMP\splash.png" + Quit +FunctionEnd + +Section +SectionEnd diff --git a/examples/images/splash.bmp b/examples/images/splash.bmp new file mode 100644 index 0000000..32c2bee Binary files /dev/null and b/examples/images/splash.bmp differ diff --git a/examples/images/splash.gif b/examples/images/splash.gif new file mode 100644 index 0000000..7172f89 Binary files /dev/null and b/examples/images/splash.gif differ diff --git a/examples/images/splash.jpg b/examples/images/splash.jpg new file mode 100644 index 0000000..89d95d2 Binary files /dev/null and b/examples/images/splash.jpg differ diff --git a/examples/images/splash.png b/examples/images/splash.png new file mode 100644 index 0000000..fd530ce Binary files /dev/null and b/examples/images/splash.png differ diff --git a/src/SplashPNG.cpp b/src/SplashPNG.cpp new file mode 100644 index 0000000..b1cce27 --- /dev/null +++ b/src/SplashPNG.cpp @@ -0,0 +1,553 @@ +// nsSplashPNG - PNG Splash Screen Plugin for NSIS +// Full PNG alpha transparency support using WIC and layered windows + +#define _WIN32_WINNT 0x0500 +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include +#include +#include "pluginapi.h" + +#pragma comment(lib, "windowscodecs.lib") +#pragma comment(lib, "ole32.lib") + +// Constants +#define RESOLUTION 30 // 30ms timer for smooth animation +#define FADEIN_DURATION 500 // 500ms fade in (255 / 15 * 30ms ≈ 510ms) +#define FADEOUT_DURATION 500 // 500ms fade out +#define CLASS_NAME _T("_nsSplashPNG") + +// Global variables +HWND g_hWnd = NULL; +HINSTANCE g_hInstance = NULL; +HBITMAP g_hBitmap = NULL; +HDC g_hdcMem = NULL; +int g_imageWidth = 0; +int g_imageHeight = 0; +int g_timeoutMs = 0; +int g_fadeAlpha = 0; +bool g_fadeIn = false; +bool g_fadeOut = false; +bool g_noCancel = false; +int g_fadeInStep = 15; // Default: 15 alpha units per timer tick +int g_fadeOutStep = 15; // Default: 15 alpha units per timer tick +HANDLE g_hThread = NULL; +HANDLE g_hEvent = NULL; + +// Monitor selection +#define MONITOR_MODE_PRIMARY 0 // Primary monitor (default) +#define MONITOR_MODE_MOUSE 1 // Monitor where mouse cursor is +#define MONITOR_MODE_INDEX 2 // Monitor by 1-based index +#define MONITOR_MODE_POINT 3 // Monitor nearest to a specific screen point +int g_monitorMode = MONITOR_MODE_PRIMARY; +int g_monitorIndex = 0; // 0-based index used internally +int g_monitorX = 0; // Screen X coordinate for MONITOR_MODE_POINT +int g_monitorY = 0; // Screen Y coordinate for MONITOR_MODE_POINT + +struct MonitorEnumData { + int targetIndex; + int currentIndex; + HMONITOR hMonitor; + MONITORINFO info; +}; + +static BOOL CALLBACK GetMonitorByIndexProc(HMONITOR hMonitor, HDC, LPRECT, LPARAM dwData) { + MonitorEnumData* pData = (MonitorEnumData*)dwData; + if (pData->currentIndex == pData->targetIndex) { + pData->hMonitor = hMonitor; + pData->info.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(hMonitor, &pData->info); + return FALSE; // Stop enumeration + } + pData->currentIndex++; + return TRUE; +} + +typedef BOOL (WINAPI *SetLayeredWindowAttributesProc)(HWND, COLORREF, BYTE, DWORD); +SetLayeredWindowAttributesProc g_SetLayeredWindowAttributes = NULL; + +// Window procedure +LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { + switch (uMsg) { + case WM_CREATE: + return 0; + + case WM_PAINT: { + PAINTSTRUCT ps; + BeginPaint(hwnd, &ps); + // Don't draw anything - layered window uses UpdateLayeredWindow + EndPaint(hwnd, &ps); + return 0; + } + + case WM_LBUTTONDOWN: + if (g_noCancel) + return 0; + // Intentional fallthrough to close splash on click + DestroyWindow(hwnd); + return 0; + + case WM_TIMER: + { + if (wParam == 1) { // Fade in timer + if (g_fadeAlpha < 255) { + g_fadeAlpha += g_fadeInStep; + if (g_fadeAlpha > 255) g_fadeAlpha = 255; + + // Update alpha using UpdateLayeredWindow + if (g_hBitmap && g_hdcMem) { + HDC hdcScreen = GetDC(NULL); + POINT ptSrc = {0, 0}; + SIZE sizeWnd = {g_imageWidth, g_imageHeight}; + BLENDFUNCTION blend = {AC_SRC_OVER, 0, (BYTE)g_fadeAlpha, AC_SRC_ALPHA}; + UpdateLayeredWindow(hwnd, hdcScreen, NULL, &sizeWnd, g_hdcMem, &ptSrc, 0, &blend, ULW_ALPHA); + ReleaseDC(NULL, hdcScreen); + } + } else { + KillTimer(hwnd, 1); + } + } else if (wParam == 2) { // Close timer + KillTimer(hwnd, 2); + if (g_fadeOut) { + // Start fade out + SetTimer(hwnd, 3, RESOLUTION, NULL); + } else { + DestroyWindow(hwnd); + } + } else if (wParam == 3) { // Fade out timer + if (g_fadeAlpha > 0) { + g_fadeAlpha -= g_fadeOutStep; + if (g_fadeAlpha < 0) g_fadeAlpha = 0; + + // Update alpha using UpdateLayeredWindow + if (g_hBitmap && g_hdcMem) { + HDC hdcScreen = GetDC(NULL); + POINT ptSrc = {0, 0}; + SIZE sizeWnd = {g_imageWidth, g_imageHeight}; + BLENDFUNCTION blend = {AC_SRC_OVER, 0, (BYTE)g_fadeAlpha, AC_SRC_ALPHA}; + UpdateLayeredWindow(hwnd, hdcScreen, NULL, &sizeWnd, g_hdcMem, &ptSrc, 0, &blend, ULW_ALPHA); + ReleaseDC(NULL, hdcScreen); + } + } else { + KillTimer(hwnd, 3); + DestroyWindow(hwnd); + } + } + return 0; + } + + case WM_CLOSE: + return 0; + + case WM_DESTROY: + { + if (g_hdcMem) { + DeleteDC(g_hdcMem); + g_hdcMem = NULL; + } + if (g_hBitmap) { + DeleteObject(g_hBitmap); + g_hBitmap = NULL; + } + g_hWnd = NULL; + PostQuitMessage(0); + return 0; + } + } + + return DefWindowProc(hwnd, uMsg, wParam, lParam); +} + +// DLL entry point +BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { + if (dwReason == DLL_PROCESS_ATTACH) { + g_hInstance = hInstance; + // This is acceptable for NSIS plugins which are loaded/unloaded by the installer process + } + return TRUE; +} + +// Thread function to create and manage the splash window +DWORD WINAPI SplashThreadProc(LPVOID lpParam) { + // Register window class + WNDCLASS wc = {0}; + wc.lpfnWndProc = WndProc; + wc.hInstance = g_hInstance; + wc.lpszClassName = CLASS_NAME; + wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); + RegisterClass(&wc); + + // Calculate centered position on the target monitor + RECT monitorRect = {0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)}; + if (g_monitorMode == MONITOR_MODE_MOUSE) { + POINT pt; + if (GetCursorPos(&pt)) { + HMONITOR hMon = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = {sizeof(mi)}; + if (GetMonitorInfo(hMon, &mi)) + monitorRect = mi.rcMonitor; + } + } else if (g_monitorMode == MONITOR_MODE_INDEX) { + MonitorEnumData data = {}; + data.targetIndex = g_monitorIndex; + data.info.cbSize = sizeof(MONITORINFO); + EnumDisplayMonitors(NULL, NULL, GetMonitorByIndexProc, (LPARAM)&data); + if (data.hMonitor) + monitorRect = data.info.rcMonitor; + } else if (g_monitorMode == MONITOR_MODE_POINT) { + POINT pt = {g_monitorX, g_monitorY}; + HMONITOR hMon = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST); + MONITORINFO mi = {sizeof(mi)}; + if (GetMonitorInfo(hMon, &mi)) + monitorRect = mi.rcMonitor; + } + int x = monitorRect.left + (monitorRect.right - monitorRect.left - g_imageWidth) / 2; + int y = monitorRect.top + (monitorRect.bottom - monitorRect.top - g_imageHeight) / 2; + + // Create layered window + g_hWnd = CreateWindowEx( + WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW, + CLASS_NAME, + _T(""), + WS_POPUP, + x, y, g_imageWidth, g_imageHeight, + NULL, + NULL, + g_hInstance, + NULL + ); + + if (!g_hWnd) { + SetEvent(g_hEvent); + return 1; + } + + // Display using UpdateLayeredWindow + g_fadeAlpha = g_fadeIn ? 0 : 255; + + HDC hdcScreen = GetDC(NULL); + POINT ptSrc = {0, 0}; + SIZE sizeWnd = {g_imageWidth, g_imageHeight}; + BLENDFUNCTION blend = {AC_SRC_OVER, 0, (BYTE)g_fadeAlpha, AC_SRC_ALPHA}; + UpdateLayeredWindow(g_hWnd, hdcScreen, NULL, &sizeWnd, g_hdcMem, &ptSrc, 0, &blend, ULW_ALPHA); + ReleaseDC(NULL, hdcScreen); + + ShowWindow(g_hWnd, SW_SHOW); + SetForegroundWindow(g_hWnd); + + // Start timers + if (g_fadeIn) { + SetTimer(g_hWnd, 1, RESOLUTION, NULL); + } + if (g_timeoutMs > 0) { + SetTimer(g_hWnd, 2, g_timeoutMs, NULL); + } + + // Signal that window is created + SetEvent(g_hEvent); + + // Message loop + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + // Cleanup + UnregisterClass(CLASS_NAME, g_hInstance); + return 0; +} + +// Load PNG using Windows Imaging Component (WIC) +HBITMAP LoadPNGWithWIC(const TCHAR* filename, int* width, int* height) { + + HRESULT hr; + IWICImagingFactory* pFactory = NULL; + IWICBitmapDecoder* pDecoder = NULL; + IWICBitmapFrameDecode* pFrame = NULL; + IWICFormatConverter* pConverter = NULL; + HBITMAP hBitmap = NULL; + + // Initialize COM (don't check failure - might already be initialized by NSIS) + CoInitialize(NULL); + + hr = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, (LPVOID*)&pFactory); + if (FAILED(hr)) { + return NULL; + } + + hr = pFactory->CreateDecoderFromFilename((LPCWSTR)filename, NULL, GENERIC_READ, + WICDecodeMetadataCacheOnLoad, &pDecoder); + if (FAILED(hr)) goto cleanup; + + hr = pDecoder->GetFrame(0, &pFrame); + if (FAILED(hr)) goto cleanup; + + UINT imgWidth, imgHeight; + pFrame->GetSize(&imgWidth, &imgHeight); + *width = imgWidth; + *height = imgHeight; + + hr = pFactory->CreateFormatConverter(&pConverter); + if (FAILED(hr)) goto cleanup; + + hr = pConverter->Initialize(pFrame, GUID_WICPixelFormat32bppPBGRA, + WICBitmapDitherTypeNone, NULL, 0.0, + WICBitmapPaletteTypeMedianCut); + if (FAILED(hr)) goto cleanup; + + // Declare variables before goto + BITMAPINFO bmi; + ZeroMemory(&bmi, sizeof(bmi)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = imgWidth; + bmi.bmiHeader.biHeight = -(int)imgHeight; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + void* pBits; + pBits = NULL; + HDC hdc; + hdc = GetDC(NULL); + hBitmap = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &pBits, NULL, 0); + ReleaseDC(NULL, hdc); + + if (!hBitmap) goto cleanup; + + hr = pConverter->CopyPixels(NULL, imgWidth * 4, imgWidth * imgHeight * 4, (BYTE*)pBits); + if (FAILED(hr)) { + DeleteObject(hBitmap); + hBitmap = NULL; + } + +cleanup: + if (pConverter) pConverter->Release(); + if (pFrame) pFrame->Release(); + if (pDecoder) pDecoder->Release(); + if (pFactory) pFactory->Release(); + // Don't call CoUninitialize - let NSIS handle COM cleanup + // CoUninitialize(); + + return hBitmap; +} + +// Plugin exported function: show +extern "C" void __declspec(dllexport) show(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) { + EXDLL_INIT(); + + // Close any existing window and thread first + if (g_hWnd && IsWindow(g_hWnd)) { + DestroyWindow(g_hWnd); + // Give thread time to process WM_DESTROY and exit message loop + Sleep(200); + } + + // Wait for previous thread to terminate if it exists + if (g_hThread) { + // Wait briefly for thread to terminate + WaitForSingleObject(g_hThread, 500); + CloseHandle(g_hThread); + g_hThread = NULL; + } + + // Ensure window handle is cleared + g_hWnd = NULL; + + // Parameters come from stack in order: timeout, flags..., imagepath + // Read timeout first + TCHAR param[MAX_PATH]; + TCHAR imagePath[MAX_PATH] = {0}; + + popstring(param); + g_timeoutMs = myatoi(param); + + // Reset flags + g_fadeIn = false; + g_fadeOut = false; + g_noCancel = false; + g_fadeInStep = 15; // Default step + g_fadeOutStep = 15; // Default step + g_monitorMode = MONITOR_MODE_PRIMARY; + g_monitorIndex = 0; + g_monitorX = 0; + g_monitorY = 0; + + // Read flags and image path + while (popstring(param) == 0 && param[0] == _T('/')) { + if (_tcsicmp(param, _T("/FADEIN")) == 0) { + g_fadeIn = true; + // Check if next parameter is a number (custom step) + TCHAR nextParam[MAX_PATH]; + if (popstring(nextParam) == 0 && nextParam[0] != _T('/')) { + int customStep = myatoi(nextParam); + if (customStep > 0 && customStep <= 255) { + g_fadeInStep = customStep; + } else { + // Not a valid step, push it back (it's probably the image path) + lstrcpy(param, nextParam); + break; + } + } else { + // Push it back for next iteration + lstrcpy(param, nextParam); + } + } else if (_tcsicmp(param, _T("/FADEOUT")) == 0) { + g_fadeOut = true; + // Check if next parameter is a number (custom step) + TCHAR nextParam[MAX_PATH]; + if (popstring(nextParam) == 0 && nextParam[0] != _T('/')) { + int customStep = myatoi(nextParam); + if (customStep > 0 && customStep <= 255) { + g_fadeOutStep = customStep; + } else { + // Not a valid step, push it back + lstrcpy(param, nextParam); + break; + } + } else { + // Push it back for next iteration + lstrcpy(param, nextParam); + } + } else if (_tcsicmp(param, _T("/NOCANCEL")) == 0) { + g_noCancel = true; + } else if (_tcsicmp(param, _T("/MONITOR")) == 0) { + TCHAR monParam[64] = {0}; + if (popstring(monParam) == 0) { + if (_tcsicmp(monParam, _T("CURRENT")) == 0 || _tcsicmp(monParam, _T("MOUSE")) == 0) { + g_monitorMode = MONITOR_MODE_MOUSE; + } else if (_tcsicmp(monParam, _T("PRIMARY")) == 0) { + g_monitorMode = MONITOR_MODE_PRIMARY; + } else if (_tcsicmp(monParam, _T("POINT")) == 0) { + TCHAR xParam[64] = {0}, yParam[64] = {0}; + if (popstring(xParam) == 0 && popstring(yParam) == 0) { + g_monitorMode = MONITOR_MODE_POINT; + g_monitorX = myatoi(xParam); + g_monitorY = myatoi(yParam); + } + } else { + int idx = myatoi(monParam); + if (idx >= 1) { + g_monitorMode = MONITOR_MODE_INDEX; + g_monitorIndex = idx - 1; // Convert to 0-based + } + } + } + } + } + + // Last parameter read (not starting with /) is the image path + lstrcpy(imagePath, param); + + // Validate image path + if (imagePath[0] == 0) { + pushstring(_T("error: no image path")); + return; + } + + // Load PNG image with WIC + g_hBitmap = LoadPNGWithWIC(imagePath, &g_imageWidth, &g_imageHeight); + if (!g_hBitmap) { + pushstring(_T("error: cannot load image")); + return; + } + + // Create memory DC for the bitmap + HDC hdc = GetDC(NULL); + g_hdcMem = CreateCompatibleDC(hdc); + ReleaseDC(NULL, hdc); + SelectObject(g_hdcMem, g_hBitmap); + + // Create event for synchronization + g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (!g_hEvent) { + pushstring(_T("error: cannot create event")); + if (g_hdcMem) DeleteDC(g_hdcMem); + if (g_hBitmap) DeleteObject(g_hBitmap); + return; + } + + // Create thread to manage the window + g_hThread = CreateThread(NULL, 0, SplashThreadProc, NULL, 0, NULL); + if (!g_hThread) { + pushstring(_T("error: cannot create thread")); + CloseHandle(g_hEvent); + if (g_hdcMem) DeleteDC(g_hdcMem); + if (g_hBitmap) DeleteObject(g_hBitmap); + return; + } + + // Wait for window to be created (with timeout) + DWORD result = WaitForSingleObject(g_hEvent, 2000); + CloseHandle(g_hEvent); + g_hEvent = NULL; + + if (result != WAIT_OBJECT_0 || !g_hWnd) { + pushstring(_T("error: window not created")); + return; + } + + // If fade-in is enabled, wait for it to complete before returning + if (g_fadeIn) { + // Calculate fade-in duration: (255 / step) * 30ms + 100ms margin + int fadeSteps = (255 + g_fadeInStep - 1) / g_fadeInStep; // Round up + int fadeDuration = fadeSteps * RESOLUTION + 100; + Sleep(fadeDuration); + } + + // Return control to NSIS immediately - window stays open in background thread + // Window will close either: + // 1. When timeout expires (if g_timeoutMs > 0) + // 2. When user clicks (if !g_noCancel) + // 3. When stop() is called explicitly + pushstring(_T("success")); +} + +// Plugin exported function: stop +extern "C" void __declspec(dllexport) stop(HWND hwndParent, int string_size, TCHAR *variables, stack_t **stacktop) { + EXDLL_INIT(); + + // Parse flags + TCHAR param[256] = {0}; + bool fadeOut = false; + + while (popstring(param) == 0) { + if (_tcsicmp(param, _T("/FADEOUT")) == 0) { + fadeOut = true; + // Check if next parameter is a number (custom step) + TCHAR nextParam[MAX_PATH]; + if (popstring(nextParam) == 0 && nextParam[0] != _T('/')) { + int customStep = myatoi(nextParam); + if (customStep > 0 && customStep <= 255) { + g_fadeOutStep = customStep; + } + } + } + } + + if (g_hWnd && IsWindow(g_hWnd)) { + if (fadeOut) { + // Start fade out timer + SetTimer(g_hWnd, 3, RESOLUTION, NULL); + + // Calculate expected fade-out duration and wait + int fadeSteps = (255 + g_fadeOutStep - 1) / g_fadeOutStep; + int fadeDuration = fadeSteps * RESOLUTION + 200; + + Sleep(fadeDuration); + + // Give extra time for window destruction and thread cleanup + Sleep(100); + } else { + // No fade-out, destroy immediately + DestroyWindow(g_hWnd); + Sleep(100); // Give thread time to cleanup + } + } + + pushstring(_T("success")); +} diff --git a/src/api.h b/src/api.h new file mode 100644 index 0000000..85d41c4 --- /dev/null +++ b/src/api.h @@ -0,0 +1,83 @@ +/* + * apih + * + * This file is a part of NSIS. + * + * Copyright (C) 1999-2013 Nullsoft and Contributors + * + * Licensed under the zlib/libpng license (the "License"); + * you may not use this file except in compliance with the License. + * + * Licence details can be found in the file COPYING. + * + * This software is provided 'as-is', without any express or implied + * warranty. + */ + +#ifndef _NSIS_EXEHEAD_API_H_ +#define _NSIS_EXEHEAD_API_H_ + +// Starting with NSIS 2.42, you can check the version of the plugin API in exec_flags->plugin_api_version +// The format is 0xXXXXYYYY where X is the major version and Y is the minor version (MAKELONG(y,x)) +// When doing version checks, always remember to use >=, ex: if (pX->exec_flags->plugin_api_version >= NSISPIAPIVER_1_0) {} + +#define NSISPIAPIVER_1_0 0x00010000 +#define NSISPIAPIVER_CURR NSISPIAPIVER_1_0 + +// NSIS Plug-In Callback Messages +enum NSPIM +{ + NSPIM_UNLOAD, // This is the last message a plugin gets, do final cleanup + NSPIM_GUIUNLOAD, // Called after .onGUIEnd +}; + +// Prototype for callbacks registered with extra_parameters->RegisterPluginCallback() +// Return NULL for unknown messages +// Should always be __cdecl for future expansion possibilities +typedef UINT_PTR (*NSISPLUGINCALLBACK)(enum NSPIM); + +// extra_parameters data structures containing other interesting stuff +// but the stack, variables and HWND passed on to plug-ins. +typedef struct +{ + int autoclose; + int all_user_var; + int exec_error; + int abort; + int exec_reboot; // NSIS_SUPPORT_REBOOT + int reboot_called; // NSIS_SUPPORT_REBOOT + int XXX_cur_insttype; // depreacted + int plugin_api_version; // see NSISPIAPIVER_CURR + // used to be XXX_insttype_changed + int silent; // NSIS_CONFIG_SILENT_SUPPORT + int instdir_error; + int rtl; + int errlvl; + int alter_reg_view; + int status_update; +} exec_flags_t; + +#ifndef NSISCALL +# define NSISCALL __stdcall +#endif + +typedef struct { + exec_flags_t *exec_flags; + int (NSISCALL *ExecuteCodeSegment)(int, HWND); + void (NSISCALL *validate_filename)(TCHAR *); + int (NSISCALL *RegisterPluginCallback)(HMODULE, NSISPLUGINCALLBACK); // returns 0 on success, 1 if already registered and < 0 on errors +} extra_parameters; + +// Definitions for page showing plug-ins +// See Ui.c to understand better how they're used + +// sent to the outer window to tell it to go to the next inner window +#define WM_NOTIFY_OUTER_NEXT (WM_USER+0x8) + +// custom pages should send this message to let NSIS know they're ready +#define WM_NOTIFY_CUSTOM_READY (WM_USER+0xd) + +// sent as wParam with WM_NOTIFY_OUTER_NEXT when user cancels - heed its warning +#define NOTIFY_BYE_BYE 'x' + +#endif /* _PLUGIN_H_ */ diff --git a/src/nsSplashPNG.def b/src/nsSplashPNG.def new file mode 100644 index 0000000..8293bdb --- /dev/null +++ b/src/nsSplashPNG.def @@ -0,0 +1,4 @@ +LIBRARY nsSplashPNG +EXPORTS + show @1 + stop @2 diff --git a/src/nsSplashPNG.vcxproj b/src/nsSplashPNG.vcxproj new file mode 100644 index 0000000..413d9d7 --- /dev/null +++ b/src/nsSplashPNG.vcxproj @@ -0,0 +1,187 @@ + + + + + Release + Win32 + + + Release Unicode + Win32 + + + Release + x64 + + + Release Unicode + x64 + + + + 17.0 + {B8F3A9D1-4C5E-4F2A-9B1C-8D7E6F5A4B3C} + Win32Proj + nsSplashPNG + 10.0 + + + + DynamicLibrary + false + v143 + true + MultiByte + + + DynamicLibrary + false + v143 + true + Unicode + + + DynamicLibrary + false + v143 + true + MultiByte + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + + + + + + + + + + + Level3 + true + true + false + + WIN32;NDEBUG;_WINDOWS;_USRDLL;EXDLL_EXPORTS;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + true + MultiThreaded + MaxSpeed + + + Windows + true + true + false + + kernel32.lib;user32.lib;gdi32.lib;ole32.lib;vfw32.lib;winmm.lib;%(AdditionalDependencies) + nsSplashPNG.def + + + + + Level3 + true + true + false + + WIN32;NDEBUG;_WINDOWS;_USRDLL;EXDLL_EXPORTS;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + true + MultiThreaded + MaxSpeed + + + Windows + true + true + false + + kernel32.lib;user32.lib;gdi32.lib;ole32.lib;vfw32.lib;winmm.lib;%(AdditionalDependencies) + nsSplashPNG.def + + + + + Level3 + true + true + false + + NDEBUG;_WINDOWS;_USRDLL;EXDLL_EXPORTS;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + true + MultiThreaded + MaxSpeed + + + Windows + true + true + false + + kernel32.lib;user32.lib;gdi32.lib;ole32.lib;vfw32.lib;winmm.lib;%(AdditionalDependencies) + nsSplashPNG.def + + + + + Level3 + true + true + false + + NDEBUG;_WINDOWS;_USRDLL;EXDLL_EXPORTS;UNICODE;_UNICODE;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + true + MultiThreaded + MaxSpeed + + + Windows + true + true + false + + windowscodecs.lib;ole32.lib;kernel32.lib;user32.lib;gdi32.lib;msimg32.lib;%(AdditionalDependencies) + nsSplashPNG.def + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/nsis_tchar.h b/src/nsis_tchar.h new file mode 100644 index 0000000..c818356 --- /dev/null +++ b/src/nsis_tchar.h @@ -0,0 +1,243 @@ +/* + * nsis_tchar.h + * + * This file is a part of NSIS. + * + * Copyright (C) 1999-2013 Nullsoft and Contributors + * + * This software is provided 'as-is', without any express or implied + * warranty. + * + * For Unicode support by Jim Park -- 08/30/2007 + */ + +// Jim Park: Only those we use are listed here. + +#pragma once + +#ifdef _UNICODE + +#ifndef _T +#define __T(x) L ## x +#define _T(x) __T(x) +#define _TEXT(x) __T(x) +#endif + +#ifndef _TCHAR_DEFINED +#define _TCHAR_DEFINED +#if !defined(_NATIVE_WCHAR_T_DEFINED) && !defined(_WCHAR_T_DEFINED) +typedef unsigned short TCHAR; +#else +typedef wchar_t TCHAR; +#endif +#endif + + +// program +#define _tenviron _wenviron +#define __targv __wargv + +// printfs +#define _ftprintf fwprintf +#define _sntprintf _snwprintf +#if (defined(_MSC_VER) && (_MSC_VER<=1310)) || defined(__MINGW32__) +# define _stprintf swprintf +#else +# define _stprintf _swprintf +#endif +#define _tprintf wprintf +#define _vftprintf vfwprintf +#define _vsntprintf _vsnwprintf +#if defined(_MSC_VER) && (_MSC_VER<=1310) +# define _vstprintf vswprintf +#else +# ifndef _vstprintf +# define _vstprintf _vswprintf +# endif +#endif + +// scanfs +#define _tscanf wscanf +#define _stscanf swscanf + +// string manipulations +#define _tcscat wcscat +#define _tcschr wcschr +#define _tcsclen wcslen +#define _tcscpy wcscpy +#define _tcsdup _wcsdup +#define _tcslen wcslen +#define _tcsnccpy wcsncpy +#define _tcsncpy wcsncpy +#define _tcsrchr wcsrchr +#define _tcsstr wcsstr +#ifndef _tcstok +#define _tcstok wcstok +#endif + +// string comparisons +#define _tcscmp wcscmp +#define _tcsicmp _wcsicmp +#define _tcsncicmp _wcsnicmp +#define _tcsncmp wcsncmp +#define _tcsnicmp _wcsnicmp + +// upper / lower +#define _tcslwr _wcslwr +#define _tcsupr _wcsupr +#define _totlower towlower +#define _totupper towupper + +// conversions to numbers +#define _tcstoi64 _wcstoi64 +#define _tcstol wcstol +#define _tcstoul wcstoul +#define _tstof _wtof +#define _tstoi _wtoi +#define _tstoi64 _wtoi64 +#define _ttoi _wtoi +#define _ttoi64 _wtoi64 +#define _ttol _wtol + +// conversion from numbers to strings +#define _itot _itow +#define _ltot _ltow +#define _i64tot _i64tow +#define _ui64tot _ui64tow + +// file manipulations +#define _tfopen _wfopen +#define _topen _wopen +#define _tremove _wremove +#define _tunlink _wunlink + +// reading and writing to i/o +#define _fgettc fgetwc +#define _fgetts fgetws +#define _fputts fputws +#define _gettchar getwchar + +// directory +#define _tchdir _wchdir + +// environment +#define _tgetenv _wgetenv +#define _tsystem _wsystem + +// time +#define _tcsftime wcsftime + +#else // ANSI + +#ifndef _T +#define _T(x) x +#define _TEXT(x) x +#endif + +#ifndef _TCHAR_DEFINED +#define _TCHAR_DEFINED +typedef char TCHAR; +#endif + +// program +#ifndef _tenviron +#define _tenviron environ +#endif +#define __targv __argv + +// printfs +#define _ftprintf fprintf +#define _sntprintf _snprintf +#define _stprintf sprintf +#define _tprintf printf +#define _vftprintf vfprintf +#define _vsntprintf _vsnprintf +#define _vstprintf vsprintf + +// scanfs +#define _tscanf scanf +#define _stscanf sscanf + +// string manipulations +#define _tcscat strcat +#define _tcschr strchr +#define _tcsclen strlen +#define _tcscnlen strnlen +#define _tcscpy strcpy +#define _tcsdup _strdup +#define _tcslen strlen +#define _tcsnccpy strncpy +#define _tcsrchr strrchr +#define _tcsstr strstr +#define _tcstok strtok + +// string comparisons +#define _tcscmp strcmp +#define _tcsicmp _stricmp +#define _tcsncmp strncmp +#define _tcsncicmp _strnicmp +#define _tcsnicmp _strnicmp + +// upper / lower +#define _tcslwr _strlwr +#define _tcsupr _strupr + +#ifndef _totupper +#define _totupper toupper +#endif +#ifndef _totlower +#define _totlower tolower +#endif + +// conversions to numbers +#define _tcstol strtol +#define _tcstoul strtoul +#define _tstof atof +#define _tstoi atoi +#define _tstoi64 _atoi64 +#define _tstoi64 _atoi64 +#define _ttoi atoi +#define _ttoi64 _atoi64 +#define _ttol atol + +// conversion from numbers to strings +#define _i64tot _i64toa +#define _itot _itoa +#define _ltot _ltoa +#define _ui64tot _ui64toa + +// file manipulations +#define _tfopen fopen +#define _topen _open +#define _tremove remove +#define _tunlink _unlink + +// reading and writing to i/o +#define _fgettc fgetc +#define _fgetts fgets +#define _fputts fputs +#define _gettchar getchar + +// directory +#define _tchdir _chdir + +// environment +#define _tgetenv getenv +#define _tsystem system + +// time +#define _tcsftime strftime + +#endif + +// is functions (the same in Unicode / ANSI) +#ifndef _istgraph +#define _istgraph isgraph +#endif +#ifndef _istascii +#define _istascii __isascii +#endif + +#define __TFILE__ _T(__FILE__) +#define __TDATE__ _T(__DATE__) +#define __TTIME__ _T(__TIME__) diff --git a/src/pluginapi.c b/src/pluginapi.c new file mode 100644 index 0000000..1b537da --- /dev/null +++ b/src/pluginapi.c @@ -0,0 +1,298 @@ +#include +#include +#include + +#include "pluginapi.h" + +#ifdef _countof +#define COUNTOF _countof +#else +#define COUNTOF(a) (sizeof(a)/sizeof(a[0])) +#endif + +unsigned int g_stringsize; +stack_t **g_stacktop; +TCHAR *g_variables; + +// utility functions (not required but often useful) + +int NSISCALL popstring(TCHAR *str) +{ + stack_t *th; + if (!g_stacktop || !*g_stacktop) return 1; + th=(*g_stacktop); + + if (str) _tcscpy(str,th->text); + + *g_stacktop = th->next; + GlobalFree((HGLOBAL)th); + return 0; +} + +int NSISCALL popstringn(TCHAR *str, int maxlen) +{ + stack_t *th; + if (!g_stacktop || !*g_stacktop) return 1; + th=(*g_stacktop); + if (str) lstrcpyn(str,th->text,maxlen?maxlen:g_stringsize); + *g_stacktop = th->next; + GlobalFree((HGLOBAL)th); + return 0; +} + +void NSISCALL pushstring(const TCHAR *str) +{ + stack_t *th; + if (!g_stacktop) return; + th=(stack_t*)GlobalAlloc(GPTR,(sizeof(stack_t)+(g_stringsize)*sizeof(TCHAR))); + lstrcpyn(th->text,str,g_stringsize); + th->next=*g_stacktop; + *g_stacktop=th; +} + +TCHAR* NSISCALL getuservariable(const int varnum) +{ + if (varnum < 0 || varnum >= __INST_LAST) return NULL; + return g_variables+varnum*g_stringsize; +} + +void NSISCALL setuservariable(const int varnum, const TCHAR *var) +{ + if (var != NULL && varnum >= 0 && varnum < __INST_LAST) + lstrcpy(g_variables + varnum*g_stringsize, var); +} + +#ifdef _UNICODE +int NSISCALL PopStringA(char* ansiStr) +{ + wchar_t* wideStr = (wchar_t*) GlobalAlloc(GPTR, g_stringsize*sizeof(wchar_t)); + int rval = popstring(wideStr); + WideCharToMultiByte(CP_ACP, 0, wideStr, -1, ansiStr, g_stringsize, NULL, NULL); + GlobalFree((HGLOBAL)wideStr); + return rval; +} + +int NSISCALL PopStringNA(char* ansiStr, int maxlen) +{ + int realLen = maxlen ? maxlen : g_stringsize; + wchar_t* wideStr = (wchar_t*) GlobalAlloc(GPTR, realLen*sizeof(wchar_t)); + int rval = popstringn(wideStr, realLen); + WideCharToMultiByte(CP_ACP, 0, wideStr, -1, ansiStr, realLen, NULL, NULL); + GlobalFree((HGLOBAL)wideStr); + return rval; +} + +void NSISCALL PushStringA(const char* ansiStr) +{ + wchar_t* wideStr = (wchar_t*) GlobalAlloc(GPTR, g_stringsize*sizeof(wchar_t)); + MultiByteToWideChar(CP_ACP, 0, ansiStr, -1, wideStr, g_stringsize); + pushstring(wideStr); + GlobalFree((HGLOBAL)wideStr); + return; +} + +void NSISCALL GetUserVariableW(const int varnum, wchar_t* wideStr) +{ + lstrcpyW(wideStr, getuservariable(varnum)); +} + +void NSISCALL GetUserVariableA(const int varnum, char* ansiStr) +{ + wchar_t* wideStr = getuservariable(varnum); + WideCharToMultiByte(CP_ACP, 0, wideStr, -1, ansiStr, g_stringsize, NULL, NULL); +} + +void NSISCALL SetUserVariableA(const int varnum, const char* ansiStr) +{ + if (ansiStr != NULL && varnum >= 0 && varnum < __INST_LAST) + { + wchar_t* wideStr = g_variables + varnum * g_stringsize; + MultiByteToWideChar(CP_ACP, 0, ansiStr, -1, wideStr, g_stringsize); + } +} + +#else +// ANSI defs +int NSISCALL PopStringW(wchar_t* wideStr) +{ + char* ansiStr = (char*) GlobalAlloc(GPTR, g_stringsize); + int rval = popstring(ansiStr); + MultiByteToWideChar(CP_ACP, 0, ansiStr, -1, wideStr, g_stringsize); + GlobalFree((HGLOBAL)ansiStr); + return rval; +} + +int NSISCALL PopStringNW(wchar_t* wideStr, int maxlen) +{ + int realLen = maxlen ? maxlen : g_stringsize; + char* ansiStr = (char*) GlobalAlloc(GPTR, realLen); + int rval = popstringn(ansiStr, realLen); + MultiByteToWideChar(CP_ACP, 0, ansiStr, -1, wideStr, realLen); + GlobalFree((HGLOBAL)ansiStr); + return rval; +} + +void NSISCALL PushStringW(wchar_t* wideStr) +{ + char* ansiStr = (char*) GlobalAlloc(GPTR, g_stringsize); + WideCharToMultiByte(CP_ACP, 0, wideStr, -1, ansiStr, g_stringsize, NULL, NULL); + pushstring(ansiStr); + GlobalFree((HGLOBAL)ansiStr); +} + +void NSISCALL GetUserVariableW(const int varnum, wchar_t* wideStr) +{ + char* ansiStr = getuservariable(varnum); + MultiByteToWideChar(CP_ACP, 0, ansiStr, -1, wideStr, g_stringsize); +} + +void NSISCALL GetUserVariableA(const int varnum, char* ansiStr) +{ + lstrcpyA(ansiStr, getuservariable(varnum)); +} + +void NSISCALL SetUserVariableW(const int varnum, const wchar_t* wideStr) +{ + if (wideStr != NULL && varnum >= 0 && varnum < __INST_LAST) + { + char* ansiStr = g_variables + varnum * g_stringsize; + WideCharToMultiByte(CP_ACP, 0, wideStr, -1, ansiStr, g_stringsize, NULL, NULL); + } +} +#endif + +// playing with integers + +INT_PTR NSISCALL nsishelper_str_to_ptr(const TCHAR *s) +{ + INT_PTR v=0; + if (*s == _T('0') && (s[1] == _T('x') || s[1] == _T('X'))) + { + s++; + for (;;) + { + int c=*(++s); + if (c >= _T('0') && c <= _T('9')) c-=_T('0'); + else if (c >= _T('a') && c <= _T('f')) c-=_T('a')-10; + else if (c >= _T('A') && c <= _T('F')) c-=_T('A')-10; + else break; + v<<=4; + v+=c; + } + } + else if (*s == _T('0') && s[1] <= _T('7') && s[1] >= _T('0')) + { + for (;;) + { + int c=*(++s); + if (c >= _T('0') && c <= _T('7')) c-=_T('0'); + else break; + v<<=3; + v+=c; + } + } + else + { + int sign=0; + if (*s == _T('-')) sign++; else s--; + for (;;) + { + int c=*(++s) - _T('0'); + if (c < 0 || c > 9) break; + v*=10; + v+=c; + } + if (sign) v = -v; + } + + return v; +} + +unsigned int NSISCALL myatou(const TCHAR *s) +{ + unsigned int v=0; + + for (;;) + { + unsigned int c=*s++; + if (c >= _T('0') && c <= _T('9')) c-=_T('0'); + else break; + v*=10; + v+=c; + } + return v; +} + +int NSISCALL myatoi_or(const TCHAR *s) +{ + int v=0; + if (*s == _T('0') && (s[1] == _T('x') || s[1] == _T('X'))) + { + s++; + for (;;) + { + int c=*(++s); + if (c >= _T('0') && c <= _T('9')) c-=_T('0'); + else if (c >= _T('a') && c <= _T('f')) c-=_T('a')-10; + else if (c >= _T('A') && c <= _T('F')) c-=_T('A')-10; + else break; + v<<=4; + v+=c; + } + } + else if (*s == _T('0') && s[1] <= _T('7') && s[1] >= _T('0')) + { + for (;;) + { + int c=*(++s); + if (c >= _T('0') && c <= _T('7')) c-=_T('0'); + else break; + v<<=3; + v+=c; + } + } + else + { + int sign=0; + if (*s == _T('-')) sign++; else s--; + for (;;) + { + int c=*(++s) - _T('0'); + if (c < 0 || c > 9) break; + v*=10; + v+=c; + } + if (sign) v = -v; + } + + // Support for simple ORed expressions + if (*s == _T('|')) + { + v |= myatoi_or(s+1); + } + + return v; +} + +INT_PTR NSISCALL popintptr() +{ + TCHAR buf[128]; + if (popstringn(buf,COUNTOF(buf))) + return 0; + return nsishelper_str_to_ptr(buf); +} + +int NSISCALL popint_or() +{ + TCHAR buf[128]; + if (popstringn(buf,COUNTOF(buf))) + return 0; + return myatoi_or(buf); +} + +void NSISCALL pushintptr(INT_PTR value) +{ + TCHAR buffer[30]; + wsprintf(buffer, sizeof(void*) > 4 ? _T("%Id") : _T("%d"), value); + pushstring(buffer); +} diff --git a/src/pluginapi.h b/src/pluginapi.h new file mode 100644 index 0000000..ca671a8 --- /dev/null +++ b/src/pluginapi.h @@ -0,0 +1,104 @@ +#ifndef ___NSIS_PLUGIN__H___ +#define ___NSIS_PLUGIN__H___ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "api.h" +#include "nsis_tchar.h" + +#ifndef NSISCALL +# define NSISCALL __stdcall +#endif + +#define EXDLL_INIT() { \ + g_stringsize=string_size; \ + g_stacktop=stacktop; \ + g_variables=variables; } + +typedef struct _stack_t { + struct _stack_t *next; + TCHAR text[1]; // this should be the length of string_size +} stack_t; + +enum +{ +INST_0, // $0 +INST_1, // $1 +INST_2, // $2 +INST_3, // $3 +INST_4, // $4 +INST_5, // $5 +INST_6, // $6 +INST_7, // $7 +INST_8, // $8 +INST_9, // $9 +INST_R0, // $R0 +INST_R1, // $R1 +INST_R2, // $R2 +INST_R3, // $R3 +INST_R4, // $R4 +INST_R5, // $R5 +INST_R6, // $R6 +INST_R7, // $R7 +INST_R8, // $R8 +INST_R9, // $R9 +INST_CMDLINE, // $CMDLINE +INST_INSTDIR, // $INSTDIR +INST_OUTDIR, // $OUTDIR +INST_EXEDIR, // $EXEDIR +INST_LANG, // $LANGUAGE +__INST_LAST +}; + +extern unsigned int g_stringsize; +extern stack_t **g_stacktop; +extern TCHAR *g_variables; + +void NSISCALL pushstring(const TCHAR *str); +void NSISCALL pushintptr(INT_PTR value); +#define pushint(v) pushintptr((INT_PTR)(v)) +int NSISCALL popstring(TCHAR *str); // 0 on success, 1 on empty stack +int NSISCALL popstringn(TCHAR *str, int maxlen); // with length limit, pass 0 for g_stringsize +INT_PTR NSISCALL popintptr(); +#define popint() ( (int) popintptr() ) +int NSISCALL popint_or(); // with support for or'ing (2|4|8) +INT_PTR NSISCALL nsishelper_str_to_ptr(const TCHAR *s); +#define myatoi(s) ( (int) nsishelper_str_to_ptr(s) ) // converts a string to an integer +unsigned int NSISCALL myatou(const TCHAR *s); // converts a string to an unsigned integer, decimal only +int NSISCALL myatoi_or(const TCHAR *s); // with support for or'ing (2|4|8) +TCHAR* NSISCALL getuservariable(const int varnum); +void NSISCALL setuservariable(const int varnum, const TCHAR *var); + +#ifdef _UNICODE +#define PopStringW(x) popstring(x) +#define PushStringW(x) pushstring(x) +#define SetUserVariableW(x,y) setuservariable(x,y) + +int NSISCALL PopStringA(char* ansiStr); +void NSISCALL PushStringA(const char* ansiStr); +void NSISCALL GetUserVariableW(const int varnum, wchar_t* wideStr); +void NSISCALL GetUserVariableA(const int varnum, char* ansiStr); +void NSISCALL SetUserVariableA(const int varnum, const char* ansiStr); + +#else +// ANSI defs + +#define PopStringA(x) popstring(x) +#define PushStringA(x) pushstring(x) +#define SetUserVariableA(x,y) setuservariable(x,y) + +int NSISCALL PopStringW(wchar_t* wideStr); +void NSISCALL PushStringW(wchar_t* wideStr); +void NSISCALL GetUserVariableW(const int varnum, wchar_t* wideStr); +void NSISCALL GetUserVariableA(const int varnum, char* ansiStr); +void NSISCALL SetUserVariableW(const int varnum, const wchar_t* wideStr); + +#endif + +#ifdef __cplusplus +} +#endif + +#endif//!___NSIS_PLUGIN__H___ diff --git a/tools/fix_repo.py b/tools/fix_repo.py new file mode 100644 index 0000000..ec781bb --- /dev/null +++ b/tools/fix_repo.py @@ -0,0 +1,137 @@ +""" +Fix nsSplashPNG repo: +1. Rimuove dal tracking i file di build MSBuild in src/ (artifacts + DLL grezze) +2. Sposta le DLL pre-built in dist/ (struttura canonica) +3. Aggiunge regole .gitignore per escludere le cartelle Release in src/ +4. Aggiunge dist/ al tracking (con DLL binarie) +5. Commit + push Gitea + push GitHub +""" +import os, sys, shutil, subprocess +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + +# --- Load .env --- +env_path = REPO.parent.parent / '.env' # Launchers/.env +if env_path.exists(): + for line in env_path.read_text(encoding='utf-8').splitlines(): + line = line.strip() + if line and not line.startswith('#') and '=' in line: + k, v = line.split('=', 1) + os.environ.setdefault(k.strip(), v.strip()) + +GITEA_URL = os.environ['GITEA_URL'] +GITEA_USER = os.environ['GITEA_USER'] +GITEA_TOK = os.environ['GITEA_TOKEN'] +GH_USER = os.environ['GITHUB_USER'] +GH_TOK = os.environ['GITHUB_TOKEN'] +REPO_NAME = 'nsis-plugin-nssplashpng' + +def git(*args): + r = subprocess.run(['git', '-C', str(REPO)] + list(args), capture_output=True, text=True) + if r.returncode != 0: + print(f'[git] stderr: {r.stderr.strip()}') + return r + +# 1. Rimuovi dal tracking tutti i file di build in src/ +print('[1] Rimuovo artifacts di build dal tracking...') +tracked_artifacts = [ + 'src/nsSplashPNG/Release Unicode/nsSplashPNG.dll.recipe', + 'src/nsSplashPNG/Release Unicode/nsSplashPNG.iobj', + 'src/nsSplashPNG/Release Unicode/nsSplashPNG.ipdb', + 'src/nsSplashPNG/Release/nsSplashPNG.dll.recipe', + 'src/nsSplashPNG/Release/nsSplashPNG.iobj', + 'src/nsSplashPNG/Release/nsSplashPNG.ipdb', + 'src/nsSplashPNG/x64/Release Unicode/nsSplashPNG.dll.recipe', + 'src/nsSplashPNG/x64/Release Unicode/nsSplashPNG.iobj', + 'src/nsSplashPNG/x64/Release Unicode/nsSplashPNG.ipdb', + 'src/Release Unicode/nsSplashPNG.dll', + 'src/Release/nsSplashPNG.dll', + 'src/x64/Release Unicode/nsSplashPNG.dll', +] +for f in tracked_artifacts: + r = git('rm', '--cached', f) + if r.returncode == 0: + print(f' [rm cached] {f}') + else: + print(f' [skip] {f} — not tracked or already removed') + +# 2. Sposta DLL in dist/ (struttura canonica) +print('[2] Sposto DLL in dist/...') +moves = [ + ('src/Release/nsSplashPNG.dll', 'dist/x86-ansi/nsSplashPNG.dll'), + ('src/Release Unicode/nsSplashPNG.dll', 'dist/x86-unicode/nsSplashPNG.dll'), + ('src/x64/Release Unicode/nsSplashPNG.dll', 'dist/amd64-unicode/nsSplashPNG.dll'), +] +for src_rel, dst_rel in moves: + src = REPO / Path(src_rel) + dst = REPO / Path(dst_rel) + dst.parent.mkdir(parents=True, exist_ok=True) + if src.exists(): + shutil.copy2(src, dst) + print(f' [copied] {src_rel} → {dst_rel}') + elif dst.exists(): + print(f' [exists] {dst_rel} (src già rimosso)') + else: + print(f' [warn] {src_rel} non trovato — DLL mancante') + +# 3. Aggiungi .gitignore rules per le cartelle MSBuild in src/ +print('[3] Aggiorno .gitignore...') +gi_path = REPO / '.gitignore' +gi_content = gi_path.read_text(encoding='utf-8') +extra = ( + '\n# MSBuild output directories in src/ (build artifacts, not source)\n' + 'src/**/Release/\n' + 'src/**/Release Unicode/\n' + 'src/**/x64/\n' + '*.recipe\n' + '*.iobj\n' + '*.ipdb\n' +) +if 'MSBuild output directories' not in gi_content: + gi_path.write_text(gi_content + extra, encoding='utf-8') + print(' [ok] .gitignore aggiornato') +else: + print(' [skip] .gitignore già aggiornato') + +# 4. Stage dist/ e .gitignore +print('[4] Stage dist/ e .gitignore...') +git('add', 'dist/') +git('add', '.gitignore') + +# Verifica status +r = git('status', '--short') +print(r.stdout.strip() or '(nothing staged)') + +# 5. Commit +print('[5] Commit...') +r = git('commit', '-m', + 'fix: move pre-built DLLs to dist/, remove MSBuild artifacts from tracking\n\n' + '- src/Release*/ and src/nsSplashPNG/Release*/ were committed by mistake\n' + ' (MSBuild artifacts: .iobj, .ipdb, .recipe + raw DLL outputs)\n' + '- DLLs moved to canonical dist/{x86-ansi,x86-unicode,amd64-unicode}/\n' + '- .gitignore updated to exclude src/**/Release*/ going forward' +) +if r.returncode != 0: + print(f'[FAIL] commit: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] {r.stdout.strip()}') + +# 6. Push Gitea +print('[6] Push Gitea...') +gitea_url = f"{GITEA_URL.replace('://', f'://{GITEA_USER}:{GITEA_TOK}@')}/{GITEA_USER}/{REPO_NAME}.git" +r = subprocess.run(['git', '-C', str(REPO), 'push', gitea_url, 'main'], + capture_output=True, text=True) +if r.returncode != 0: + print(f'[FAIL] Gitea: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] Gitea: {r.stderr.strip() or "up to date"}') + +# 7. Push GitHub +print('[7] Push GitHub...') +gh_url = f'https://{GH_USER}:{GH_TOK}@github.com/{GH_USER}/{REPO_NAME}.git' +r = subprocess.run(['git', '-C', str(REPO), 'push', gh_url, 'main'], + capture_output=True, text=True) +if r.returncode != 0: + print(f'[FAIL] GitHub: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] GitHub: {r.stderr.strip() or "up to date"}') + +print('[DONE]') diff --git a/tools/push_and_disable_gitea_actions.py b/tools/push_and_disable_gitea_actions.py new file mode 100644 index 0000000..8713069 --- /dev/null +++ b/tools/push_and_disable_gitea_actions.py @@ -0,0 +1,80 @@ +""" +Push latest commit to Gitea + GitHub directly, +then disable Gitea Actions on the repo (keep GitHub Actions enabled). +""" +import urllib.request, urllib.error, json, os, sys, subprocess + +# --- Load .env from workspace root --- +env_path = os.path.join(os.path.dirname(__file__), '..', '..', '..', '.env') +env_path = os.path.normpath(env_path) +if os.path.exists(env_path): + with open(env_path, encoding='utf-8') as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + k, v = line.split('=', 1) + os.environ.setdefault(k.strip(), v.strip()) + +GITEA_URL = os.environ['GITEA_URL'] +GITEA_USER = os.environ['GITEA_USER'] +GITEA_TOK = os.environ['GITEA_TOKEN'] +GH_USER = os.environ['GITHUB_USER'] +GH_TOK = os.environ['GITHUB_TOKEN'] +REPO = 'nsis-plugin-nssplashpng' + +STAGING = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) + +def api_call(url, method='GET', data=None, headers=None): + body = json.dumps(data).encode() if data else None + req = urllib.request.Request(url, data=body, headers=headers or {}, method=method) + try: + with urllib.request.urlopen(req) as r: + raw = r.read() + return r.status, json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + raw = e.read() + return e.code, json.loads(raw) if raw else {} + +# --- 1. Push to Gitea --- +print('[1] Push to Gitea...') +gitea_push_url = f"{GITEA_URL.replace('://', f'://{GITEA_USER}:{GITEA_TOK}@')}/{GITEA_USER}/{REPO}.git" +r = subprocess.run(['git', '-C', STAGING, 'push', gitea_push_url, 'main'], + capture_output=True, text=True) +if r.returncode != 0: + print(f'[FAIL] Gitea push: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] Gitea push: {r.stderr.strip() or "up to date"}') + +# --- 2. Push to GitHub --- +print('[2] Push to GitHub...') +gh_push_url = f'https://{GH_USER}:{GH_TOK}@github.com/{GH_USER}/{REPO}.git' +r = subprocess.run(['git', '-C', STAGING, 'push', gh_push_url, 'main'], + capture_output=True, text=True) +if r.returncode != 0: + print(f'[FAIL] GitHub push: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] GitHub push: {r.stderr.strip() or "up to date"}') + +# --- 3. Disable Gitea Actions --- +print('[3] Disabling Gitea Actions...') +gitea_hdrs = {'Authorization': f'token {GITEA_TOK}', 'Content-Type': 'application/json'} +s, resp = api_call( + f'{GITEA_URL}/api/v1/repos/{GITEA_USER}/{REPO}', + method='PATCH', + data={'has_actions': False}, + headers=gitea_hdrs +) +if s in (200, 201, 204): + actions_status = resp.get('has_actions', '?') + print(f'[ok] Gitea Actions disabled (has_actions={actions_status})') +else: + print(f'[warn] PATCH repo returned {s}: {resp}') + print('[warn] Trying via repo settings API...') + # Alternative: some Gitea versions use a different field name + s2, resp2 = api_call( + f'{GITEA_URL}/api/v1/repos/{GITEA_USER}/{REPO}', + method='PATCH', + data={'actions': False}, + headers=gitea_hdrs + ) + print(f'[info] actions=False attempt: {s2}: {resp2.get("has_actions", resp2.get("actions", "?"))}') + +print('[DONE]') diff --git a/tools/squash_to_single_commit.py b/tools/squash_to_single_commit.py new file mode 100644 index 0000000..d0ec030 --- /dev/null +++ b/tools/squash_to_single_commit.py @@ -0,0 +1,121 @@ +""" +Squash everything in the nsSplashPNG repo into a single initial commit, +then force-push to Gitea and GitHub. + +Steps: +1. Create orphan branch 'init' +2. git add -A (all currently tracked + untracked non-ignored files) +3. Commit with the canonical initial message +4. Replace 'main' with 'init' +5. Delete old v1.0.0 tag, recreate on new commit +6. Force-push main + tag to Gitea and GitHub +""" +import os, sys, subprocess +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent + +# --- Load .env --- +env_path = REPO.parent.parent / '.env' +if env_path.exists(): + for line in env_path.read_text(encoding='utf-8').splitlines(): + line = line.strip() + if line and not line.startswith('#') and '=' in line: + k, v = line.split('=', 1) + os.environ.setdefault(k.strip(), v.strip()) + +GITEA_URL = os.environ['GITEA_URL'] +GITEA_USER = os.environ['GITEA_USER'] +GITEA_TOK = os.environ['GITEA_TOKEN'] +GH_USER = os.environ['GITHUB_USER'] +GH_TOK = os.environ['GITHUB_TOKEN'] +REPO_NAME = 'nsis-plugin-nssplashpng' + +GITEA_PUSH = f"{GITEA_URL.replace('://', f'://{GITEA_USER}:{GITEA_TOK}@')}/{GITEA_USER}/{REPO_NAME}.git" +GH_PUSH = f'https://{GH_USER}:{GH_TOK}@github.com/{GH_USER}/{REPO_NAME}.git' + +COMMIT_MSG = 'chore: initial commit (extracted from Launchers monorepo)' +TAG = 'v1.0.0' + +def git(*args, check=True): + r = subprocess.run(['git', '-C', str(REPO)] + list(args), + capture_output=True, text=True) + if check and r.returncode != 0: + print(f'[git {" ".join(args)}] stderr: {r.stderr.strip()}', file=sys.stderr) + sys.exit(1) + return r + +# 1. Crea branch orfano +print('[1] Creo branch orfano "init"...') +git('checkout', '--orphan', 'init') + +# 2. Stage tutto (include file attualmente tracciati + non-ignorati) +print('[2] Stage di tutti i file...') +git('add', '-A') + +# Verifica cosa verrà committato +r = git('status', '--short') +print(r.stdout.strip()) + +# 3. Commit +print('[3] Commit unico...') +# Configura autore se non già globale +r = git('commit', '-m', COMMIT_MSG) +print(f'[ok] {r.stdout.strip()}') + +# 4. Sostituisci main con init +print('[4] Sostituisco branch main...') +git('branch', '-D', 'main') +git('branch', '-m', 'init', 'main') + +# 5. Elimina vecchio tag e ricrealo sul nuovo commit +print(f'[5] Ricreo tag {TAG}...') +git('tag', '-d', TAG, check=False) # potrebbe non esistere +git('tag', TAG) + +# Verifica finale +r = git('log', '--oneline') +print(f'[log] {r.stdout.strip()}') +r = git('tag', '--list') +print(f'[tags] {r.stdout.strip()}') + +# 6. Force push a Gitea +print('[6] Force push a Gitea...') +r = subprocess.run( + ['git', '-C', str(REPO), 'push', '--force', GITEA_PUSH, 'main', TAG], + capture_output=True, text=True +) +if r.returncode != 0: + print(f'[FAIL] Gitea: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] Gitea: {r.stderr.strip()}') + +# 7. Force push a GitHub +print('[7] Force push a GitHub...') +r = subprocess.run( + ['git', '-C', str(REPO), 'push', '--force', GH_PUSH, 'main', TAG], + capture_output=True, text=True +) +if r.returncode != 0: + print(f'[FAIL] GitHub: {r.stderr}', file=sys.stderr); sys.exit(1) +print(f'[ok] GitHub: {r.stderr.strip()}') + +# 8. Aggiorna origin/main +print('[8] Aggiorno remote tracking...') +r = subprocess.run( + ['git', '-C', str(REPO), 'remote', 'set-head', 'origin', 'main'], + capture_output=True, text=True +) +# Fetch per sincronizzare origin/main con il nuovo HEAD +gitea_fetch = GITEA_PUSH +r2 = subprocess.run( + ['git', '-C', str(REPO), 'fetch', '--force', gitea_fetch, 'main:refs/remotes/origin/main'], + capture_output=True, text=True +) + +# Verifica stato finale +r = git('log', '--oneline', '-3') +print(f'\n[FINAL LOG]\n{r.stdout.strip()}') +r = git('status', '-sb') +print(f'[FINAL STATUS]\n{r.stdout.strip()}') + +print('\n[DONE] Repository ridotto a singolo commit iniziale.')