Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dd793253b |
No files matched your search
+107
-1
@@ -239,6 +239,7 @@ def run_pyinstaller():
|
|||||||
if app_path.exists():
|
if app_path.exists():
|
||||||
log(f"Build completata: {app_path}")
|
log(f"Build completata: {app_path}")
|
||||||
patch_info_plist(app_path)
|
patch_info_plist(app_path)
|
||||||
|
wrap_subprocess_in_bundle(app_path)
|
||||||
adhoc_codesign(app_path)
|
adhoc_codesign(app_path)
|
||||||
# Mostra dimensione
|
# Mostra dimensione
|
||||||
size = sum(f.stat().st_size for f in app_path.rglob("*") if f.is_file())
|
size = sum(f.stat().st_size for f in app_path.rglob("*") if f.is_file())
|
||||||
@@ -296,6 +297,85 @@ def patch_info_plist(app_path):
|
|||||||
_BUNDLE_ID = "com.djluza.musictools"
|
_BUNDLE_ID = "com.djluza.musictools"
|
||||||
|
|
||||||
|
|
||||||
|
def _subprocess_info_plist(executable, bundle_id):
|
||||||
|
"""Info.plist per un mini-bundle che incapsula un binario subprocess.
|
||||||
|
|
||||||
|
NSMicrophoneUsageDescription qui dentro e' la chiave: senza di essa,
|
||||||
|
macOS uccide il subprocess con SIGABRT appena tenta di aprire un
|
||||||
|
device audio, anche se il main MusicTools ha il proprio TCC concesso.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" '
|
||||||
|
'"http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
|
||||||
|
'<plist version="1.0">\n'
|
||||||
|
'<dict>\n'
|
||||||
|
f' <key>CFBundleExecutable</key><string>{executable}</string>\n'
|
||||||
|
f' <key>CFBundleIdentifier</key><string>{bundle_id}</string>\n'
|
||||||
|
f' <key>CFBundleName</key><string>{executable}</string>\n'
|
||||||
|
' <key>CFBundlePackageType</key><string>APPL</string>\n'
|
||||||
|
' <key>CFBundleShortVersionString</key><string>1.0</string>\n'
|
||||||
|
' <key>CFBundleVersion</key><string>1</string>\n'
|
||||||
|
' <key>LSBackgroundOnly</key><true/>\n'
|
||||||
|
' <key>LSUIElement</key><true/>\n'
|
||||||
|
' <key>NSMicrophoneUsageDescription</key>\n'
|
||||||
|
' <string>MusicTools usa il microfono per registrare l\'audio del '
|
||||||
|
'sistema (BlackHole o altri dispositivi loopback).</string>\n'
|
||||||
|
'</dict>\n'
|
||||||
|
'</plist>\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_subprocess_in_bundle(app_path):
|
||||||
|
"""Sposta ffmpeg e ffprobe in mini-bundle .app dentro Frameworks/.
|
||||||
|
|
||||||
|
macOS 14+ ENFORCE il fatto che ogni binario che apre device
|
||||||
|
privacy-sensitive (microfono, camera, ...) debba avere un proprio
|
||||||
|
Info.plist accessibile con NSMicrophoneUsageDescription. Quando
|
||||||
|
ffmpeg viene lanciato come binario standalone (anche se figlio di
|
||||||
|
MusicTools), non c'e' Info.plist associato -> SIGABRT.
|
||||||
|
|
||||||
|
Soluzione: incapsulare ffmpeg in MusicTools.app/Contents/Frameworks/
|
||||||
|
ffmpeg.app/Contents/MacOS/ffmpeg, con Info.plist nel suo bundle.
|
||||||
|
Stesso per ffprobe.
|
||||||
|
"""
|
||||||
|
log("Wrapping ffmpeg e ffprobe in sub-bundle .app...")
|
||||||
|
fw = app_path / "Contents" / "Frameworks"
|
||||||
|
if not fw.exists():
|
||||||
|
log("ATTENZIONE: Contents/Frameworks non trovato, skip wrapping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for name in ("ffmpeg", "ffprobe"):
|
||||||
|
src = fw / name
|
||||||
|
if not src.exists() or not src.is_file():
|
||||||
|
log(f" skip {name}: non trovato in Frameworks/")
|
||||||
|
continue
|
||||||
|
wrap = fw / f"{name}.app"
|
||||||
|
if wrap.exists():
|
||||||
|
shutil.rmtree(wrap)
|
||||||
|
macos = wrap / "Contents" / "MacOS"
|
||||||
|
macos.mkdir(parents=True, exist_ok=True)
|
||||||
|
dest = macos / name
|
||||||
|
shutil.move(str(src), str(dest))
|
||||||
|
dest.chmod(0o755)
|
||||||
|
|
||||||
|
# Aggiungi un nuovo rpath che punta a Contents/Frameworks/ (dove
|
||||||
|
# stanno le dylib del bundle MusicTools).
|
||||||
|
# @executable_path = ffmpeg.app/Contents/MacOS/
|
||||||
|
# ../../../ = MusicTools.app/Contents/Frameworks/
|
||||||
|
subprocess.run(
|
||||||
|
["install_name_tool", "-add_rpath",
|
||||||
|
"@executable_path/../../../", str(dest)],
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Crea Info.plist con NSMicrophoneUsageDescription.
|
||||||
|
info = wrap / "Contents" / "Info.plist"
|
||||||
|
bundle_id = f"com.djluza.musictools.{name}"
|
||||||
|
info.write_text(_subprocess_info_plist(name, bundle_id))
|
||||||
|
log(f" wrap: {name} -> {dest.relative_to(app_path)}")
|
||||||
|
|
||||||
|
|
||||||
def adhoc_codesign(app_path):
|
def adhoc_codesign(app_path):
|
||||||
"""Ad-hoc codesign dell'intero bundle .app con identifier coerente.
|
"""Ad-hoc codesign dell'intero bundle .app con identifier coerente.
|
||||||
|
|
||||||
@@ -340,9 +420,19 @@ def adhoc_codesign(app_path):
|
|||||||
log(f"Trovati {len(nested_machos)} binari Mach-O da firmare.")
|
log(f"Trovati {len(nested_machos)} binari Mach-O da firmare.")
|
||||||
|
|
||||||
for p in nested_machos:
|
for p in nested_machos:
|
||||||
|
# Per ffmpeg/ffprobe dentro al sub-bundle .app, usa l'identifier
|
||||||
|
# del SUB-bundle (TCC li tratta come app separate con Info.plist
|
||||||
|
# proprio). Per gli altri binari nested (dylib, framework), usa
|
||||||
|
# l'identifier del bundle principale.
|
||||||
|
ident = _BUNDLE_ID
|
||||||
|
parts = p.relative_to(app_path).parts
|
||||||
|
for sub in ("ffmpeg.app", "ffprobe.app"):
|
||||||
|
if sub in parts:
|
||||||
|
ident = f"{_BUNDLE_ID}.{sub.replace('.app', '')}"
|
||||||
|
break
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
["codesign", "--force", "--sign", "-",
|
["codesign", "--force", "--sign", "-",
|
||||||
"--identifier", _BUNDLE_ID,
|
"--identifier", ident,
|
||||||
"--timestamp=none",
|
"--timestamp=none",
|
||||||
str(p)],
|
str(p)],
|
||||||
capture_output=True, text=True,
|
capture_output=True, text=True,
|
||||||
@@ -350,6 +440,22 @@ def adhoc_codesign(app_path):
|
|||||||
if r.returncode != 0:
|
if r.returncode != 0:
|
||||||
log(f" warn: firma {p.name} fallita: {r.stderr.strip()}")
|
log(f" warn: firma {p.name} fallita: {r.stderr.strip()}")
|
||||||
|
|
||||||
|
# Firma anche i sub-bundle .app come bundle (con il loro Info.plist).
|
||||||
|
for sub in ("ffmpeg.app", "ffprobe.app"):
|
||||||
|
sub_path = app_path / "Contents" / "Frameworks" / sub
|
||||||
|
if not sub_path.exists():
|
||||||
|
continue
|
||||||
|
sub_ident = f"{_BUNDLE_ID}.{sub.replace('.app', '')}"
|
||||||
|
r = subprocess.run(
|
||||||
|
["codesign", "--force", "--sign", "-",
|
||||||
|
"--identifier", sub_ident,
|
||||||
|
"--timestamp=none",
|
||||||
|
str(sub_path)],
|
||||||
|
capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
log(f" warn: firma {sub} fallita: {r.stderr.strip()}")
|
||||||
|
|
||||||
# 3) Firma del bundle .app principale (l'identifier corretto viene
|
# 3) Firma del bundle .app principale (l'identifier corretto viene
|
||||||
# letto dall'Info.plist - CFBundleIdentifier che gia' settiamo).
|
# letto dall'Info.plist - CFBundleIdentifier che gia' settiamo).
|
||||||
r = subprocess.run(
|
r = subprocess.run(
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VERSION = "v1.7.12"
|
VERSION = "v1.7.13"
|
||||||
|
|
||||||
|
|
||||||
APP_NAME = "MusicTools"
|
APP_NAME = "MusicTools"
|
||||||
|
|||||||
+31
-34
@@ -33,11 +33,18 @@ def _bundle_dirs() -> list[Path]:
|
|||||||
# Directory dell'eseguibile
|
# Directory dell'eseguibile
|
||||||
exe_dir = Path(sys.executable).parent
|
exe_dir = Path(sys.executable).parent
|
||||||
dirs.append(exe_dir)
|
dirs.append(exe_dir)
|
||||||
# macOS: Contents/Frameworks/ (PyInstaller onedir .app)
|
# macOS: Contents/Frameworks/ (PyInstaller onedir .app) + sub-bundle
|
||||||
|
# ffmpeg.app / ffprobe.app dove sono incapsulati per TCC.
|
||||||
if not _IS_WINDOWS:
|
if not _IS_WINDOWS:
|
||||||
frameworks = exe_dir.parent / "Frameworks"
|
frameworks = exe_dir.parent / "Frameworks"
|
||||||
if frameworks.exists():
|
if frameworks.exists():
|
||||||
dirs.append(frameworks)
|
dirs.append(frameworks)
|
||||||
|
# Mini-bundle .app per i subprocess (vedi build_macos.py
|
||||||
|
# wrap_subprocess_in_bundle).
|
||||||
|
for sub in ("ffmpeg.app", "ffprobe.app"):
|
||||||
|
sub_macos = frameworks / sub / "Contents" / "MacOS"
|
||||||
|
if sub_macos.exists():
|
||||||
|
dirs.append(sub_macos)
|
||||||
return dirs
|
return dirs
|
||||||
|
|
||||||
|
|
||||||
@@ -96,55 +103,45 @@ def find_ytdlp() -> str:
|
|||||||
raise FileNotFoundError(msg)
|
raise FileNotFoundError(msg)
|
||||||
|
|
||||||
|
|
||||||
def find_ffmpeg_dir() -> Optional[str]:
|
def _find_binary(name: str) -> Optional[str]:
|
||||||
"""Trova la directory contenente ffmpeg e ffprobe.
|
"""Trova un binario (ffmpeg o ffprobe) nelle dir del bundle o nel sistema."""
|
||||||
|
exe_name = _exe(name)
|
||||||
Ordine di ricerca:
|
# 1. Bundle (incluso eventuali sub-bundle .app su macOS)
|
||||||
1. Bundle PyInstaller
|
|
||||||
2. Percorsi noti per OS
|
|
||||||
3. Qualsiasi posizione nel PATH
|
|
||||||
"""
|
|
||||||
ffmpeg_name = _exe("ffmpeg")
|
|
||||||
|
|
||||||
# 1. Bundle
|
|
||||||
for d in _bundle_dirs():
|
for d in _bundle_dirs():
|
||||||
if (d / ffmpeg_name).exists():
|
candidate = d / exe_name
|
||||||
return str(d)
|
if candidate.exists():
|
||||||
|
return str(candidate)
|
||||||
# 2. Percorsi noti
|
# 2. Percorsi noti per OS
|
||||||
if _IS_WINDOWS:
|
if _IS_WINDOWS:
|
||||||
# Posizioni comuni su Windows
|
|
||||||
for search in (
|
for search in (
|
||||||
Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "ffmpeg" / "bin",
|
Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "ffmpeg" / "bin",
|
||||||
Path("C:/ffmpeg/bin"),
|
Path("C:/ffmpeg/bin"),
|
||||||
Path(os.environ.get("ProgramFiles", "")) / "ffmpeg" / "bin",
|
Path(os.environ.get("ProgramFiles", "")) / "ffmpeg" / "bin",
|
||||||
):
|
):
|
||||||
if (search / "ffmpeg.exe").exists():
|
cand = search / exe_name
|
||||||
return str(search)
|
if cand.exists():
|
||||||
|
return str(cand)
|
||||||
else:
|
else:
|
||||||
for search_path in ("/opt/homebrew/bin", "/usr/local/bin"):
|
for search_path in ("/opt/homebrew/bin", "/usr/local/bin"):
|
||||||
if (Path(search_path) / "ffmpeg").exists():
|
cand = Path(search_path) / exe_name
|
||||||
return search_path
|
if cand.exists():
|
||||||
|
return str(cand)
|
||||||
# 3. PATH generico
|
# 3. PATH generico
|
||||||
ffmpeg = shutil.which("ffmpeg")
|
found = shutil.which(name)
|
||||||
if ffmpeg:
|
return found if found else None
|
||||||
return str(Path(ffmpeg).parent)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
def find_ffmpeg_dir() -> Optional[str]:
|
||||||
|
"""Ritorna la directory contenente ffmpeg (utile per PATH env)."""
|
||||||
|
ff = _find_binary("ffmpeg")
|
||||||
|
return str(Path(ff).parent) if ff else None
|
||||||
|
|
||||||
|
|
||||||
def find_ffmpeg() -> Optional[str]:
|
def find_ffmpeg() -> Optional[str]:
|
||||||
"""Ritorna il path completo di ffmpeg."""
|
"""Ritorna il path completo di ffmpeg."""
|
||||||
d = find_ffmpeg_dir()
|
return _find_binary("ffmpeg")
|
||||||
if d:
|
|
||||||
return str(Path(d) / _exe("ffmpeg"))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def find_ffprobe() -> Optional[str]:
|
def find_ffprobe() -> Optional[str]:
|
||||||
"""Ritorna il path completo di ffprobe."""
|
"""Ritorna il path completo di ffprobe."""
|
||||||
d = find_ffmpeg_dir()
|
return _find_binary("ffprobe")
|
||||||
if d:
|
|
||||||
return str(Path(d) / _exe("ffprobe"))
|
|
||||||
return None
|
|
||||||
Reference in new issue
Block a user