Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 017072b3d4 | |||
| bb27c931a0 | |||
| e7427109a7 | |||
| 75b4a319cf | |||
| dd2b3c4123 | |||
| 2c29d91022 |
+11
-1
@@ -7,6 +7,15 @@ e il progetto aderisce al [Semantic Versioning](https://semver.org/spec/v2.0.0.h
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.1.0] — 2026-04-30
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated `build_plugin.py` with expanded CLI options and build improvements
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Added bilingual README (EN/IT) with full plugin reference
|
||||||
|
- Added build options section to README
|
||||||
|
|
||||||
## [1.0.0] — 2026-04-29
|
## [1.0.0] — 2026-04-29
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -15,5 +24,6 @@ e il progetto aderisce al [Semantic Versioning](https://semver.org/spec/v2.0.0.h
|
|||||||
- CI/CD via GitHub Actions (mirror automatico Gitea → GitHub)
|
- CI/CD via GitHub Actions (mirror automatico Gitea → GitHub)
|
||||||
- Documentazione completa (README, CONTRIBUTING, SECURITY)
|
- Documentazione completa (README, CONTRIBUTING, SECURITY)
|
||||||
|
|
||||||
[Unreleased]: https://gitea.emulab.it/Simone/nsis-plugin-nsshellexecasuser/compare/v1.0.0...HEAD
|
[Unreleased]: https://gitea.emulab.it/Simone/nsis-plugin-nsshellexecasuser/compare/v1.1.0...HEAD
|
||||||
|
[1.1.0]: https://gitea.emulab.it/Simone/nsis-plugin-nsshellexecasuser/compare/v1.0.0...v1.1.0
|
||||||
[1.0.0]: https://gitea.emulab.it/Simone/nsis-plugin-nsshellexecasuser/releases/tag/v1.0.0
|
[1.0.0]: https://gitea.emulab.it/Simone/nsis-plugin-nsshellexecasuser/releases/tag/v1.0.0
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# nsShellExecAsUser NSIS Plugin
|
# nsShellExecAsUser NSIS Plugin
|
||||||
|
|
||||||
**Versione personale modificata**
|
**Personal modified version**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Descrizione Originale
|
## Original Description
|
||||||
|
|
||||||
**ShellExecAsUser plug-in**
|
**ShellExecAsUser plug-in**
|
||||||
|
|
||||||
@@ -65,36 +65,52 @@ There is no wrapper macros file for this plugin. Call the plugin directly as sho
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚠️ Differenze nella versione personale
|
## ⚠️ Differences in the Personal Version
|
||||||
|
|
||||||
### Architetture supportate
|
### Supported Architectures
|
||||||
|
|
||||||
Il progetto supporta le configurazioni giĂ presenti nell'originale:
|
The project supports all configurations:
|
||||||
- **amd64-unicode** (x64)
|
- **amd64-unicode** (x64)
|
||||||
- **x86-ansi**
|
- **x86-ansi**
|
||||||
- **x86-unicode**
|
- **x86-unicode**
|
||||||
|
|
||||||
### Progetto Visual Studio
|
### Visual Studio Project
|
||||||
|
|
||||||
Il progetto è stato aggiornato e riorganizzato:
|
The project has been updated and reorganized:
|
||||||
- `Contrib/ShellExecAsUser/ShellExecAsUser.sln` - Solution VS2022
|
- `Contrib/ShellExecAsUser/ShellExecAsUser.sln` - VS2022 Solution
|
||||||
- `Contrib/ShellExecAsUser/ShellExecAsUser.vcxproj` - Progetto VS2022 aggiornato
|
- `Contrib/ShellExecAsUser/ShellExecAsUser.vcxproj` - Updated VS2022 Project
|
||||||
|
|
||||||
### File aggiunti
|
### Added Files
|
||||||
|
|
||||||
- `build_plugin.py` - Script Python per compilare il plugin per tutte le architetture supportate.
|
- `build_plugin.py` - Python script to compile the plugin for all supported architectures.
|
||||||
- `nsis/` - Header NSIS inclusi per facilitare la compilazione.
|
- `nsis/` - NSIS headers included to facilitate compilation.
|
||||||
|
|
||||||
### Modifiche al Codice
|
### Code Changes
|
||||||
|
|
||||||
- **Rimozione Warning Deprecazione**: Sostituita la chiamata deprecata `GetVersionEx` in `VistaTools.cxx` con la moderna API `VerifyVersionInfo` per evitare warning C4996 durante la compilazione con i toolset moderni.
|
- **Deprecation Warning Removal**: Replaced the deprecated `GetVersionEx` call in `VistaTools.cxx` with the modern `VerifyVersionInfo` API to avoid C4996 warnings when compiling with modern toolsets.
|
||||||
- **Aggiornamento Build**: Configurazione aggiornata per utilizzare il toolset **v143** (Visual Studio 2022).
|
- **Build Update**: Configuration updated to use the **v143** toolset (Visual Studio 2022).
|
||||||
|
|
||||||
### Compilazione
|
### Build
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
cd nsShellExecAsUser
|
cd nsShellExecAsUser
|
||||||
python build_plugin.py
|
python build_plugin.py
|
||||||
```
|
```
|
||||||
|
|
||||||
I DLL vengono copiati in `plugins/{platform}/ShellExecAsUser.dll`.
|
DLLs are copied to `plugins/{platform}/ShellExecAsUser.dll`.
|
||||||
|
|
||||||
|
### Build Options
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python build_plugin.py --config x86-unicode # Single architecture (x86-ansi|x86-unicode|amd64-unicode|all)
|
||||||
|
python build_plugin.py --toolset 2026 # Specific toolset (2022|2026|auto)
|
||||||
|
python build_plugin.py --jobs 4 # Number of parallel MSBuild jobs (default: CPU count)
|
||||||
|
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 --verbose # Extended MSBuild output
|
||||||
|
python build_plugin.py --version # Print version and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See [README_IT.md](README_IT.md) for the Italian version.*
|
||||||
|
|||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
# nsShellExecAsUser NSIS Plugin
|
||||||
|
|
||||||
|
**Versione personale modificata**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descrizione Originale
|
||||||
|
|
||||||
|
**ShellExecAsUser plug-in**
|
||||||
|
|
||||||
|
Esegue il programma specificato tramite ShellExecute.
|
||||||
|
Utile quando il processo installer è in esecuzione con privilegi elevati (UAC) ma si vuole avviare un'applicazione come l'utente standard connesso (es. avvio dell'applicazione dopo l'installazione).
|
||||||
|
|
||||||
|
## FunzionalitĂ
|
||||||
|
|
||||||
|
- Avvia applicazioni come l'utente desktop corrente
|
||||||
|
- Bypassa l'elevazione UAC se l'installer è in esecuzione come Amministratore
|
||||||
|
- Supporta i verbi standard di ShellExecute (open, runas, print, ecc.)
|
||||||
|
- Supporta le modalitĂ di visualizzazione finestra (SW_SHOWNORMAL, SW_HIDE, ecc.)
|
||||||
|
- Supporto Win95/98/ME/NT/2000/XP/Vista/7/8/10/11
|
||||||
|
- Supporto NSIS UNICODE
|
||||||
|
|
||||||
|
## Utilizzo
|
||||||
|
|
||||||
|
```nsis
|
||||||
|
ShellExecAsUser::ShellExecAsUser "azione" "comando" "parametri" "modalitĂ _finestra"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parametri:**
|
||||||
|
|
||||||
|
1. `azione` - Il verbo da usare (es. "open", "runas", "print"). Può essere vuoto `""` per l'azione default.
|
||||||
|
2. `comando` - Percorso all'eseguibile o file da aprire.
|
||||||
|
3. `parametri` - Argomenti della riga di comando.
|
||||||
|
4. `modalitĂ _finestra` - ModalitĂ di visualizzazione della finestra.
|
||||||
|
|
||||||
|
**ModalitĂ Finestra Supportate:**
|
||||||
|
|
||||||
|
- `SW_SHOWDEFAULT`
|
||||||
|
- `SW_SHOWNORMAL` (default)
|
||||||
|
- `SW_SHOWMAXIMIZED`
|
||||||
|
- `SW_SHOWMINIMIZED`
|
||||||
|
- `SW_HIDE`
|
||||||
|
|
||||||
|
**Directory di Lavoro:**
|
||||||
|
|
||||||
|
Il plugin usa la directory di output NSIS corrente (`$OUTDIR`) come directory di lavoro per il processo avviato.
|
||||||
|
|
||||||
|
## Esempio
|
||||||
|
|
||||||
|
```nsis
|
||||||
|
Section "Avvia Applicazione"
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
ShellExecAsUser::ShellExecAsUser "open" "$INSTDIR\MyApp.exe" "" "SW_SHOWNORMAL"
|
||||||
|
SectionEnd
|
||||||
|
```
|
||||||
|
|
||||||
|
Per aprire un URL nel browser predefinito come utente:
|
||||||
|
```nsis
|
||||||
|
ShellExecAsUser::ShellExecAsUser "open" "https://example.com" "" "SW_SHOWNORMAL"
|
||||||
|
```
|
||||||
|
|
||||||
|
## File include
|
||||||
|
|
||||||
|
Non esiste un file di macro wrapper per questo plugin. Chiama il plugin direttamente come mostrato nella sezione utilizzo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Differenze nella versione personale
|
||||||
|
|
||||||
|
### Architetture supportate
|
||||||
|
|
||||||
|
Il progetto supporta le configurazioni giĂ presenti nell'originale:
|
||||||
|
- **amd64-unicode** (x64)
|
||||||
|
- **x86-ansi**
|
||||||
|
- **x86-unicode**
|
||||||
|
|
||||||
|
### Progetto Visual Studio
|
||||||
|
|
||||||
|
Il progetto è stato aggiornato e riorganizzato:
|
||||||
|
- `Contrib/ShellExecAsUser/ShellExecAsUser.sln` - Solution VS2022
|
||||||
|
- `Contrib/ShellExecAsUser/ShellExecAsUser.vcxproj` - Progetto VS2022 aggiornato
|
||||||
|
|
||||||
|
### File aggiunti
|
||||||
|
|
||||||
|
- `build_plugin.py` - Script Python per compilare il plugin per tutte le architetture supportate.
|
||||||
|
- `nsis/` - Header NSIS inclusi per facilitare la compilazione.
|
||||||
|
|
||||||
|
### Modifiche al Codice
|
||||||
|
|
||||||
|
- **Rimozione Warning Deprecazione**: Sostituita la chiamata deprecata `GetVersionEx` in `VistaTools.cxx` con la moderna API `VerifyVersionInfo` per evitare warning C4996 durante la compilazione con i toolset moderni.
|
||||||
|
- **Aggiornamento Build**: Configurazione aggiornata per utilizzare il toolset **v143** (Visual Studio 2022).
|
||||||
|
|
||||||
|
### Compilazione
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
cd nsShellExecAsUser
|
||||||
|
python build_plugin.py
|
||||||
|
```
|
||||||
|
|
||||||
|
I DLL vengono copiati in `plugins/{platform}/ShellExecAsUser.dll`.
|
||||||
|
|
||||||
|
### Opzioni build
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python build_plugin.py --config x86-unicode # Solo un'architettura (x86-ansi|x86-unicode|amd64-unicode|all)
|
||||||
|
python build_plugin.py --toolset 2026 # Toolset specifico (2022|2026|auto)
|
||||||
|
python build_plugin.py --jobs 4 # Numero di job MSBuild paralleli (default: CPU count)
|
||||||
|
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 --verbose # Output MSBuild esteso
|
||||||
|
python build_plugin.py --version # Stampa versione ed esce
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See [README.md](README.md) for the English version.*
|
||||||
+632
-111
@@ -1,158 +1,679 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Build script canonico per il plugin ShellExecAsUser.
|
Build script for nsShellExecAsUser plugin - All configurations
|
||||||
|
Supports multiple build configurations with flexible parameters
|
||||||
CLI standard: vedi `python build_plugin.py --help`
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import io
|
import errno
|
||||||
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
# Ensure UTF-8 output on Windows (avoids cp1252 UnicodeEncodeError)
|
# Ensure stdout/stderr handle Unicode on Windows CI (cp1252 default breaks non-ASCII)
|
||||||
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
|
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ('utf-8', 'utf_8'):
|
||||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
import io
|
||||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
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 = {
|
CONFIGS = {
|
||||||
"x86-ansi": ("Release", "Win32", "x86-ansi"),
|
'x86-ansi': BuildConfig('x86-ansi', 'Release', 'Win32', 'x86-ansi'),
|
||||||
"x86-unicode": ("Release Unicode", "Win32", "x86-unicode"),
|
'x86-unicode': BuildConfig('x86-unicode', 'ReleaseUnicode', 'Win32', 'x86-unicode'),
|
||||||
"x64-unicode": ("Release Unicode", "x64", "amd64-unicode"),
|
'x64-unicode': BuildConfig('x64-unicode', 'ReleaseUnicode', 'x64', 'amd64-unicode'),
|
||||||
}
|
}
|
||||||
|
|
||||||
VCXPROJ = SRC_DIR / "ShellExecAsUser.vcxproj"
|
DLL_NAME = 'ShellExecAsUser.dll'
|
||||||
DLL_NAME = "ShellExecAsUser.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 / 'ShellExecAsUser.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:
|
def read_version() -> str:
|
||||||
# utf-8-sig strips BOM (\ufeff) automatically
|
vf = Path(__file__).parent / 'VERSION'
|
||||||
return VERSION_FILE.read_text(encoding="utf-8-sig").strip()
|
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
|
def get_cpu_info() -> dict:
|
||||||
PlatformToolset v143 (VS2022) which may not be available in VS2026 build tools.
|
try:
|
||||||
"""
|
import psutil
|
||||||
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
logical = psutil.cpu_count(logical=True)
|
||||||
vswhere = Path(pf86) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe"
|
physical = psutil.cpu_count(logical=False)
|
||||||
if not vswhere.exists():
|
return {'logical_cores': logical, 'physical_cores': physical,
|
||||||
sys.exit(2)
|
'has_hyperthreading': logical > physical, 'has_psutil': True}
|
||||||
|
except ImportError:
|
||||||
def _query(version_range: str | None) -> list[str]:
|
n = multiprocessing.cpu_count()
|
||||||
args = [str(vswhere), "-latest", "-products", "*",
|
return {'logical_cores': n, 'physical_cores': n,
|
||||||
"-requires", "Microsoft.Component.MSBuild",
|
'has_hyperthreading': False, 'has_psutil': False}
|
||||||
"-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 build_one(msbuild: Path, config: str, platform: str, jobs: int, verbose: bool) -> bool:
|
def get_optimal_threads() -> int:
|
||||||
cmd = [
|
info = get_cpu_info()
|
||||||
str(msbuild), str(VCXPROJ),
|
return info['physical_cores'] if info['has_hyperthreading'] and info['physical_cores'] > 1 else info['logical_cores']
|
||||||
f"/p:Configuration={config}",
|
|
||||||
f"/p:Platform={platform}",
|
|
||||||
f"/m:{jobs}",
|
def get_build_optimizations() -> List[str]:
|
||||||
"/nologo",
|
return [
|
||||||
"/v:" + ("normal" if verbose else "minimal"),
|
'/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:
|
def get_memory_optimizations() -> List[str]:
|
||||||
"""Trova la DLL prodotta dal vcxproj e la copia in dist/<dist_subdir>/."""
|
try:
|
||||||
plat_dir = "" if platform == "Win32" else platform + "/"
|
import psutil
|
||||||
candidates = list(SRC_DIR.glob(f"**/{plat_dir}{config}/{DLL_NAME}"))
|
gb = psutil.virtual_memory().total / (1024 ** 3)
|
||||||
candidates += list(ROOT.glob(f"**/{plat_dir}{config}/{DLL_NAME}"))
|
if gb >= 16:
|
||||||
if not candidates:
|
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
|
return None
|
||||||
src = candidates[0]
|
dst_dir = plugins_dir / config.dest_dir
|
||||||
dst_dir = DIST_DIR / dist_subdir
|
|
||||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||||
dst = dst_dir / DLL_NAME
|
dst = dst_dir / DLL_NAME
|
||||||
shutil.copy2(src, dst)
|
shutil.copy2(src, dst)
|
||||||
return 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:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=f"Build ShellExecAsUser plugin")
|
parser = argparse.ArgumentParser(description='Build nsShellExecAsUser plugin for NSIS')
|
||||||
parser.add_argument("--config", default="all",
|
parser.add_argument('--configs', nargs='+',
|
||||||
choices=list(CONFIGS.keys()) + ["all"])
|
choices=list(CONFIGS.keys()) + ['all'], default=None)
|
||||||
parser.add_argument("--toolset", default="auto", choices=["2022", "2026", "auto"])
|
parser.add_argument('--config', # singular alias used by CI
|
||||||
parser.add_argument("--jobs", type=int, default=os.cpu_count() or 4)
|
choices=list(CONFIGS.keys()) + ['all'], default=None)
|
||||||
parser.add_argument("--clean", action="store_true")
|
parser.add_argument('--rebuild', action='store_true', default=True)
|
||||||
parser.add_argument("--install-dir", type=Path, default=None)
|
parser.add_argument('--no-rebuild', action='store_false', dest='rebuild')
|
||||||
parser.add_argument("--verbose", action="store_true")
|
parser.add_argument('--parallel', action='store_true', default=True)
|
||||||
parser.add_argument("--version", action="store_true",
|
parser.add_argument('--no-parallel', action='store_false', dest='parallel')
|
||||||
help="Mostra versione e termina")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.version:
|
# Merge --config (singular) into --configs list
|
||||||
print(read_version())
|
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
|
return 0
|
||||||
|
|
||||||
if not VCXPROJ.exists():
|
if args.list:
|
||||||
print(f"ERROR: {VCXPROJ} not found", file=sys.stderr)
|
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
|
return 3
|
||||||
|
|
||||||
if args.clean and DIST_DIR.exists():
|
msbuild_result = find_msbuild(args.vs_version)
|
||||||
shutil.rmtree(DIST_DIR)
|
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)
|
version = read_version()
|
||||||
print(f"[info] MSBuild: {msbuild}")
|
|
||||||
print(f"[info] Version: {read_version()}")
|
|
||||||
|
|
||||||
targets = list(CONFIGS.keys()) if args.config == "all" else [args.config]
|
if not args.configs:
|
||||||
failed = []
|
configs_to_build = list(CONFIGS.values())
|
||||||
for cfg_name in targets:
|
elif 'all' in args.configs:
|
||||||
config, platform, subdir = CONFIGS[cfg_name]
|
configs_to_build = list(CONFIGS.values())
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
print(f"[ok] {dst}")
|
configs_to_build = [CONFIGS[n] for n in args.configs]
|
||||||
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}")
|
|
||||||
|
|
||||||
if failed:
|
# Banner
|
||||||
print(f"FAILED: {failed}", file=sys.stderr)
|
print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}{'='*60}")
|
||||||
return 1
|
print(f"Building nsShellExecAsUser plugin v{version} - {Colors.BRIGHT_WHITE}{len(configs_to_build)}{Colors.RESET} {Colors.BOLD}{Colors.BRIGHT_CYAN}configuration(s)")
|
||||||
print("[done] all targets built successfully")
|
print(f"{'='*60}{Colors.RESET}")
|
||||||
return 0
|
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())
|
sys.exit(main())
|
||||||
|
|||||||
Reference in New Issue
Block a user