6 Commits

Author SHA1 Message Date
Simone fbf50f5b56 fix: add BuildTools support in MSBuild discovery 2026-04-30 22:49:53 +02:00
Simone ef88d32877 fix: align Release UNICODE config name with vcxproj; use --config in CI 2026-04-30 22:44:48 +02:00
Simone 5f7bf03806 docs: fix build options in README/README_IT (correct flags, dist/ path) 2026-04-30 20:12:52 +02:00
Simone 2a961ee075 build: align build script and CI to canonical plugin pattern
- Rewrite build_plugin.py following nsParser reference pattern:
  correct vcxproj/dist/plugins paths, --configs plural + --config alias,
  --final flag, platform_toolset param, parallel builds with spinner,
  correct clean_build_artifacts targeting src/Build/
- CI build.yml: replace --config + --verbose with --configs + --verbosity minimal
- CI release.yml: replace --config with --configs
2026-04-30 00:23:35 +02:00
Simone 9e9758e174 docs: add bilingual README (EN/IT) 2026-04-29 15:17:33 +02:00
Simone 2e0aaa77a5 docs: add build options to README 2026-04-29 15:01:05 +02:00
4 changed files with 855 additions and 138 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ jobs:
"version=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: Build ${{ matrix.config }}
run: python build_plugin.py --config ${{ matrix.config }} --verbose
run: python build_plugin.py --config ${{ matrix.config }} --verbosity minimal
- name: Upload DLL
uses: actions/upload-artifact@v4
+39 -25
View File
@@ -1,10 +1,10 @@
# nsProcess NSIS Plugin v1.6
**Versione personale modificata**
**Personal modified version**
---
## Descrizione Originale
## Original Description
**2006 Shengalts Aleksander aka Instructor (Shengalts@mail.ru)**
@@ -119,49 +119,63 @@ Include `nsProcess.nsh` to use the macros:
---
## ⚠️ Differenze nella versione personale
## ⚠️ Differences in the Personal Version
### Nuova architettura supportata: x64 (amd64-unicode)
### New supported architecture: x64 (amd64-unicode)
L'originale supportava solo:
The original supported only:
- x86-ansi (Plugins\nsProcess.dll)
- x86-unicode (nsProcessW.dll da rinominare in nsProcess.dll)
- x86-unicode (nsProcessW.dll renamed to nsProcess.dll)
La versione personale aggiunge il supporto per:
The personal version adds support for:
- **amd64-unicode** (x64)
### Progetto Visual Studio
### Visual Studio Project
L'originale includeva:
- `Source/nsProcess.sln` - Solution VS2008
- `Source/nsProcess.vcproj` - Progetto VS2008 (solo x86)
The original included:
- `Source/nsProcess.sln` - VS2008 Solution
- `Source/nsProcess.vcproj` - VS2008 Project (x86 only)
La versione personale converte a VS2022 con supporto x64:
- `nsProcess.sln` - Solution VS2022
- `nsProcess/nsProcess.vcxproj` - Progetto VS2022 (x86-ansi, x86-unicode, amd64-unicode)
The personal version upgrades to VS2022 with x64 support:
- `nsProcess.sln` - VS2022 Solution
- `nsProcess/nsProcess.vcxproj` - VS2022 Project (x86-ansi, x86-unicode, amd64-unicode)
### File aggiunti
### Added Files
- `build_plugin.py` - Script Python per compilare il plugin per tutte le architetture
- `nsis/` - Cartella con gli header NSIS per la compilazione:
- `build_plugin.py` - Python script to compile the plugin for all architectures
- `nsis/` - Folder with NSIS headers for compilation:
- `api.h`, `crt.c`, `nsis_tchar.h`, `pluginapi.c`, `pluginapi.h`
### File rimossi
### Removed Files
I file DLL precompilati sono stati rimossi dalla distribuzione:
Pre-compiled DLL files have been removed from the distribution:
- `Plugins/nsProcess.dll` (x86 ANSI)
- `nsProcessW.dll` (x86 UNICODE)
### Compilazione
### Build
```cmd
cd nsProcess
```powershell
python build_plugin.py
```
I DLL vengono copiati in `plugins/{platform}/nsProcess.dll`.
DLLs are copied to `dist/{platform}/nsProcess.dll`.
### Modifiche funzionali
### Build Options
Nessuna modifica funzionale rispetto all'originale. Le modifiche riguardano solo l'infrastruttura di build e il supporto x64.
```powershell
python build_plugin.py --configs x86-unicode # Single architecture (x86-ansi|x86-unicode|amd64-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)
python build_plugin.py --final # Force rebuild + clean (pre-release build)
```
### Functional Changes
No functional changes compared to the original. Modifications are limited to the build infrastructure and x64 support.
---
*See [README_IT.md](README_IT.md) for the Italian version.*
+173
View File
@@ -0,0 +1,173 @@
# nsProcess NSIS Plugin v1.6
**Versione personale modificata**
---
## Descrizione Originale
**2006 Shengalts Aleksander aka Instructor (Shengalts@mail.ru)**
Funzione sorgente FIND_PROC_BY_NAME basata sul codice di Ravi Kochhar (kochhar@physiology.wisc.edu).
Grazie a iceman_k (plugin FindProcDLL) e DITMan (plugin KillProcDLL) per l'ispirazione.
Versione NSIS UNICODE compatibile (1.6) di brainsucker.
## FunzionalitĂ 
- Cerca un processo per nome
- Termina tutti i processi con il nome specificato (non solo uno)
- Chiude tutti i processi con il nome specificato (prima tenta di chiudere le finestre, attende 3 secondi, poi termina se ancora attivo)
- Il nome del processo non è case-sensitive
- Supporto Win95/98/ME/NT/2000/XP/Win7
- Dimensione plugin ridotta (4 Kb)
- Supporto NSIS UNICODE
## Utilizzo
### Ricerca processo
```nsis
${nsProcess::FindProcess} "[file.exe]" $var
```
**Parametri:**
- `[file.exe]` - Nome processo (es. "notepad.exe")
**Valori restituiti ($var):**
| Codice | Descrizione |
|--------|-------------|
| 0 | Successo |
| 603 | Processo non in esecuzione |
| 604 | Impossibile identificare il tipo di sistema |
| 605 | OS non supportato |
| 606 | Impossibile caricare NTDLL.DLL |
| 607 | Impossibile ottenere l'indirizzo della procedura da NTDLL.DLL |
| 608 | NtQuerySystemInformation fallita |
| 609 | Impossibile caricare KERNEL32.DLL |
| 610 | Impossibile ottenere l'indirizzo della procedura da KERNEL32.DLL |
| 611 | CreateToolhelp32Snapshot fallita |
### Terminazione/Chiusura processo
```nsis
${nsProcess::KillProcess} "[file.exe]" $var
${nsProcess::CloseProcess} "[file.exe]" $var
```
**Valori restituiti ($var):**
| Codice | Descrizione |
|--------|-------------|
| 0 | Successo |
| 601 | Permessi insufficienti per terminare il processo |
| 602 | Non tutti i processi sono stati terminati |
| 603 | Processo non in esecuzione |
| 604 | Impossibile identificare il tipo di sistema |
| 605 | OS non supportato |
| ... | (vedi sopra) |
### Chiamata diretta al plugin (senza macro)
```nsis
nsProcess::_CloseProcess "notepad.exe"
Pop $R0
```
### Scaricamento plugin
```nsis
${nsProcess::Unload}
```
## Esempio
```nsis
!include "nsProcess.nsh"
Section "Verifica Notepad"
${nsProcess::FindProcess} "notepad.exe" $R0
${If} $R0 == 0
MessageBox MB_YESNO "Notepad è in esecuzione. Chiuderlo?" IDNO skip
${nsProcess::CloseProcess} "notepad.exe" $R0
${If} $R0 == 0
MessageBox MB_OK "Notepad chiuso con successo"
${Else}
MessageBox MB_OK "Impossibile chiudere Notepad"
${EndIf}
skip:
${Else}
MessageBox MB_OK "Notepad non è in esecuzione"
${EndIf}
${nsProcess::Unload}
SectionEnd
```
## File include
Includi `nsProcess.nsh` per utilizzare le macro:
```nsis
!include "nsProcess.nsh"
```
---
## ⚠️ Differenze nella versione personale
### Nuova architettura supportata: x64 (amd64-unicode)
L'originale supportava solo:
- x86-ansi (Plugins\nsProcess.dll)
- x86-unicode (nsProcessW.dll da rinominare in nsProcess.dll)
La versione personale aggiunge il supporto per:
- **amd64-unicode** (x64)
### Progetto Visual Studio
L'originale includeva:
- `Source/nsProcess.sln` - Solution VS2008
- `Source/nsProcess.vcproj` - Progetto VS2008 (solo x86)
La versione personale converte a VS2022 con supporto x64:
- `nsProcess.sln` - Solution VS2022
- `nsProcess/nsProcess.vcxproj` - Progetto VS2022 (x86-ansi, x86-unicode, amd64-unicode)
### File aggiunti
- `build_plugin.py` - Script Python per compilare il plugin per tutte le architetture
- `nsis/` - Cartella con gli header NSIS per la compilazione:
- `api.h`, `crt.c`, `nsis_tchar.h`, `pluginapi.c`, `pluginapi.h`
### File rimossi
I file DLL precompilati sono stati rimossi dalla distribuzione:
- `Plugins/nsProcess.dll` (x86 ANSI)
- `nsProcessW.dll` (x86 UNICODE)
### Compilazione
```powershell
python build_plugin.py
```
I DLL vengono copiati in `dist/{platform}/nsProcess.dll`.
### Opzioni build
```powershell
python build_plugin.py --configs x86-unicode # Solo un'architettura (x86-ansi|x86-unicode|amd64-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 # VerbositĂ  MSBuild (quiet|minimal|normal|detailed|diagnostic)
python build_plugin.py --final # Rebuild forzato + pulizia (build pre-release)
```
### Modifiche funzionali
Nessuna modifica funzionale rispetto all'originale. Le modifiche riguardano solo l'infrastruttura di build e il supporto x64.
---
*See [README.md](README.md) for the English version.*
+641 -111
View File
@@ -1,158 +1,688 @@
#!/usr/bin/env python3
#!/usr/bin/env python3
"""
Build script canonico per il plugin nsProcess.
CLI standard: vedi `python build_plugin.py --help`
Build script for nsProcess plugin - All configurations
Supports multiple build configurations with flexible parameters
"""
from __future__ import annotations
import argparse
import io
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 UTF-8 output on Windows (avoids cp1252 UnicodeEncodeError)
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
# 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
ROOT = Path(__file__).resolve().parent
SRC_DIR = ROOT / "src"
DIST_DIR = ROOT / "dist"
VERSION_FILE = ROOT / "VERSION"
# Mapping CLI config -> (MSBuild Configuration, MSBuild Platform, dist subdir)
CONFIGS = {
"x86-ansi": ("Release", "Win32", "x86-ansi"),
"x86-unicode": ("Release Unicode", "Win32", "x86-unicode"),
"x64-unicode": ("Release Unicode", "x64", "amd64-unicode"),
'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'),
}
VCXPROJ = SRC_DIR / "nsProcess.vcxproj"
DLL_NAME = "nsProcess.dll"
DLL_NAME = 'nsProcess.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), '-products', '*', '-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
_VS_NUMERIC_FOLDER = {'2026': '18', '2022': '2022'}
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 (incl. BuildTools and VS2026 numeric folder)
to_try = ['2026', '2022'] if vs_version == 'auto' else [vs_version]
for ver in to_try:
numeric = _VS_NUMERIC_FOLDER.get(ver, ver)
base_paths = [
Path(rf'C:\Program Files (x86)\Microsoft Visual Studio\{numeric}'),
Path(rf'C:\Program Files\Microsoft Visual Studio\{ver}'),
]
for base in base_paths:
for edition in ('Community', 'Professional', 'Enterprise', 'BuildTools'):
p = base / 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 / 'nsProcess.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/<plugin>/)."""
p = Path(__file__).parent.parent.parent.parent / 'plugins'
return p if p.is_dir() else None
def read_version() -> str:
# utf-8-sig strips BOM (\ufeff) automatically
return VERSION_FILE.read_text(encoding="utf-8-sig").strip()
vf = Path(__file__).parent / 'VERSION'
try:
return vf.read_text(encoding='utf-8-sig').strip()
except Exception:
return '0.0.0'
def find_msbuild(toolset: str) -> Path:
"""Localizza MSBuild via vswhere; toolset='2022', '2026', o 'auto'.
# ---------------------------------------------------------------------------
# CPU info & build optimizations
# ---------------------------------------------------------------------------
In auto mode, tries 2022 before 2026 because most vcxproj files use
PlatformToolset v143 (VS2022) which may not be available in VS2026 build tools.
"""
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
vswhere = Path(pf86) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe"
if not vswhere.exists():
sys.exit(2)
def _query(version_range: str | None) -> list[str]:
args = [str(vswhere), "-latest", "-products", "*",
"-requires", "Microsoft.Component.MSBuild",
"-find", r"MSBuild\**\Bin\MSBuild.exe"]
if version_range:
args += ["-version", version_range]
result = subprocess.run(args, capture_output=True, text=True)
return [ln.strip() for ln in result.stdout.splitlines() if ln.strip()]
if toolset == "2022":
paths = _query("[17.0,18.0)")
elif toolset == "2026":
paths = _query("[18.0,19.0)")
else: # auto: prefer 2022 (v143) over 2026 (v145)
paths = _query("[17.0,18.0)") or _query("[18.0,19.0)") or _query(None)
if not paths:
sys.exit(2)
return Path(paths[0])
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 build_one(msbuild: Path, config: str, platform: str, jobs: int, verbose: bool) -> bool:
cmd = [
str(msbuild), str(VCXPROJ),
f"/p:Configuration={config}",
f"/p:Platform={platform}",
f"/m:{jobs}",
"/nologo",
"/v:" + ("normal" if verbose else "minimal"),
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',
]
print(f"[build] {config} | {platform}")
return subprocess.run(cmd).returncode == 0
def collect_dll(config: str, platform: str, dist_subdir: str) -> Path | None:
"""Trova la DLL prodotta dal vcxproj e la copia in dist/<dist_subdir>/."""
plat_dir = "" if platform == "Win32" else platform + "/"
candidates = list(SRC_DIR.glob(f"**/{plat_dir}{config}/{DLL_NAME}"))
candidates += list(ROOT.glob(f"**/{plat_dir}{config}/{DLL_NAME}"))
if not candidates:
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/<dest_dir>/ to plugins/<dest_dir>/."""
src = dist_dir / config.dest_dir / DLL_NAME
if not src.exists():
return None
src = candidates[0]
dst_dir = DIST_DIR / dist_subdir
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=f"Build nsProcess plugin")
parser.add_argument("--config", default="all",
choices=list(CONFIGS.keys()) + ["all"])
parser.add_argument("--toolset", default="auto", choices=["2022", "2026", "auto"])
parser.add_argument("--jobs", type=int, default=os.cpu_count() or 4)
parser.add_argument("--clean", action="store_true")
parser.add_argument("--install-dir", type=Path, default=None)
parser.add_argument("--verbose", action="store_true")
parser.add_argument("--version", action="store_true",
help="Mostra versione e termina")
parser = argparse.ArgumentParser(description='Build nsProcess 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 <config>/ subdirs)')
args = parser.parse_args()
if args.version:
print(read_version())
# 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 not VCXPROJ.exists():
print(f"ERROR: {VCXPROJ} not found", file=sys.stderr)
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
if args.clean and DIST_DIR.exists():
shutil.rmtree(DIST_DIR)
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
msbuild = find_msbuild(args.toolset)
print(f"[info] MSBuild: {msbuild}")
print(f"[info] Version: {read_version()}")
version = read_version()
targets = list(CONFIGS.keys()) if args.config == "all" else [args.config]
failed = []
for cfg_name in targets:
config, platform, subdir = CONFIGS[cfg_name]
if not build_one(msbuild, config, platform, args.jobs, args.verbose):
failed.append(cfg_name)
continue
dst = collect_dll(config, platform, subdir)
if dst is None:
print(f"ERROR: DLL not found for {cfg_name}", file=sys.stderr)
failed.append(cfg_name)
if not args.configs:
configs_to_build = list(CONFIGS.values())
elif 'all' in args.configs:
configs_to_build = list(CONFIGS.values())
else:
print(f"[ok] {dst}")
if args.install_dir:
install_path = args.install_dir / subdir
install_path.mkdir(parents=True, exist_ok=True)
shutil.copy2(dst, install_path / DLL_NAME)
print(f"[install] {install_path / DLL_NAME}")
configs_to_build = [CONFIGS[n] for n in args.configs]
if failed:
print(f"FAILED: {failed}", file=sys.stderr)
return 1
print("[done] all targets built successfully")
return 0
# Banner
print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}{'='*60}")
print(f"Building nsProcess 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__":
if __name__ == '__main__':
sys.exit(main())