Compare commits
3
Commits
fb191449d1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb059ac20d | ||
|
|
5e2fab7fd3 | ||
|
|
0ec5de50bb |
No files matched your search
@@ -7,6 +7,28 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.4] – 2026-11-03
|
||||||
|
|
||||||
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- automatische Update Funktion
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.3.4] – 2026-11-03
|
||||||
|
|
||||||
|
### Aktualisiert
|
||||||
|
- Template PDF erneuert. Neues und altes PDF sind nun enthalten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.3.3] – 2026-10-07
|
||||||
|
|
||||||
|
### Behoben
|
||||||
|
|
||||||
|
- internal development changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.3.2] – 2026-10-07
|
## [1.3.2] – 2026-10-07
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ Um das Programm zu starten: Doppelklick auf `Tätigkeitsnachweis-Generator.exe`
|
|||||||
5. **"Tätigkeitsnachweise generieren"** klicken
|
5. **"Tätigkeitsnachweise generieren"** klicken
|
||||||
- Die generierten Dateien werden im `output/`-Ordner abgelegt
|
- Die generierten Dateien werden im `output/`-Ordner abgelegt
|
||||||
|
|
||||||
|
## Update
|
||||||
|
|
||||||
|
Liegt eine neuere Version auf dem Laufwerk J:, erscheint oben rechts ein Hinweis.
|
||||||
|
|
||||||
|
- **„Anwendung ersetzen“** richtet die neue Version in einem eigenen Ordner neben der
|
||||||
|
aktuellen ein (z. B. `Tätigkeitsnachweis-Generator v1.4.0`), übernimmt `input/`,
|
||||||
|
`output/` und eigene Template-Dateien und startet sie. Das folgende Rückfragefenster
|
||||||
|
mit „Ja“ bestätigen, dann wird die alte Version beendet.
|
||||||
|
- Die alte Version bleibt als Ordner erhalten und kann gelöscht werden, sobald die neue läuft.
|
||||||
|
- Wurde `settings.ini` angepasst, liegt die bisherige Datei als
|
||||||
|
`config/settings.vorherige-version.ini` daneben.
|
||||||
|
- Desktop-Verknüpfungen zeigen weiterhin auf den alten Ordner und müssen neu angelegt werden.
|
||||||
|
- **„Update Ordner öffnen“** öffnet das Verzeichnis auf J: für ein manuelles Update.
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
Alle Einstellungen liegen im `config/`-Ordner:
|
Alle Einstellungen liegen im `config/`-Ordner:
|
||||||
|
|||||||
+33
-4
@@ -30,6 +30,7 @@ from src.parser import parse_input_file, group_by_employee, get_unique_antragsnu
|
|||||||
from src.validator import validate_input_file, validate_template
|
from src.validator import validate_input_file, validate_template
|
||||||
from src.writer import generate_activity_reports
|
from src.writer import generate_activity_reports
|
||||||
from version_check import UPDATE_DIR, get_latest_version
|
from version_check import UPDATE_DIR, get_latest_version
|
||||||
|
from update_installer import can_replace_application, install_update, start_application
|
||||||
|
|
||||||
|
|
||||||
def open_input_folder(directory: Path):
|
def open_input_folder(directory: Path):
|
||||||
@@ -91,7 +92,7 @@ try:
|
|||||||
local_version, latest_version = get_latest_version()
|
local_version, latest_version = get_latest_version()
|
||||||
current_version_text = f"v{'.'.join(map(str, local_version))}"
|
current_version_text = f"v{'.'.join(map(str, local_version))}"
|
||||||
if latest_version:
|
if latest_version:
|
||||||
available_version, _ = latest_version
|
available_version, update_path = latest_version
|
||||||
version_text = ".".join(map(str, available_version))
|
version_text = ".".join(map(str, available_version))
|
||||||
st.html(
|
st.html(
|
||||||
f'''<style>
|
f'''<style>
|
||||||
@@ -105,7 +106,7 @@ try:
|
|||||||
#update-notice-dismiss:checked + .update-notice {{ display:none; }}
|
#update-notice-dismiss:checked + .update-notice {{ display:none; }}
|
||||||
body:has(#update-notice-dismiss:checked) div.st-key-update_notice_open {{ display:none!important; }}
|
body:has(#update-notice-dismiss:checked) div.st-key-update_notice_open {{ display:none!important; }}
|
||||||
.update-notice__actions {{ display:flex; justify-content:flex-end; gap:8px; margin-top:8px; }}
|
.update-notice__actions {{ display:flex; justify-content:flex-end; gap:8px; margin-top:8px; }}
|
||||||
.update-notice__actions--footer {{ min-height:26px; }}
|
.update-notice__actions--footer {{ min-height:60px; }}
|
||||||
.update-notice__button {{
|
.update-notice__button {{
|
||||||
display:inline-flex; align-items:center; justify-content:center;
|
display:inline-flex; align-items:center; justify-content:center;
|
||||||
width:26px; height:26px; color:#5f4700; background:#fffdf3;
|
width:26px; height:26px; color:#5f4700; background:#fffdf3;
|
||||||
@@ -114,11 +115,12 @@ try:
|
|||||||
}}
|
}}
|
||||||
.update-notice__button:hover {{ background:#f6e7b2; }}
|
.update-notice__button:hover {{ background:#f6e7b2; }}
|
||||||
div.st-key-update_notice_open {{
|
div.st-key-update_notice_open {{
|
||||||
position:fixed; top:8.5rem; right:3.75rem; z-index:10001; width:26px;
|
position:fixed; top:8.5rem; right:calc(1rem + 13px); z-index:10001;
|
||||||
|
width:264px; gap:6px;
|
||||||
}}
|
}}
|
||||||
div.st-key-update_notice_open div[data-testid="stButton"] {{ margin:0; }}
|
div.st-key-update_notice_open div[data-testid="stButton"] {{ margin:0; }}
|
||||||
div.st-key-update_notice_open div[data-testid="stButton"] button {{
|
div.st-key-update_notice_open div[data-testid="stButton"] button {{
|
||||||
min-width:200px; min-height:26px; height:26px; padding:0; color:#5f4700;
|
width:100%; min-height:26px; height:26px; padding:0; color:#5f4700;
|
||||||
background:#fffdf3; border:1px solid #d9bd66; border-radius:5px;
|
background:#fffdf3; border:1px solid #d9bd66; border-radius:5px;
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
@@ -136,6 +138,33 @@ try:
|
|||||||
if st.button("Update Ordner öffnen 📁", key="open_update_directory", help="Update-Ordner öffnen"):
|
if st.button("Update Ordner öffnen 📁", key="open_update_directory", help="Update-Ordner öffnen"):
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
os.startfile(str(UPDATE_DIR))
|
os.startfile(str(UPDATE_DIR))
|
||||||
|
replace_supported = can_replace_application()
|
||||||
|
replace_clicked = st.button(
|
||||||
|
"Anwendung ersetzen 🔄", key="replace_application",
|
||||||
|
disabled=not replace_supported or st.session_state.get("update_started", False),
|
||||||
|
help=("Neue Version neben der aktuellen einrichten, input/ und output/ übernehmen "
|
||||||
|
"und starten. Die aktuelle Version bleibt als Ordner erhalten."
|
||||||
|
if replace_supported else "Nur in der installierten Windows-Anwendung verfügbar."))
|
||||||
|
if replace_clicked:
|
||||||
|
try:
|
||||||
|
with st.spinner(f"Version v{version_text} wird eingerichtet …"):
|
||||||
|
result = install_update(update_path, PROJECT_ROOT)
|
||||||
|
start_application(result.target_dir)
|
||||||
|
except Exception as exc: # Der umgebende Block verschluckt sonst jeden Fehler.
|
||||||
|
st.error(f"Update fehlgeschlagen: {exc}\n\n"
|
||||||
|
"Alternativ das Update manuell aus dem Update-Ordner einrichten.")
|
||||||
|
else:
|
||||||
|
st.session_state.update_started = True
|
||||||
|
details = [f"Neuer Programmordner: `{result.target_dir}`"]
|
||||||
|
if result.already_installed:
|
||||||
|
details.append("Die Version war dort bereits eingerichtet und wurde nur gestartet.")
|
||||||
|
else:
|
||||||
|
details.append("Die Ordner input/ und output/ wurden übernommen.")
|
||||||
|
if result.previous_settings:
|
||||||
|
details.append(f"Die bisherige settings.ini wurde als `{result.previous_settings.name}` aufbewahrt.")
|
||||||
|
details.append("Bitte im folgenden Fenster bestätigen, dass die alte Version beendet wird. "
|
||||||
|
"Desktop-Verknüpfungen zeigen noch auf den alten Ordner.")
|
||||||
|
st.success(f"Version v{version_text} wird gestartet.\n\n" + "\n\n".join(details))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""Tests für das Einrichten einer neuen Version neben der aktuellen Installation."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
import update_installer
|
||||||
|
from update_installer import EXE_NAME, UpdateError, install_update
|
||||||
|
|
||||||
|
|
||||||
|
class InstallUpdateTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self._temporary = tempfile.TemporaryDirectory()
|
||||||
|
self.root = Path(self._temporary.name).resolve()
|
||||||
|
self.current = self.root / "Tätigkeitsnachweis-Generator v1.3.3"
|
||||||
|
for folder in ["input", "output/2026", "config", "template"]:
|
||||||
|
(self.current / folder).mkdir(parents=True)
|
||||||
|
(self.current / EXE_NAME).write_bytes(b"old")
|
||||||
|
(self.current / "input" / "daten.xlsx").write_bytes(b"input")
|
||||||
|
(self.current / "output" / "2026" / "nachweis.pdf").write_bytes(b"output")
|
||||||
|
(self.current / "config" / "settings.ini").write_bytes(b"[a]\nx=1\n")
|
||||||
|
(self.current / "template" / "vorlage.pdf").write_bytes(b"old template")
|
||||||
|
(self.current / "template" / "eigene.pdf").write_bytes(b"own template")
|
||||||
|
self.share = self.root / "share"
|
||||||
|
self.share.mkdir()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self._temporary.cleanup()
|
||||||
|
|
||||||
|
def make_zip(self, files=None, version="1.4.0"):
|
||||||
|
files = files if files is not None else {
|
||||||
|
EXE_NAME: b"new",
|
||||||
|
"config/settings.ini": b"[a]\nx=1\n",
|
||||||
|
"template/vorlage.pdf": b"new template",
|
||||||
|
"input/": b"", "output/": b"",
|
||||||
|
}
|
||||||
|
path = self.share / f"Tätigkeitsnachweis-Generator v{version}.zip"
|
||||||
|
with zipfile.ZipFile(path, "w") as package:
|
||||||
|
for name, content in files.items():
|
||||||
|
package.writestr(name, content)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_installs_next_to_current_and_copies_user_data(self):
|
||||||
|
result = install_update(self.make_zip(), self.current)
|
||||||
|
target = self.root / "Tätigkeitsnachweis-Generator v1.4.0"
|
||||||
|
self.assertEqual(result.target_dir, target)
|
||||||
|
self.assertFalse(result.already_installed)
|
||||||
|
self.assertIsNone(result.previous_settings)
|
||||||
|
self.assertEqual((target / EXE_NAME).read_bytes(), b"new")
|
||||||
|
self.assertEqual((target / "input" / "daten.xlsx").read_bytes(), b"input")
|
||||||
|
self.assertEqual((target / "output" / "2026" / "nachweis.pdf").read_bytes(), b"output")
|
||||||
|
self.assertEqual((target / "template" / "vorlage.pdf").read_bytes(), b"new template")
|
||||||
|
self.assertEqual((target / "template" / "eigene.pdf").read_bytes(), b"own template")
|
||||||
|
# Die alte Installation bleibt unverändert.
|
||||||
|
self.assertEqual((self.current / EXE_NAME).read_bytes(), b"old")
|
||||||
|
self.assertEqual([p.name for p in self.root.iterdir() if p.name.startswith(".")], [])
|
||||||
|
|
||||||
|
def test_keeps_changed_settings_next_to_new_ones(self):
|
||||||
|
(self.current / "config" / "settings.ini").write_text("[a]\nx=2\n", encoding="utf-8")
|
||||||
|
result = install_update(self.make_zip(), self.current)
|
||||||
|
config = result.target_dir / "config"
|
||||||
|
self.assertEqual((config / "settings.ini").read_text(encoding="utf-8"), "[a]\nx=1\n")
|
||||||
|
self.assertEqual(result.previous_settings, config / "settings.vorherige-version.ini")
|
||||||
|
self.assertEqual(result.previous_settings.read_text(encoding="utf-8"), "[a]\nx=2\n")
|
||||||
|
|
||||||
|
def test_existing_installation_is_only_reused(self):
|
||||||
|
zip_path = self.make_zip()
|
||||||
|
install_update(zip_path, self.current)
|
||||||
|
result = install_update(zip_path, self.current)
|
||||||
|
self.assertTrue(result.already_installed)
|
||||||
|
|
||||||
|
def test_rejects_package_without_exe(self):
|
||||||
|
with self.assertRaisesRegex(UpdateError, "keine"):
|
||||||
|
install_update(self.make_zip({"README.md": b"x"}), self.current)
|
||||||
|
self.assertFalse((self.root / "Tätigkeitsnachweis-Generator v1.4.0").exists())
|
||||||
|
|
||||||
|
def test_rejects_path_traversal(self):
|
||||||
|
with self.assertRaisesRegex(UpdateError, "ungültigen Pfad"):
|
||||||
|
install_update(self.make_zip({EXE_NAME: b"x", "../evil.txt": b"x"}), self.current)
|
||||||
|
self.assertFalse((self.root / "evil.txt").exists())
|
||||||
|
|
||||||
|
def test_rejects_broken_zip_and_cleans_up(self):
|
||||||
|
broken = self.share / "Tätigkeitsnachweis-Generator v1.4.0.zip"
|
||||||
|
broken.write_bytes(b"no zip")
|
||||||
|
with self.assertRaisesRegex(UpdateError, "keine gültige ZIP"):
|
||||||
|
install_update(broken, self.current)
|
||||||
|
self.assertEqual(sorted(p.name for p in self.root.iterdir()), sorted([self.current.name, "share"]))
|
||||||
|
|
||||||
|
def test_unreachable_share_gives_vpn_hint(self):
|
||||||
|
with self.assertRaisesRegex(UpdateError, "VPN"):
|
||||||
|
install_update(self.share / "Tätigkeitsnachweis-Generator v1.4.0.zip", self.current)
|
||||||
|
|
||||||
|
def test_refuses_existing_folder_without_program(self):
|
||||||
|
(self.root / "Tätigkeitsnachweis-Generator v1.4.0").mkdir()
|
||||||
|
with self.assertRaisesRegex(UpdateError, "existiert bereits"):
|
||||||
|
install_update(self.make_zip(), self.current)
|
||||||
|
|
||||||
|
|
||||||
|
class StartApplicationTests(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(sys.platform == "win32", "Windows-Prozessflags")
|
||||||
|
def test_starts_new_exe_with_clean_bundle_environment(self):
|
||||||
|
target = Path("C:/neu")
|
||||||
|
environment = {"PATH": "x", "APP_BASE_DIR": "alt", "_MEIPASS2": "alt",
|
||||||
|
"_PYI_APPLICATION_HOME_DIR": "alt", "STREAMLIT_SERVER_PORT": "8501"}
|
||||||
|
with patch.dict(update_installer.os.environ, environment, clear=True), \
|
||||||
|
patch.object(update_installer.subprocess, "Popen") as popen:
|
||||||
|
update_installer.start_application(target)
|
||||||
|
args, kwargs = popen.call_args
|
||||||
|
self.assertEqual(args[0], [str(target / EXE_NAME)])
|
||||||
|
self.assertEqual(kwargs["cwd"], target)
|
||||||
|
self.assertEqual(kwargs["env"], {"PATH": "x", "PYINSTALLER_RESET_ENVIRONMENT": "1"})
|
||||||
|
self.assertTrue(kwargs["creationflags"] & subprocess.CREATE_NEW_CONSOLE)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""Richtet eine neue Programmversion neben der aktuellen Installation ein.
|
||||||
|
|
||||||
|
Ablauf von "Anwendung ersetzen":
|
||||||
|
1. ZIP vom SMB-Laufwerk lokal zwischenspeichern und prüfen.
|
||||||
|
2. In einen Zwischenordner neben der aktuellen Installation entpacken.
|
||||||
|
3. input/ und output/ sowie eigene Template-PDFs übernehmen.
|
||||||
|
4. Zwischenordner in "Tätigkeitsnachweis-Generator vX.Y.Z" umbenennen.
|
||||||
|
5. Neue EXE starten; sie beendet die alte Instanz nach Rückfrage selbst.
|
||||||
|
|
||||||
|
Die alte Installation bleibt unverändert als Rückfallebene erhalten.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
|
||||||
|
EXE_NAME = "Tätigkeitsnachweis-Generator.exe"
|
||||||
|
USER_DATA_DIRS = ("input", "output")
|
||||||
|
SETTINGS_FILE = Path("config") / "settings.ini"
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateError(Exception):
|
||||||
|
"""Fehler mit einer Meldung, die direkt angezeigt werden kann."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UpdateResult:
|
||||||
|
target_dir: Path
|
||||||
|
already_installed: bool = False
|
||||||
|
previous_settings: Path | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def can_replace_application() -> bool:
|
||||||
|
"""Nur die gebaute Windows-EXE kann sich selbst ersetzen."""
|
||||||
|
return sys.platform == "win32" and bool(getattr(sys, "frozen", False))
|
||||||
|
|
||||||
|
|
||||||
|
def _check_members(package: zipfile.ZipFile) -> None:
|
||||||
|
names = package.namelist()
|
||||||
|
for name in names:
|
||||||
|
path = PurePosixPath(name.replace("\\", "/"))
|
||||||
|
if path.is_absolute() or ".." in path.parts or ":" in name:
|
||||||
|
raise UpdateError(f"Das Update-Paket enthält einen ungültigen Pfad: {name}")
|
||||||
|
if EXE_NAME not in names:
|
||||||
|
raise UpdateError(f"Das Update-Paket enthält keine {EXE_NAME}.")
|
||||||
|
if package.testzip() is not None:
|
||||||
|
raise UpdateError("Das Update-Paket ist beschädigt. Bitte später erneut versuchen.")
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_user_data(current_dir: Path, staging_dir: Path) -> None:
|
||||||
|
for folder in USER_DATA_DIRS:
|
||||||
|
source = current_dir / folder
|
||||||
|
if source.is_dir():
|
||||||
|
shutil.copytree(source, staging_dir / folder, dirs_exist_ok=True)
|
||||||
|
# Eigene Templates übernehmen, ohne die mitgelieferten zu überschreiben.
|
||||||
|
templates = current_dir / "template"
|
||||||
|
if templates.is_dir():
|
||||||
|
(staging_dir / "template").mkdir(exist_ok=True)
|
||||||
|
for path in templates.iterdir():
|
||||||
|
destination = staging_dir / "template" / path.name
|
||||||
|
if path.is_file() and not destination.exists():
|
||||||
|
shutil.copy2(path, destination)
|
||||||
|
|
||||||
|
|
||||||
|
def _keep_previous_settings(current_dir: Path, staging_dir: Path) -> Path | None:
|
||||||
|
"""Neue settings.ini gilt; abweichende alte Einstellungen bleiben daneben erhalten."""
|
||||||
|
old, new = current_dir / SETTINGS_FILE, staging_dir / SETTINGS_FILE
|
||||||
|
if not old.is_file() or (new.is_file() and old.read_bytes() == new.read_bytes()):
|
||||||
|
return None
|
||||||
|
new.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
backup = new.with_name("settings.vorherige-version.ini")
|
||||||
|
shutil.copy2(old, backup)
|
||||||
|
return backup
|
||||||
|
|
||||||
|
|
||||||
|
def install_update(zip_path: Path, current_dir: Path) -> UpdateResult:
|
||||||
|
"""Entpackt zip_path neben current_dir und übernimmt die Nutzerdaten."""
|
||||||
|
current_dir = current_dir.resolve()
|
||||||
|
target_dir = current_dir.parent / zip_path.stem
|
||||||
|
if target_dir == current_dir:
|
||||||
|
raise UpdateError("Diese Version ist bereits die laufende Installation.")
|
||||||
|
if (target_dir / EXE_NAME).is_file():
|
||||||
|
return UpdateResult(target_dir, already_installed=True)
|
||||||
|
if target_dir.exists():
|
||||||
|
raise UpdateError(
|
||||||
|
f"Der Ordner {target_dir} existiert bereits, enthält aber kein Programm. "
|
||||||
|
"Bitte den Ordner entfernen oder umbenennen und erneut versuchen.")
|
||||||
|
|
||||||
|
staging_dir = current_dir.parent / f".{zip_path.stem}.update"
|
||||||
|
try:
|
||||||
|
shutil.rmtree(staging_dir, ignore_errors=True)
|
||||||
|
with tempfile.TemporaryDirectory(prefix="taetigkeitsnachweis-update-") as temporary:
|
||||||
|
# Erst lokal kopieren: ein VPN-Abbruch trifft so nur die Kopie.
|
||||||
|
local_zip = Path(temporary) / zip_path.name
|
||||||
|
try:
|
||||||
|
shutil.copyfile(zip_path, local_zip)
|
||||||
|
except OSError as exc:
|
||||||
|
raise UpdateError(
|
||||||
|
"Das Update konnte nicht vom Netzlaufwerk geladen werden. "
|
||||||
|
"Bitte VPN-Verbindung und Laufwerk J: prüfen.") from exc
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(local_zip) as package:
|
||||||
|
_check_members(package)
|
||||||
|
package.extractall(staging_dir)
|
||||||
|
except zipfile.BadZipFile as exc:
|
||||||
|
raise UpdateError("Das Update-Paket ist keine gültige ZIP-Datei.") from exc
|
||||||
|
_copy_user_data(current_dir, staging_dir)
|
||||||
|
previous_settings = _keep_previous_settings(current_dir, staging_dir)
|
||||||
|
staging_dir.rename(target_dir)
|
||||||
|
except UpdateError:
|
||||||
|
shutil.rmtree(staging_dir, ignore_errors=True)
|
||||||
|
raise
|
||||||
|
except OSError as exc:
|
||||||
|
shutil.rmtree(staging_dir, ignore_errors=True)
|
||||||
|
raise UpdateError(
|
||||||
|
f"Die neue Version konnte nicht eingerichtet werden ({exc}). "
|
||||||
|
f"Bitte Schreibrechte für {current_dir.parent} prüfen.") from exc
|
||||||
|
if previous_settings is not None:
|
||||||
|
previous_settings = target_dir / SETTINGS_FILE.parent / previous_settings.name
|
||||||
|
return UpdateResult(target_dir, previous_settings=previous_settings)
|
||||||
|
|
||||||
|
|
||||||
|
def start_application(target_dir: Path) -> None:
|
||||||
|
"""Startet die neue EXE in einem eigenen Konsolenfenster wie per Doppelklick."""
|
||||||
|
env = {key: value for key, value in os.environ.items()
|
||||||
|
if key != "APP_BASE_DIR" and not key.startswith(("STREAMLIT_", "_MEIPASS", "_PYI_"))}
|
||||||
|
# Sonst übernimmt die neue EXE Pfade aus dem Bundle der laufenden alten EXE.
|
||||||
|
env["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
|
||||||
|
subprocess.Popen(
|
||||||
|
[str(target_dir / EXE_NAME)], cwd=target_dir, env=env, close_fds=True,
|
||||||
|
creationflags=subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||||
|
)
|
||||||
@@ -1 +1 @@
|
|||||||
1.3.3
|
1.4
|
||||||
@@ -144,6 +144,20 @@ def update_changelog(
|
|||||||
print(f" ✓ CHANGELOG.md aktualisiert → [{new_version}]")
|
print(f" ✓ CHANGELOG.md aktualisiert → [{new_version}]")
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_changelog_edit() -> None:
|
||||||
|
"""Gibt dem Benutzer Zeit, den neuen Changelog-Eintrag zu bearbeiten.
|
||||||
|
|
||||||
|
Der Build liest CHANGELOG.md erst nach dieser Pause; Änderungen in der
|
||||||
|
Datei fließen also in den Build ein.
|
||||||
|
"""
|
||||||
|
print(f"\n✏️ Bitte den neuen Eintrag jetzt bearbeiten und speichern:\n {CHANGELOG_PATH}")
|
||||||
|
try:
|
||||||
|
input(" Danach Enter drücken, um den Build fortzusetzen (Strg+C bricht ab) … ")
|
||||||
|
except (EOFError, KeyboardInterrupt):
|
||||||
|
print("\nAbgebrochen.")
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
def _build_sections(bump_type: str, message: Optional[str]) -> str:
|
def _build_sections(bump_type: str, message: Optional[str]) -> str:
|
||||||
"""Baut die Sektionen für den Changelog-Eintrag."""
|
"""Baut die Sektionen für den Changelog-Eintrag."""
|
||||||
if message:
|
if message:
|
||||||
@@ -353,6 +367,10 @@ def main() -> None:
|
|||||||
"--message", "-m",
|
"--message", "-m",
|
||||||
help="Changelog-Beschreibung für diese Version",
|
help="Changelog-Beschreibung für diese Version",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-pause", action="store_true",
|
||||||
|
help="Nicht auf die Bearbeitung des Changelogs warten (z.B. für CI)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -406,6 +424,8 @@ def main() -> None:
|
|||||||
if not args.no_bump:
|
if not args.no_bump:
|
||||||
print("\n📝 Changelog …")
|
print("\n📝 Changelog …")
|
||||||
update_changelog(new_version, bump_type, args.message)
|
update_changelog(new_version, bump_type, args.message)
|
||||||
|
if not args.no_pause and sys.stdin.isatty():
|
||||||
|
_wait_for_changelog_edit()
|
||||||
_prepare_build_dir()
|
_prepare_build_dir()
|
||||||
exe_path = _run_pyinstaller()
|
exe_path = _run_pyinstaller()
|
||||||
final_exe = _copy_to_build(exe_path)
|
final_exe = _copy_to_build(exe_path)
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ except ImportError:
|
|||||||
datas = [
|
datas = [
|
||||||
(str(_APP_DIR / "app.py"), "."),
|
(str(_APP_DIR / "app.py"), "."),
|
||||||
(str(_APP_DIR / "version_check.py"), "."),
|
(str(_APP_DIR / "version_check.py"), "."),
|
||||||
|
(str(_APP_DIR / "update_installer.py"), "."),
|
||||||
(str(_APP_DIR / "CHANGELOG.md"), "."),
|
(str(_APP_DIR / "CHANGELOG.md"), "."),
|
||||||
(str(_APP_DIR / "src"), "src"),
|
(str(_APP_DIR / "src"), "src"),
|
||||||
(str(_APP_DIR / "config"), "config"),
|
(str(_APP_DIR / "config"), "config"),
|
||||||
|
|||||||
Reference in new issue
Block a user