fix: config save broken (relative_to→relpath), startup validation, auto-create, untrack config.yaml

- _relative_path(): replace Path.relative_to() with os.path.relpath()
  (ValueError crossing filesystems silently returned absolute paths)
- _sync_config_from_ui(): always derive relative from get_path() absolute,
  never trust stale FileSelector._relative_path after Browse
- validate_paths(): check all file paths exist on startup, clear missing
- Clear UI selectors after validation to prevent re-save of stale paths
- _load_config(): auto-create blank config.yaml when missing on disk
- .gitignore: add config.yaml (user-specific, must not be tracked)
This commit is contained in:
2026-06-12 15:02:09 +08:00
parent 9238cbc63e
commit 9502bba636
4 changed files with 84 additions and 47 deletions
+48 -17
View File
@@ -92,6 +92,32 @@ class MainWindow(ctk.CTk):
self._load_config()
self._build_ui()
# Validate file paths — clear any that don't exist on this machine
if self._config and self._config._config_path:
try:
cleared = self._config.validate_paths()
if cleared:
joined = ", ".join(cleared)
self._log_message(f" ⚠ Cleared stale paths from config: {joined}")
# Also clear the UI selectors so stale paths don't
# get re-saved by subsequent _auto_save_config calls
_selector_map = {
"bootloader_bit_path": self._bit_selector,
"bootloader_elf_path": self._elf_selector,
"bootloader_bin_path": self._bootloader_bin_selector,
"fsbl_elf_path": self._fsbl_selector,
"firmware_bin_path": self._firmware_bin_selector,
}
for attr in cleared:
sel = _selector_map.get(attr)
if sel:
sel.set_path("")
self._config.save()
self._log_message(f" ✓ Config saved after clearing stale paths")
except Exception as e:
self._log_message(f" ✗ Config validation failed: {e}")
self._update_step_dependencies()
# Handle window close
@@ -156,21 +182,28 @@ class MainWindow(ctk.CTk):
return config_path
def _load_config(self) -> None:
"""Load configuration from file or create default."""
"""Load configuration from file or create a blank one on disk.
If config.yaml does not exist or cannot be read, a new one
is created with default values and written to disk immediately.
"""
user_config_path = self._get_user_config_path()
if self._config_path:
try:
self._config = Config.from_file(self._config_path)
if self._config_path.name == "default_config.yaml":
user_config_path = self._get_user_config_path()
self._config._config_path = user_config_path
return
except Exception:
self._config = Config.from_default()
user_config_path = self._get_user_config_path()
self._config._config_path = user_config_path
else:
self._config = Config.from_default()
user_config_path = self._get_user_config_path()
self._config._config_path = user_config_path
pass # Fall through and create default
# Config file missing or unreadable — create a blank one
self._config = Config.from_default()
self._config._config_path = user_config_path
try:
self._config.save()
except Exception:
pass # Will be saved later by _auto_save_config
def _reload_config(self) -> None:
"""Re-read config.yaml and update UI selectors with latest paths.
@@ -234,7 +267,8 @@ class MainWindow(ctk.CTk):
if hasattr(self, '_port_var'):
self._config.serial_port = self._port_var.get()
# Sync file paths from FileSelectors
files = self._get_selected_files()
# Always derive relative from absolute — the _relative_path
# stored in FileSelector can be stale after Browse.
for attr, selector in [
("bootloader_bit_path", self._bit_selector),
("bootloader_elf_path", self._elf_selector),
@@ -242,14 +276,11 @@ class MainWindow(ctk.CTk):
("fsbl_elf_path", self._fsbl_selector),
("firmware_bin_path", self._firmware_bin_selector),
]:
rel = selector.get_relative_path()
if rel:
setattr(self._config, attr, rel)
abs_path = selector.get_path()
if abs_path:
setattr(self._config, attr, self._config._relative_path(abs_path))
else:
abs_path = selector.get_path()
if abs_path:
self._config._config_path # ensure _config_path is set
setattr(self._config, attr, self._config._relative_path(abs_path))
setattr(self._config, attr, "")
# Sync erase checkbox (may not exist yet)
if hasattr(self, '_erase_cb_var'):
self._config.erase_all = self._erase_cb_var.get()