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:
+2
-1
@@ -33,7 +33,8 @@ htmlcov/
|
|||||||
# ── Opencode (internal tooling) ──────────────────────────────
|
# ── Opencode (internal tooling) ──────────────────────────────
|
||||||
.opencode/
|
.opencode/
|
||||||
|
|
||||||
# ── Config overrides (user-specific) ─────────────────────────
|
# ── Config (user-specific, auto-generated if missing) ─────────
|
||||||
|
config.yaml
|
||||||
config/zynq_flasher.yaml
|
config/zynq_flasher.yaml
|
||||||
|
|
||||||
# ── Logs ─────────────────────────────────────────────────────
|
# ── Logs ─────────────────────────────────────────────────────
|
||||||
|
|||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
boot_wait_delay: 10
|
|
||||||
bootloader_bin_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/bootloader_z100_800M.bin
|
|
||||||
bootloader_bit_path: /home/ly0kos/work/Verify/FW/tftp_app/bootloader/pl_arm_wrapper_hw_platform_0/pl_arm_wrapper.bit
|
|
||||||
bootloader_elf_path: /home/ly0kos/work/Verify/FW/tftp_app/tftp_app/tftp.elf
|
|
||||||
erase_all: false
|
|
||||||
firmware_bin_path: /home/ly0kos/work/Verify/FW/V1.3.1.3.3_06_01_pmu_cpld_check/V1.3.1.3.3_06_01_pmu_cpld_check.bin
|
|
||||||
flash_model: s25fl256s1
|
|
||||||
flash_type: qspi-x4-single
|
|
||||||
fsbl_elf_path: /home/ly0kos/work/Verify/FPGA/Xilinx/Z100/fsbl_qspi_bypass.elf
|
|
||||||
inter_step_delay: 2
|
|
||||||
ping_count: 3
|
|
||||||
ping_timeout: 5
|
|
||||||
reboot_timeout: 30
|
|
||||||
serial_baudrate: 115200
|
|
||||||
serial_port: /dev/ttyUSB0
|
|
||||||
step_timeouts:
|
|
||||||
bitstream: 60
|
|
||||||
check_env: 30
|
|
||||||
elf_download: 60
|
|
||||||
flash_erase: 120
|
|
||||||
flash_program: 600
|
|
||||||
tftp_reboot: 120
|
|
||||||
tftp_upload_name: z7bin
|
|
||||||
uart_delay: 3
|
|
||||||
xilinx_path: /data/xilinx
|
|
||||||
zynq_ip: 192.168.100.11
|
|
||||||
zynq_part: XC7Z100
|
|
||||||
+34
-2
@@ -190,8 +190,11 @@ class Config:
|
|||||||
def _relative_path(self, path: str) -> str:
|
def _relative_path(self, path: str) -> str:
|
||||||
"""Convert an absolute path to a relative path from the config file.
|
"""Convert an absolute path to a relative path from the config file.
|
||||||
|
|
||||||
|
Uses :func:`os.path.relpath` which works across filesystem
|
||||||
|
boundaries (unlike :meth:`Path.relative_to`).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Absolute path string.
|
path: Absolute or relative path string.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Relative path string, or the original if conversion fails.
|
Relative path string, or the original if conversion fails.
|
||||||
@@ -201,7 +204,8 @@ class Config:
|
|||||||
try:
|
try:
|
||||||
target = Path(path)
|
target = Path(path)
|
||||||
if target.is_absolute():
|
if target.is_absolute():
|
||||||
return str(target.relative_to(self._config_path.parent))
|
base = str(self._config_path.parent)
|
||||||
|
return os.path.relpath(str(target), base)
|
||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
pass
|
pass
|
||||||
return path
|
return path
|
||||||
@@ -219,6 +223,34 @@ class Config:
|
|||||||
return Path()
|
return Path()
|
||||||
return (self._config_path.parent / relative_path).resolve()
|
return (self._config_path.parent / relative_path).resolve()
|
||||||
|
|
||||||
|
def validate_paths(self) -> list[str]:
|
||||||
|
"""Validate file paths in config and clear those that don't exist.
|
||||||
|
|
||||||
|
Checks each file path field, resolves it against the config
|
||||||
|
directory, and clears it if the target file does not exist
|
||||||
|
on the current filesystem.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of field names that were cleared.
|
||||||
|
"""
|
||||||
|
cleared: list[str] = []
|
||||||
|
path_fields = [
|
||||||
|
"bootloader_bit_path",
|
||||||
|
"bootloader_elf_path",
|
||||||
|
"bootloader_bin_path",
|
||||||
|
"fsbl_elf_path",
|
||||||
|
"firmware_bin_path",
|
||||||
|
]
|
||||||
|
for attr in path_fields:
|
||||||
|
val = getattr(self, attr, "")
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
resolved = self.resolve_path(val)
|
||||||
|
if not resolved.is_file():
|
||||||
|
setattr(self, attr, "")
|
||||||
|
cleared.append(attr)
|
||||||
|
return cleared
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config_path(self) -> Path | None:
|
def config_path(self) -> Path | None:
|
||||||
"""Return the path to the config file on disk."""
|
"""Return the path to the config file on disk."""
|
||||||
|
|||||||
+44
-13
@@ -92,6 +92,32 @@ class MainWindow(ctk.CTk):
|
|||||||
|
|
||||||
self._load_config()
|
self._load_config()
|
||||||
self._build_ui()
|
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()
|
self._update_step_dependencies()
|
||||||
|
|
||||||
# Handle window close
|
# Handle window close
|
||||||
@@ -156,21 +182,28 @@ class MainWindow(ctk.CTk):
|
|||||||
return config_path
|
return config_path
|
||||||
|
|
||||||
def _load_config(self) -> None:
|
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:
|
if self._config_path:
|
||||||
try:
|
try:
|
||||||
self._config = Config.from_file(self._config_path)
|
self._config = Config.from_file(self._config_path)
|
||||||
if self._config_path.name == "default_config.yaml":
|
if self._config_path.name == "default_config.yaml":
|
||||||
user_config_path = self._get_user_config_path()
|
|
||||||
self._config._config_path = user_config_path
|
self._config._config_path = user_config_path
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
|
pass # Fall through and create default
|
||||||
|
|
||||||
|
# Config file missing or unreadable — create a blank one
|
||||||
self._config = Config.from_default()
|
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
|
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:
|
def _reload_config(self) -> None:
|
||||||
"""Re-read config.yaml and update UI selectors with latest paths.
|
"""Re-read config.yaml and update UI selectors with latest paths.
|
||||||
@@ -234,7 +267,8 @@ class MainWindow(ctk.CTk):
|
|||||||
if hasattr(self, '_port_var'):
|
if hasattr(self, '_port_var'):
|
||||||
self._config.serial_port = self._port_var.get()
|
self._config.serial_port = self._port_var.get()
|
||||||
# Sync file paths from FileSelectors
|
# 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 [
|
for attr, selector in [
|
||||||
("bootloader_bit_path", self._bit_selector),
|
("bootloader_bit_path", self._bit_selector),
|
||||||
("bootloader_elf_path", self._elf_selector),
|
("bootloader_elf_path", self._elf_selector),
|
||||||
@@ -242,14 +276,11 @@ class MainWindow(ctk.CTk):
|
|||||||
("fsbl_elf_path", self._fsbl_selector),
|
("fsbl_elf_path", self._fsbl_selector),
|
||||||
("firmware_bin_path", self._firmware_bin_selector),
|
("firmware_bin_path", self._firmware_bin_selector),
|
||||||
]:
|
]:
|
||||||
rel = selector.get_relative_path()
|
|
||||||
if rel:
|
|
||||||
setattr(self._config, attr, rel)
|
|
||||||
else:
|
|
||||||
abs_path = selector.get_path()
|
abs_path = selector.get_path()
|
||||||
if abs_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, self._config._relative_path(abs_path))
|
||||||
|
else:
|
||||||
|
setattr(self._config, attr, "")
|
||||||
# Sync erase checkbox (may not exist yet)
|
# Sync erase checkbox (may not exist yet)
|
||||||
if hasattr(self, '_erase_cb_var'):
|
if hasattr(self, '_erase_cb_var'):
|
||||||
self._config.erase_all = self._erase_cb_var.get()
|
self._config.erase_all = self._erase_cb_var.get()
|
||||||
|
|||||||
Reference in New Issue
Block a user