From 9502bba6369fd03406fd984bf72149c3f9e6ad50 Mon Sep 17 00:00:00 2001 From: Jeremy Shen Date: Fri, 12 Jun 2026 15:02:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20config=20save=20broken=20(relative=5Fto?= =?UTF-8?q?=E2=86=92relpath),=20startup=20validation,=20auto-create,=20unt?= =?UTF-8?q?rack=20config.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _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) --- .gitignore | 3 +- config.yaml | 27 ------------------ src/config_manager.py | 36 +++++++++++++++++++++-- src/gui/main_window.py | 65 +++++++++++++++++++++++++++++++----------- 4 files changed, 84 insertions(+), 47 deletions(-) delete mode 100644 config.yaml diff --git a/.gitignore b/.gitignore index 482a79d..5abcbde 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,8 @@ htmlcov/ # ── Opencode (internal tooling) ────────────────────────────── .opencode/ -# ── Config overrides (user-specific) ───────────────────────── +# ── Config (user-specific, auto-generated if missing) ───────── +config.yaml config/zynq_flasher.yaml # ── Logs ───────────────────────────────────────────────────── diff --git a/config.yaml b/config.yaml deleted file mode 100644 index 3892f46..0000000 --- a/config.yaml +++ /dev/null @@ -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 diff --git a/src/config_manager.py b/src/config_manager.py index 27f2cdd..e881f15 100644 --- a/src/config_manager.py +++ b/src/config_manager.py @@ -190,8 +190,11 @@ class Config: def _relative_path(self, path: str) -> str: """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: - path: Absolute path string. + path: Absolute or relative path string. Returns: Relative path string, or the original if conversion fails. @@ -201,7 +204,8 @@ class Config: try: target = Path(path) 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): pass return path @@ -219,6 +223,34 @@ class Config: return Path() 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 def config_path(self) -> Path | None: """Return the path to the config file on disk.""" diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 5575b3b..792a4b2 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -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()