From d58c70a81ebfd0454c427ff749307d183b85c69d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:01:25 +0000 Subject: [PATCH 1/2] Initial plan From d31d0d4b186a1a80a7a56bd4a8a945138a09cbab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 21 Jan 2026 10:08:25 +0000 Subject: [PATCH 2/2] Implement proper deletion of array-like UCI sections using shell script Replace hardcoded 10 deletion attempts with proper shell loop using while loop. Change output format from UCI batch commands to shell script that runs uci commands. Update Justfile to pipe output to 'sh' instead of 'uci batch; uci commit'. Remove "Known bugs" section from README as issue is now resolved. Co-authored-by: Mic92 <96200+Mic92@users.noreply.github.com> --- openwrt/Justfile | 2 +- openwrt/README.md | 7 ------- openwrt/nix_uci/__init__.py | 36 ++++++++++++++++++++++-------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/openwrt/Justfile b/openwrt/Justfile index 1e18a83a5..ee5fb41f8 100644 --- a/openwrt/Justfile +++ b/openwrt/Justfile @@ -22,7 +22,7 @@ apply: EOF # Apply uci configuration - just eval-config | just ssh 'uci batch; uci commit' + just eval-config | just ssh 'sh' # Set up internet after a firmware reset if ! just ssh "ip link | grep -q pppoe-wan"; then diff --git a/openwrt/README.md b/openwrt/README.md index fdc63b24d..ee9c2ee40 100644 --- a/openwrt/README.md +++ b/openwrt/README.md @@ -22,13 +22,6 @@ on how to use this project. Tipp: The justfile is read by this project, let me know and I might switch to your project and contribute to your project. -## Known bugs - -- It is not possible to reliably delete all elements of a list section with uci - batch command. Currently, the code attempts to delete up to 10 list sections - and then create a new list: - https://github.com/Mic92/dotfiles/blob/00e178a0953461f676ecc9c0ab5b36a0742e12bd/openwrt/nix_uci/**init**.py#L108 - ## Related work - https://gitea.c3d2.de/zentralwerk/network/src/branch/master/nix/pkgs/ap.nix#L142 diff --git a/openwrt/nix_uci/__init__.py b/openwrt/nix_uci/__init__.py index a705991ea..7a6e3eeb8 100644 --- a/openwrt/nix_uci/__init__.py +++ b/openwrt/nix_uci/__init__.py @@ -38,10 +38,10 @@ def serialize_option_val( """Serialize an option value to UCI format.""" if isinstance(val, float | int | str): val = interpolate_secrets(str(val), secrets) - return [f"set {key}='{val}'"] + return [f"uci set {key}='{val}'"] if isinstance(val, list): - return [f"add_list {key}='{interpolate_secrets(v, secrets)}'" for v in val] + return [f"uci add_list {key}='{interpolate_secrets(v, secrets)}'" for v in val] msg = f"{val} is not a string" raise ConfigError(msg) @@ -58,15 +58,12 @@ def serialize_list_section( lines = [] _type = None - # TODO(@joerg): Investigate how to delete all list sections - # truncate list so we can start from fresh - _type = list_obj.get("_type") if _type is None: msg = f"{config_name}.@{section_name}[{idx}] has no type!" raise ConfigError(msg) del list_obj["_type"] - lines.append(f"set {config_name}.@{section_name}[{idx}]={_type}") + lines.append(f"uci set {config_name}.@{section_name}[{idx}]={_type}") for option_name, option in list_obj.items(): lines.extend( @@ -89,14 +86,14 @@ def serialize_named_section( lines = [] _type = None # truncate section so we can start from fresh - lines.append(f"delete {config_name}.{section_name}") + lines.append(f"uci delete {config_name}.{section_name}") _type = section.get("_type") if _type is None: msg = f"{config_name}.{section_name} has no type" raise ConfigError(msg) del section["_type"] - lines.append(f"set {config_name}.{section_name}={_type}") + lines.append(f"uci set {config_name}.{section_name}={_type}") for option_name, option in section.items(): # TODO(@joerg): Investigate how escaping works in UCI @@ -113,6 +110,11 @@ def serialize_named_section( def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: """Serialize configuration dictionary to UCI format.""" lines: list[str] = [] + # Add shebang to make it a proper shell script + lines.append("#!/bin/sh") + lines.append("set -e") + lines.append("") + config_names = [] for config_name, sections in configs.items(): config_names.append(config_name) @@ -124,12 +126,14 @@ def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: for section_name, section in sections.items(): if isinstance(section, list): - # NOTE: Clear out existing list sections (no better method available) - lines.extend( - f"delete {config_name}.@{section_name}[0]" for _i in range(10) - ) + # Use shell loop to properly delete all existing list sections + lines.append(f"# Clear all existing {config_name}.@{section_name} sections") + lines.append(f"while uci -q delete {config_name}.@{section_name}[-1] 2>/dev/null; do :; done") + lines.append("") + + # Add new sections lines.extend( - f"add {config_name} {section_name}" for _i in range(len(section)) + f"uci add {config_name} {section_name}" for _i in range(len(section)) ) for idx, list_obj in enumerate(section): @@ -142,7 +146,7 @@ def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: secrets, ), ) - # for option_name, option in section: + lines.append("") elif isinstance(section, dict): lines.extend( serialize_named_section( @@ -152,12 +156,16 @@ def serialize_uci(configs: dict[str, Any], secrets: dict[str, str]) -> str: secrets, ), ) + lines.append("") else: msg = f"{config_name}.{section_name} is not a valid section object, expected dict, got: {type(section).__name__}" raise ConfigError( msg, ) + # Commit all changes at the end + lines.append("uci commit") + return "\n".join(lines)