Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/build-portable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Build portable (Windows)

# Costruisce la distribuzione portabile ZIP di Songpress++ con cx_Freeze,
# riusando lo script sanzionato installer/Build-Portable.ps1.
#
# Trigger:
# - manuale (workflow_dispatch)
# - push di un tag di versione v* -> allega lo ZIP alla Release

on:
workflow_dispatch:
push:
tags:
- 'v*'

permissions:
contents: write # necessario per creare/aggiornare la Release sui tag

jobs:
build-portable:
runs-on: windows-latest

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: '3.13'

- name: Build portable ZIP
shell: pwsh
run: .\installer\Build-Portable.ps1

- name: Locate artifact
id: artifact
shell: pwsh
run: |
$zip = Get-ChildItem -Path dist -Filter '*-portable.zip' | Select-Object -First 1
if (-not $zip) { throw "Nessuno ZIP portabile trovato in dist\" }
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
"name=$($zip.Name)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append

- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: ${{ steps.artifact.outputs.name }}
path: ${{ steps.artifact.outputs.path }}
if-no-files-found: error

- name: Attach to release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
files: dist/*-portable.zip
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ installer/~$ spiego passo.docx
/.idea/
/.vscode/

# Egg info (setuptools)
/*.egg-info/
# Egg info (setuptools / cx_Freeze) — a qualsiasi profondità (es. src/)
*.egg-info/

# Build e distribuzione
# Esclude le sottocartelle di build, non i file
Expand Down
40 changes: 28 additions & 12 deletions installer/Build-Portable.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
###############################################################
###############################################################
# Build-Portable.ps1
# Crea una distribuzione portabile ZIP di Songpress++
# usando cx_Freeze in un venv dedicato.
Expand Down Expand Up @@ -55,17 +55,25 @@ $Python = Join-Path $VenvDir 'Scripts\python.exe'

Write-Host "[2/6] Installazione dipendenze (può richiedere alcuni minuti)..." -ForegroundColor Yellow

# Le specifiche di versione vanno tenute come elementi di un array e passate
# con lo splatting (@Deps): la continuazione con backtick di stringhe
# contenenti '<' e ',' non viene analizzata correttamente da Windows
# PowerShell 5.1 ("The '<' operator is reserved for future use").
$Deps = @(
'cx_Freeze'
'wxPython>=4.2.4,<5.0.0'
'requests>=2.32.4,<3.0.0'
'python-pptx>=1.0.2,<2.0.0'
'pyshortcuts>=1.9.5,<2.0.0'
'reportlab>=4.0.0,<5.0.0'
'pypdf>=6.0.0,<7.0.0'
'markdown>=3.4,<4.0.0'
'mistune>=3.0.0,<4.0.0'
'pywin32>=308'
)

& $Pip install --upgrade pip --quiet
& $Pip install --quiet `
cx_Freeze `
"wxPython>=4.2.4,<5.0.0" `
"requests>=2.32.4,<3.0.0" `
"python-pptx>=1.0.2,<2.0.0" `
"pyshortcuts>=1.9.5,<2.0.0" `
"reportlab>=4.0.0,<5.0.0" `
"pypdf>=6.0.0,<7.0.0" `
"markdown>=3.4,<4.0.0" `
"mistune>=3.0.0,<4.0.0"
& $Pip install --quiet @Deps

if ($LASTEXITCODE -ne 0) { throw "pip install fallito (codice $LASTEXITCODE)" }
Write-Host " Dipendenze installate." -ForegroundColor Green
Expand All @@ -74,12 +82,20 @@ Write-Host " Dipendenze installate." -ForegroundColor Green

Write-Host "[3/6] Esecuzione cx_Freeze build_exe..." -ForegroundColor Yellow

# cx_Freeze deve poter importare il package 'songpressPlusPlus' (elencato in
# [tool.cxfreeze.build_exe] packages): va reso individuabile aggiungendo src/
# a PYTHONPATH, altrimenti il finder fallisce con "No module named ...".
$SrcDir = Join-Path $ProjectRoot 'src'
$OldPyPath = $env:PYTHONPATH
$env:PYTHONPATH = if ($OldPyPath) { "$SrcDir;$OldPyPath" } else { $SrcDir }

Push-Location $ProjectRoot
try {
& $Python -m cx_Freeze build_exe
if ($LASTEXITCODE -ne 0) { throw "cx_Freeze fallito (codice $LASTEXITCODE)" }
} finally {
Pop-Location
$env:PYTHONPATH = $OldPyPath
}
Write-Host " Build completata." -ForegroundColor Green

Expand All @@ -94,7 +110,7 @@ Write-Host " Cartella build: $($BuildOutput.FullName)"

Write-Host "[4/6] Verifica cartella templates\fonts..." -ForegroundColor Yellow

$SrcFonts = Join-Path $ProjectRoot 'src\songpress\templates\fonts'
$SrcFonts = Join-Path $ProjectRoot 'src\songpressPlusPlus\templates\fonts'
$DestFonts = Join-Path $BuildOutput.FullName 'templates\fonts'

if (Test-Path $SrcFonts) {
Expand Down
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,19 @@ artifacts = [
# python -m cx_Freeze build_exe

[[tool.cxfreeze.executables]]
script = "src/songpressplusplus/main.py"
# Non congelare main.py direttamente: usa import relativi di pacchetto e
# come script __main__ fallirebbe ("attempted relative import with no known
# parent package"). Il launcher importa il package e ne chiama main().
script = "src/spp_frozen_main.py"
target_name = "SongpressPlusPlus.exe"
base = "gui"
icon = "installer/songpressplusplus.ico"

[tool.cxfreeze.build_exe]
packages = [
# Case identico alla cartella reale src/songpressPlusPlus: gli import
# Python sono case-sensitive anche su Windows.
"songpressPlusPlus",
"multiprocessing",
"idna.idnadata",
"ssl",
Expand All @@ -92,4 +98,10 @@ include_files = [
["src/songpressplusplus/templates", "templates"],
["src/songpressplusplus/xrc", "xrc"],
["pyproject.toml", "pyproject.toml"],
# File markdown della guida rapida (Help): caricati a runtime da
# SongpressFrame via glb.path, quindi devono stare accanto all'exe.
["src/songpressplusplus/guida.md", "guida.md"],
["src/songpressplusplus/guida_en.md", "guida_en.md"],
["src/songpressplusplus/guida_comandi_songpress.md", "guida_comandi_songpress.md"],
["src/songpressplusplus/guida_comandi_songpress_en.md", "guida_comandi_songpress_en.md"],
]
5 changes: 4 additions & 1 deletion src/songpressPlusPlus/Editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,10 @@ def OnContextMenu(self, evt):
menu = wx.Menu()

import os
_img_dir = os.path.join(os.path.dirname(__file__), 'img')
from .Globals import glb
# Usa glb (frozen-aware) invece di __file__: nella build congelata
# img/ è nella cartella dell'exe, non dentro lib/songpressPlusPlus/.
_img_dir = glb.AddPath('img')
img = lambda name: os.path.join(_img_dir, name)

# Recupera le preferenze di visibilità (default True se non presenti)
Expand Down
12 changes: 10 additions & 2 deletions src/songpressPlusPlus/Globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,16 @@ class Globals(object):

def __init__(self):
object.__init__(self)
current_file = os.path.abspath(__file__)
self.path = os.path.dirname(current_file)
if getattr(sys, 'frozen', False):
# Build congelata (cx_Freeze): le risorse (xrc/, img/, templates/,
# locale/) vengono copiate da include_files nella cartella
# dell'eseguibile, non dentro lib/songpressPlusPlus/. Usare la dir
# dell'exe come radice risorse; __file__ punterebbe invece dentro
# lib/ dove xrc/ ecc. non esistono → wxDir::Open fallisce.
self.path = os.path.dirname(os.path.abspath(sys.executable))
else:
current_file = os.path.abspath(__file__)
self.path = os.path.dirname(current_file)
self.data_path = None

def InitDataPath(self):
Expand Down
4 changes: 3 additions & 1 deletion src/songpressPlusPlus/PreferencesDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ def __init__(self, parent, pref=None):
self.themeDeleteBtn = wx.Button(self.general, wx.ID_ANY, _(u"Delete"), wx.DefaultPosition, wx.Size(75, -1))
# Icone pulsanti tema
import os as _os
_icons_dir = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'img')
from .Globals import glb
# Frozen-aware: nella build congelata img/ è nella cartella dell'exe.
_icons_dir = glb.AddPath('img')
def _load_btn_icon(filename):
path = _os.path.join(_icons_dir, filename)
if _os.path.isfile(path):
Expand Down
8 changes: 6 additions & 2 deletions src/songpressPlusPlus/SongpressFrame.py
Original file line number Diff line number Diff line change
Expand Up @@ -6522,7 +6522,9 @@ def OnGuideMarkdown(self, evt):
if canonical:
lang_code = canonical.split('_')[0].lower() # → "it", "en", "fr"

base_dir = os.path.dirname(__file__)
# Frozen-aware: nella build congelata guida_*.md e img/GUIDE sono
# nella cartella dell'exe (glb.path), non dentro lib/songpressPlusPlus/.
base_dir = glb.path
_img_guide_url = 'file:///' + os.path.abspath(os.path.join(base_dir, 'img', 'GUIDE')).replace('\\', '/').lstrip('/') + '/'

# Priorità: guida_<lang>.md → guida.md → errore
Expand Down Expand Up @@ -7374,7 +7376,9 @@ def OnGuideCommandsMarkdown(self, evt):
if canonical:
lang_code = canonical.split('_')[0].lower()

base_dir = os.path.dirname(__file__)
# Frozen-aware: nella build congelata guida_*.md e img/GUIDE sono
# nella cartella dell'exe (glb.path), non dentro lib/songpressPlusPlus/.
base_dir = glb.path
_img_guide_url = 'file:///' + os.path.abspath(os.path.join(base_dir, 'img', 'GUIDE')).replace('\\', '/').lstrip('/') + '/'

# Priorità: guida_comandi_songpress_<lang>.md → guida_comandi_songpress.md → errore
Expand Down
6 changes: 6 additions & 0 deletions src/songpressPlusPlus/songimpress.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ def close(self):
self._add_slide(self.prev, '')
self.pres.save(self.out_filename)

def __enter__(self):
# Il blocco `with SongPresentation(...) as c` richiede __enter__:
# senza, Python solleva TypeError ("does not support the context
# manager protocol") prima ancora di eseguire il corpo.
return self

def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
# Exception in progress: save what we have but signal it to the caller
Expand Down
29 changes: 29 additions & 0 deletions src/spp_frozen_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
###############################################################
# spp_frozen_main.py
# Entry point per la build congelata (cx_Freeze).
#
# main.py usa import relativi di pacchetto (`from .Globals import glb`),
# quindi NON può essere congelato come script __main__ di primo livello:
# in quel contesto __package__ è vuoto e ogni import relativo fallisce con
# "attempted relative import with no known parent package".
#
# Questo launcher importa il package `songpressplusplus` (cx_Freeze aggiunge
# la cartella di questo script — src/ — al percorso di ricerca dei moduli,
# rendendo il package individuabile) e ne invoca main() con il corretto
# contesto di pacchetto.
###############################################################

import multiprocessing

# Nota: il nome del package rispecchia la cartella reale su disco
# (src/songpressPlusPlus). Gli import interni sono relativi, quindi il
# nome di primo livello è quello della directory. Python impone la
# corrispondenza esatta del case anche su Windows: usare la stessa
# capitalizzazione della cartella è indispensabile.
from songpressPlusPlus.main import main

if __name__ == "__main__":
# Necessario nelle build congelate su Windows: evita che i processi
# figli di multiprocessing rilancino l'intera GUI.
multiprocessing.freeze_support()
main()