⛅ Sync 2026-10-11 13:16:00

This commit is contained in:
github-actions[bot] committed 2026-10-11 13:16:00 +08:00
1 parent 705dadd362
commit e6d1b4e2d7
105 files changed
+26511 -50

No files matched your search

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""Select the plugin and Chinese translation without the SDK's all-package defaults."""
import pathlib
import re
import sys
def configure(directory):
directory = pathlib.Path(directory)
if not (directory / "rules.mk").is_file() or not (directory / "Makefile").is_file():
raise ValueError("Expected an OpenWrt SDK directory")
config = directory / ".config"
lines = config.read_text().splitlines() if config.exists() else []
pattern = r"(?:# )?CONFIG_(?:ALL(?:_NONSHARED|_KMODS)?\b|PACKAGE_|LUCI_LANG_)"
retained = [line for line in lines if not re.match(pattern, line)]
selected = [
"# CONFIG_ALL is not set", "# CONFIG_ALL_NONSHARED is not set", "# CONFIG_ALL_KMODS is not set",
"CONFIG_PACKAGE_luci-app-device-manager=m", "CONFIG_LUCI_LANG_zh_Hans=y",
"CONFIG_PACKAGE_luci-i18n-device-manager-zh-cn=m",
]
config.write_text("\n".join(retained + selected) + "\n")
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("Usage: configure-sdk.py <sdk-directory>")
configure(sys.argv[1])
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env bash
#
# deploy.sh - Deploy luci-app-device-manager directly to an OpenWrt router for development/testing.
# SSH authentication is performed once; all deployment commands reuse that connection.
#
# Usage:
# ./tools/deploy.sh [options] <target>
#
# Examples:
# ./tools/deploy.sh 192.168.1.1
# ./tools/deploy.sh root@192.168.1.1
# ./tools/deploy.sh -p 2222 root@192.168.1.1
#
set -euo pipefail
# Color formatting
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
print_info() {
printf "${BLUE}[INFO]${NC} %s\n" "$1"
}
print_success() {
printf "${GREEN}[SUCCESS]${NC} %s\n" "$1"
}
print_error() {
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
}
show_usage() {
cat <<EOF
Usage: $0 [options] <target>
Arguments:
<target> Router IP address or SSH user@host (e.g., 192.168.1.1 or root@192.168.1.1)
Options:
-p, --port <port> SSH port number (default: 22)
-h, --help Show this help message
Examples:
$0 192.168.1.1
$0 root@192.168.1.1
$0 -p 2222 root@192.168.31.1
EOF
}
# Parse options
SSH_PORT="22"
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
-p|--port)
if [[ $# -lt 2 || ! "$2" =~ ^[0-9]+$ || ${#2} -gt 5 ]] || (( 10#$2 < 1 || 10#$2 > 65535 )); then
print_error "SSH port must be an integer between 1 and 65535."
exit 1
fi
SSH_PORT="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
-*)
print_error "Unknown option: $1"
show_usage
exit 1
;;
*)
if [[ -z "$TARGET" ]]; then
TARGET="$1"
else
print_error "Unexpected argument: $1"
show_usage
exit 1
fi
shift
;;
esac
done
if [[ -z "$TARGET" ]]; then
print_error "Target router address is required."
show_usage
exit 1
fi
# Normalize SSH target
if [[ "$TARGET" != *"@"* ]]; then
SSH_TARGET="root@${TARGET}"
ROUTER_IP="${TARGET}"
else
SSH_TARGET="${TARGET}"
ROUTER_IP="${TARGET#*@}"
fi
# Check prerequisites
for cmd in ssh scp mktemp; do
if ! command -v "$cmd" &>/dev/null; then
print_error "Required local command '$cmd' is not found in PATH."
exit 1
fi
done
# Resolve workspace directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Verify essential project files exist
if [[ ! -f "${PROJECT_ROOT}/htdocs/luci-static/resources/view/device-manager/devices.js" ]]; then
print_error "Missing frontend view file: htdocs/.../devices.js"
exit 1
fi
if [[ ! -f "${PROJECT_ROOT}/root/usr/share/luci/menu.d/luci-app-device-manager.json" ]]; then
print_error "Missing menu definition file: root/.../menu.d/luci-app-device-manager.json"
exit 1
fi
if [[ ! -f "${PROJECT_ROOT}/root/usr/share/rpcd/acl.d/luci-app-device-manager.json" ]]; then
print_error "Missing ACL definition file: root/.../acl.d/luci-app-device-manager.json"
exit 1
fi
for resource in model.js service.js preferences.js table.js device-dialog.js group-dialog.js styles.css; do
if [[ ! -f "${PROJECT_ROOT}/htdocs/luci-static/resources/device-manager/${resource}" ]]; then
print_error "Missing frontend resource: device-manager/${resource}"
exit 1
fi
done
if [[ ! -f "${PROJECT_ROOT}/root/usr/libexec/rpcd/luci.device-manager" || ! -f "${PROJECT_ROOT}/tools/initialize-config.sh" ]]; then
print_error "Missing rpcd helper or configuration initializer."
exit 1
fi
# Keep the control socket private and its path short enough for Unix socket limits.
SSH_CONTROL_DIR="$(mktemp -d /tmp/luci-deploy.XXXXXX)"
SSH_CONTROL_PATH="${SSH_CONTROL_DIR}/ssh"
SSH_COMMON_OPTS=(-o ConnectTimeout=5 -o StrictHostKeyChecking=accept-new -o "ControlPath=${SSH_CONTROL_PATH}")
SSH_OPTS=(-p "${SSH_PORT}" "${SSH_COMMON_OPTS[@]}" -o ControlMaster=no -o BatchMode=yes)
SCP_OPTS=(-P "${SSH_PORT}" "${SSH_COMMON_OPTS[@]}" -o ControlMaster=no -o BatchMode=yes)
cleanup() {
ssh "${SSH_OPTS[@]}" -O exit "${SSH_TARGET}" >/dev/null 2>&1 || true
rm -rf -- "${SSH_CONTROL_DIR}"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
print_info "Connecting to OpenWrt router at ${SSH_TARGET} (port: ${SSH_PORT})..."
# Authenticate once and keep the master connection open until cleanup.
if ! ssh -p "${SSH_PORT}" "${SSH_COMMON_OPTS[@]}" -o BatchMode=no -o ControlPersist=no -M -N -f "${SSH_TARGET}"; then
print_error "Failed to connect to ${SSH_TARGET} via SSH. Please check network, IP, port, and SSH key/password."
exit 1
fi
if ! ssh "${SSH_OPTS[@]}" "${SSH_TARGET}" "command -v ip >/dev/null && command -v flock >/dev/null && test -r /usr/share/libubox/jshn.sh"; then
print_error "Router is missing ip, flock or jshn. Standard OpenWrt BusyBox provides ip and flock; custom builds may need ip-tiny and flock. Install jshn if missing."
exit 1
fi
print_info "Connection verified. Preparing remote directories..."
ssh "${SSH_OPTS[@]}" "${SSH_TARGET}" "
mkdir -p /www/luci-static/resources/view/device-manager \
/usr/share/luci/menu.d \
/usr/share/rpcd/acl.d \
/usr/lib/lua/luci/i18n \
/usr/libexec/rpcd \
/etc/uci-defaults \
/etc/config
"
print_info "Deploying shared frontend modules and stylesheet..."
scp "${SCP_OPTS[@]}" -r \
"${PROJECT_ROOT}/htdocs/luci-static/resources/device-manager" \
"${SSH_TARGET}:/www/luci-static/resources/"
print_info "Deploying frontend view (devices.js)..."
scp "${SCP_OPTS[@]}" \
"${PROJECT_ROOT}/htdocs/luci-static/resources/view/device-manager/devices.js" \
"${SSH_TARGET}:/www/luci-static/resources/view/device-manager/devices.js"
print_info "Deploying LuCI menu and rpcd ACL definitions..."
scp "${SCP_OPTS[@]}" \
"${PROJECT_ROOT}/root/usr/share/luci/menu.d/luci-app-device-manager.json" \
"${SSH_TARGET}:/usr/share/luci/menu.d/luci-app-device-manager.json"
scp "${SCP_OPTS[@]}" \
"${PROJECT_ROOT}/root/usr/share/rpcd/acl.d/luci-app-device-manager.json" \
"${SSH_TARGET}:/usr/share/rpcd/acl.d/luci-app-device-manager.json"
# Native translations are installed as the matching luci-i18n package.
# Remove files left by older development deployments without changing LuCI language.
ssh "${SSH_OPTS[@]}" "${SSH_TARGET}" "
rm -f /www/luci-static/resources/device-manager/i18n.js /www/luci-static/resources/device-manager/translations.js
rm -f /usr/lib/lua/luci/i18n/device-manager-builtin.zh-cn.lmo /etc/uci-defaults/81_device_manager_i18n
"
if [[ -f "${PROJECT_ROOT}/root/usr/libexec/rpcd/luci.device-manager" ]]; then
print_info "Deploying rpcd helper backend script..."
scp "${SCP_OPTS[@]}" \
"${PROJECT_ROOT}/root/usr/libexec/rpcd/luci.device-manager" \
"${SSH_TARGET}:/usr/libexec/rpcd/luci.device-manager"
ssh "${SSH_OPTS[@]}" "${SSH_TARGET}" "chmod +x /usr/libexec/rpcd/luci.device-manager"
fi
if [[ -f "${PROJECT_ROOT}/tools/initialize-config.sh" ]]; then
print_info "Deploying development configuration initializer..."
scp "${SCP_OPTS[@]}" \
"${PROJECT_ROOT}/tools/initialize-config.sh" \
"${SSH_TARGET}:/etc/uci-defaults/80_device_manager"
fi
print_info "Ensuring configuration persistence (protecting existing user remarks)..."
ssh "${SSH_OPTS[@]}" "${SSH_TARGET}" "
set -e
if [ ! -f /etc/config/device_manager ]; then
echo 'Initializing default /etc/config/device_manager'
else
echo 'Existing /etc/config/device_manager preserved.'
fi
if [ -f /etc/uci-defaults/80_device_manager ]; then
chmod +x /etc/uci-defaults/80_device_manager
/etc/uci-defaults/80_device_manager
rm -f /etc/uci-defaults/80_device_manager
fi
"
print_info "Clearing LuCI cache and restarting rpcd..."
ssh "${SSH_OPTS[@]}" "${SSH_TARGET}" "
rm -f /tmp/luci-indexcache* /tmp/luci-modulecache*
/etc/init.d/rpcd restart
"
print_success "Deployment completed successfully!"
printf "\n"
printf "${CYAN}================================================================${NC}\n"
printf " ${GREEN}luci-app-device-manager${NC} is now active on your router.\n"
printf "\n"
printf " Access URL: ${YELLOW}http://%s/cgi-bin/luci/admin/network/device-manager${NC}\n" "${ROUTER_IP}"
printf " Menu path: ${YELLOW}网络 -> 设备管理${NC}\n"
printf "\n"
printf " ${YELLOW}Note:${NC} If you are already logged in, please refresh the page\n"
printf " with ${CYAN}Ctrl + Shift + R${NC} to clear browser JavaScript cache.\n"
printf "${CYAN}================================================================${NC}\n"
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
# SPDX-License-Identifier: MIT
set -e
# Existing configuration, including an intentionally empty file, is authoritative.
# Development deployment only; packages ship /etc/config/device_manager directly.
[ -e /etc/config/device_manager ] && exit 0
touch /etc/config/device_manager
trap 'uci -q revert device_manager || :; rm -f /etc/config/device_manager' EXIT
uci -q batch <<'EOF'
set device_manager.smart_home=group
set device_manager.smart_home.name='Smart home'
set device_manager.phone=group
set device_manager.phone.name='Phones'
set device_manager.computer=group
set device_manager.computer.name='Computers'
set device_manager.network=group
set device_manager.network.name='Network devices'
set device_manager.other=group
set device_manager.other.name='Other devices'
commit device_manager
EOF
trap - EXIT
@@ -0,0 +1,19 @@
#!/usr/bin/env python3
"""Print a LuCI-style version from this source repository for standalone SDK builds."""
import datetime
import pathlib
import subprocess
ROOT = pathlib.Path(__file__).resolve().parents[1]
def version():
stamp, revision = subprocess.check_output(
["git", "log", "-1", "--format=%ct %h", "--abbrev=7", "--", "."],
cwd=ROOT, text=True).strip().split()
moment = datetime.datetime.fromtimestamp(int(stamp), datetime.timezone.utc)
return f"{moment:%y}.{moment.timetuple().tm_yday:03d}.{int(stamp) % 86400:05d}~{revision}"
if __name__ == "__main__":
print(version())
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Export only package sources into a LuCI checkout without modifying Git history."""
import pathlib
import shutil
import sys
ROOT = pathlib.Path(__file__).resolve().parents[1]
def prepare(directory):
directory = pathlib.Path(directory).resolve()
if not (directory / "luci.mk").is_file():
raise ValueError("Expected a LuCI checkout containing luci.mk")
target = directory / "applications/luci-app-device-manager"
if target.exists():
raise FileExistsError(f"Refusing to overwrite existing package: {target}")
target.mkdir(parents=True)
for name in ["htdocs", "root", "po"]:
shutil.copytree(ROOT / name, target / name)
for name in ["LICENSE"]:
shutil.copyfile(ROOT / name, target / name)
readme = (ROOT / "README.md").read_text()
start = readme.index("## 安装 / Installation")
end = readme.index("## 扫描范围 / Scan scope", start)
readme = readme[:start] + (
"## Installation\n\n"
"On OpenWrt releases whose feeds include this application, install it\n"
"from the configured package repositories.\n\n"
"For systems using opkg:\n\n"
"```sh\n"
"opkg update\n"
"opkg install luci-app-device-manager\n"
"opkg install luci-i18n-device-manager-zh-cn\n"
"```\n\n"
"For systems using apk:\n\n"
"```sh\n"
"apk update\n"
"apk add luci-app-device-manager\n"
"apk add luci-i18n-device-manager-zh-cn\n"
"```\n\n"
"The Chinese language package is optional. Without a translation, LuCI\n"
"displays the English source messages. Reload LuCI after installation\n"
"and open **Network → Device Manager**.\n\n"
"Dependencies are resolved by the package manager. Standard OpenWrt\n"
"BusyBox supplies the required `ip` and `flock` applets. Custom firmware\n"
"that disables these applets must enable them or install `ip-tiny`\n"
"(or `ip-full`) and `flock` separately.\n\n"
) + readme[end:]
start = readme.index("## 构建和开发")
end = readme.index("## 真实设备验收", start)
readme = readme[:start] + (
"## Development\n\n"
"This application is built in the LuCI feed with the standard luci.mk.\n"
"Independent SDK workflows, tests and deployment tools are maintained in\n"
"[the source repository](https://github.com/xxi-arch/luci-app-device-manager).\n\n"
) + readme[end:]
(target / "README.md").write_text(readme)
# Package-specific attribution and usage documentation, excluding release tooling.
shutil.copytree(ROOT / "docs", target / "docs",
ignore=shutil.ignore_patterns("upstream-submission.md", "validation.md"))
makefile = (ROOT / "Makefile").read_text()
start = makefile.index("# In-tree LuCI applications")
end = makefile.index("# call BuildPackage", start)
(target / "Makefile").write_text(makefile[:start] + "include ../../luci.mk\n\n" + makefile[end:])
return target
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("Usage: prepare-upstream.py <luci-checkout>")
print(prepare(sys.argv[1]))
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Resolve an official release or snapshot SDK URL and checksum for the requested target."""
import re
import sys
from html.parser import HTMLParser
from urllib.request import urlopen
class Links(HTMLParser):
def __init__(self):
super().__init__()
self.files = set()
def handle_starttag(self, tag, attrs):
if tag == "a":
self.files.update(value for key, value in attrs if key == "href" and value)
def resolve(version, target):
if version != "SNAPSHOT" and not re.fullmatch(r"\d+\.\d+\.\d+(?:-rc\d+)?", version):
raise ValueError("Expected SNAPSHOT or an OpenWrt release version, e.g. 24.10.0")
if not re.fullmatch(r"[a-z0-9_-]+/[a-z0-9_-]+", target):
raise ValueError("Expected an OpenWrt target/subtarget, e.g. x86/64")
base = (f"https://downloads.openwrt.org/snapshots/targets/{target}/" if version == "SNAPSHOT"
else f"https://downloads.openwrt.org/releases/{version}/targets/{target}/")
with urlopen(base, timeout=30) as response:
listing = response.read().decode("utf-8")
links = Links()
links.feed(listing)
sdk_prefix = "openwrt-sdk-" if version == "SNAPSHOT" else f"openwrt-sdk-{re.escape(version)}-"
pattern = rf"{sdk_prefix}[A-Za-z0-9_.+-]+\.Linux-x86_64\.tar\.(?:xz|zst)"
matches = sorted(filename for filename in links.files if re.fullmatch(pattern, filename))
if len(matches) != 1:
raise ValueError(f"Expected one x86_64 host SDK at {base}; found {len(matches)}")
filename = matches[0]
with urlopen(base + "sha256sums", timeout=30) as response:
checksums = response.read().decode("utf-8")
for line in checksums.splitlines():
match = re.fullmatch(r"([a-fA-F0-9]{64})\s+\*?(.+)", line)
if match and match[2] == filename:
return base + filename, match[1].lower()
raise ValueError(f"No checksum found for {filename}")
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("Usage: resolve-sdk.py <release-version|SNAPSHOT> <target/subtarget>")
sdk_url, sdk_sha256 = resolve(sys.argv[1], sys.argv[2])
print(f"sdk_url={sdk_url}")
print(f"sdk_sha256={sdk_sha256}")
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Extract native LuCI messages and merge the PO catalogue using official tools."""
import argparse
import pathlib
import subprocess
import tempfile
ROOT = pathlib.Path(__file__).resolve().parents[1]
def extract(luci_dir):
scanner = pathlib.Path(luci_dir).resolve() / "build/i18n-scan.pl"
if not scanner.is_file():
raise ValueError("Expected a LuCI checkout containing build/i18n-scan.pl")
return subprocess.run(["perl", str(scanner), "htdocs", "root"], cwd=ROOT,
check=True, capture_output=True, text=True).stdout
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--luci-dir", required=True, help="Official LuCI checkout")
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
template = ROOT / "po/templates/device-manager.pot"
catalogue = ROOT / "po/zh_Hans/device-manager.po"
content = extract(args.luci_dir)
with tempfile.TemporaryDirectory(prefix="device-manager-i18n-") as directory:
fresh = pathlib.Path(directory) / "device-manager.pot"
fresh.write_text(content)
if args.check:
if template.read_text() != content:
parser.error("Translation template is stale; regenerate with --luci-dir")
subprocess.run(["msgcmp", "--use-fuzzy", str(catalogue), str(fresh)], check=True)
subprocess.run(["msgfmt", "--check", "-o", str(pathlib.Path(directory) / "catalogue.mo"),
str(catalogue)], check=True)
else:
template.write_text(content)
subprocess.run(["msgmerge", "--update", "--no-fuzzy-matching", "--no-wrap",
"--backup=none", str(catalogue), str(fresh)], check=True)
if __name__ == "__main__":
main()