mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 00:15:13 +08:00
🤞 Sync 2026-09-27 22:16:29
This commit is contained in:
1 parent
8ede4a8320
commit
618f51e9dc
459 files changed
+44817
-2191
No files matched your search
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
fail() { printf 'build-apk-release: %s\n' "$*" >&2; exit 1; }
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
. "$root/apk-feed.env"
|
||||
sdk="${OPENWRT_SDK_DIR:-}"
|
||||
secret="${OPENWRT_APK_SIGNING_KEY:-}"
|
||||
public="$root/$OPENWRT_APK_KEY_FILE"
|
||||
[ -d "$sdk" ] || fail 'OPENWRT_SDK_DIR must name an extracted SDK'
|
||||
[ -r "$secret" ] || fail 'OPENWRT_APK_SIGNING_KEY is not readable'
|
||||
[ -r "$public" ] || fail 'shared public key is not readable'
|
||||
"$root/scripts/check-apk-trust.sh"
|
||||
verify_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$verify_dir"' EXIT HUP INT TERM
|
||||
openssl ec -in "$secret" -pubout -out "$verify_dir/public.pem" >/dev/null 2>&1 ||
|
||||
fail 'invalid EC signing key'
|
||||
cmp -s "$verify_dir/public.pem" "$public" || fail 'signing key does not match shared public key'
|
||||
OPENWRT_APK_SIGNING_KEY="$secret" OPENWRT_APK_PUBLIC_KEY="$public" \
|
||||
"$root/scripts/build-apk.sh"
|
||||
apk_tool="$sdk/staging_dir/host/bin/apk"
|
||||
package="$root/dist/$PKG_NAME-$PKG_VERSION.apk"
|
||||
[ -x "$apk_tool" ] || fail 'SDK apk tool is unavailable'
|
||||
[ -r "$package" ] || fail 'built APK is unavailable'
|
||||
"$apk_tool" --allow-untrusted adbsign --sign-key "$secret" "$package"
|
||||
"$apk_tool" --keys-dir "$root/keys" verify "$package"
|
||||
mkdir -p "$root/dist/apk"
|
||||
cp "$package" "$root/dist/apk/"
|
||||
(cd "$root/dist/apk" && sha256sum "$PKG_NAME-$PKG_VERSION.apk" >SHA256SUMS.apk)
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
fail() { printf 'build-apk: %s\n' "$*" >&2; exit 1; }
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
. "$root/apk-feed.env"
|
||||
sdk="${OPENWRT_SDK_DIR:-}"
|
||||
[ -d "$sdk" ] || fail 'OPENWRT_SDK_DIR must name an extracted SDK'
|
||||
apk_tool="$sdk/staging_dir/host/bin/apk"
|
||||
[ -x "$apk_tool" ] || fail 'SDK apk tool is unavailable'
|
||||
"$root/scripts/check-version-sync.sh"
|
||||
work="$(mktemp -d)"
|
||||
cleanup() {
|
||||
if [ -d "$work" ]; then
|
||||
chown -R "$(id -u):$(id -g)" "$work" 2>/dev/null ||
|
||||
sudo -n chown -R "$(id -u):$(id -g)" "$work" 2>/dev/null || true
|
||||
rm -rf "$work"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
"$root/scripts/stage-package.sh" "$work/full"
|
||||
mkdir -p "$work/payload"
|
||||
cp -R "$work/full/usr" "$work/full/www" "$work/payload/"
|
||||
if ! chown -R 0:0 "$work/payload" 2>/dev/null; then
|
||||
command -v sudo >/dev/null 2>&1 || fail 'root-owned APK staging requires chown or sudo'
|
||||
sudo -n chown -R 0:0 "$work/payload" || fail 'could not normalize APK ownership'
|
||||
fi
|
||||
mkdir -p "$root/dist"
|
||||
artifact="$root/dist/$PKG_NAME-$PKG_VERSION.apk"
|
||||
"$apk_tool" mkpkg \
|
||||
--info "name:$PKG_NAME" \
|
||||
--info "version:$PKG_VERSION" \
|
||||
--info 'description:Active Wi-Fi access point QR codes for LuCI Status Overview' \
|
||||
--info "arch:$OPENWRT_APK_ARCH" \
|
||||
--info 'license:MIT' \
|
||||
--info "origin:$PKG_NAME" \
|
||||
--info 'maintainer:nikitid' \
|
||||
--info 'depends:libc luci-base luci-lib-uqr' \
|
||||
--script "post-install:$work/full/CONTROL/postinst" \
|
||||
--script "post-upgrade:$work/full/CONTROL/postinst" \
|
||||
--script "pre-deinstall:$work/full/CONTROL/prerm" \
|
||||
--files "$work/payload" \
|
||||
--output "$artifact"
|
||||
"$apk_tool" adbdump --format json "$artifact" >"$work/package.json"
|
||||
python3 - "$work/package.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], encoding="utf-8") as source:
|
||||
package = json.load(source)
|
||||
if package.get("info", {}).get("depends") != ["libc", "luci-base", "luci-lib-uqr"]:
|
||||
raise SystemExit("unexpected APK dependency set")
|
||||
for path in package.get("paths", []):
|
||||
entries = [path] + path.get("files", [])
|
||||
for entry in entries:
|
||||
acl = entry.get("acl", {})
|
||||
if acl.get("user") != "root" or acl.get("group") != "root":
|
||||
raise SystemExit("APK contains a non-root-owned path")
|
||||
if any(path.get("name") == "CONTROL" for path in package.get("paths", [])):
|
||||
raise SystemExit("IPK control directory leaked into APK payload")
|
||||
scripts = package.get("scripts", {})
|
||||
if set(scripts) != {"post-install", "post-upgrade", "pre-deinstall"}:
|
||||
raise SystemExit("unexpected APK lifecycle scripts")
|
||||
PY
|
||||
printf '%s\n' "$artifact"
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
stage="${BUILD_DIR:-$root/build/ipk}/stage"
|
||||
mkdir -p "$root/dist"
|
||||
"$root/scripts/check-version-sync.sh"
|
||||
"$root/scripts/stage-package.sh" "$stage"
|
||||
rm -f "$root/dist"/*.ipk "$root/dist/SHA256SUMS"
|
||||
python3 "$root/scripts/pack-ipk.py" "$stage" "$root/dist"
|
||||
(cd "$root/dist" && sha256sum ./*.ipk >SHA256SUMS)
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/apk-feed.env"
|
||||
actual="$(sha256sum "$root/$OPENWRT_APK_KEY_FILE" | awk '{print $1}')"
|
||||
[ "$actual" = "$OPENWRT_APK_TRUST_SHA256" ] || {
|
||||
printf 'shared APK public key checksum mismatch\n' >&2
|
||||
exit 1
|
||||
}
|
||||
if grep -R -E '(apk|opkg)[[:space:]]+upgrade([[:space:]]*$|[[:space:]]*(&&|;))' \
|
||||
"$root/scripts" "$root/README.md" >/dev/null 2>&1; then
|
||||
printf 'unscoped package upgrade found\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# template: check-readme v1 (repo-templates)
|
||||
# Formatted to pass both `shfmt -i 2` and `shfmt -i 2 -bn -ci`, the styles
|
||||
# the consuming repositories check with; each gets this file verbatim.
|
||||
# Do not edit in place: change templates/shared/ in repo-templates
|
||||
# and run scripts/sync-templates.sh --update.
|
||||
#
|
||||
# Hold README.md (English) and README.ru.md (Russian) to the shared layout:
|
||||
#
|
||||
# # <Product> for <platform> # <Product> для <platform>
|
||||
# [Русский](README.ru.md) [English](README.md)
|
||||
# badges: CI, Release, License
|
||||
# ## Features ## Возможности
|
||||
# ## Requirements ## Требования
|
||||
# ## Installation ## Установка
|
||||
# ## <project sections> ## <project sections>
|
||||
# ## Development ## Разработка
|
||||
# ## Documentation ## Документация
|
||||
# ## Support ## Поддержка
|
||||
# ## License ## Лицензия
|
||||
#
|
||||
# Both files must have the same number of sections, so one language cannot
|
||||
# silently drop what the other says. Punctuation is plain ASCII in both: no
|
||||
# typographic dashes, arrows, quotes or ellipses. The release badge is required
|
||||
# only where .github/workflows/release.yml exists. Every relative link must
|
||||
# resolve.
|
||||
|
||||
set -eu
|
||||
|
||||
root="$(CDPATH='' cd -- "$(dirname "$0")/.." && pwd)"
|
||||
cd "$root"
|
||||
|
||||
status=0
|
||||
fail() {
|
||||
printf 'README: %s\n' "$*" >&2
|
||||
status=1
|
||||
}
|
||||
|
||||
headings() {
|
||||
sed -n 's/^## //p' "$1"
|
||||
}
|
||||
|
||||
# check_layout <file> <first three sections> <last four sections>, each list
|
||||
# separated by '|'. Project sections may only sit between the two.
|
||||
check_layout() {
|
||||
file="$1"
|
||||
head_want="$2"
|
||||
tail_want="$3"
|
||||
if [ ! -f "$file" ]; then
|
||||
fail "$file is missing"
|
||||
return
|
||||
fi
|
||||
if ! head -n 1 "$file" | grep -q '^# '; then
|
||||
fail "$file does not start with a '# ' title"
|
||||
fi
|
||||
head_have="$(headings "$file" | head -n 3 | paste -sd '|' -)"
|
||||
tail_have="$(headings "$file" | tail -n 4 | paste -sd '|' -)"
|
||||
if [ "$head_have" != "$head_want" ]; then
|
||||
fail "$file: the first sections are '$head_have', expected '$head_want'"
|
||||
fi
|
||||
if [ "$tail_have" != "$tail_want" ]; then
|
||||
fail "$file: the last sections are '$tail_have', expected '$tail_want'"
|
||||
fi
|
||||
}
|
||||
|
||||
check_layout README.md 'Features|Requirements|Installation' \
|
||||
'Development|Documentation|Support|License'
|
||||
check_layout README.ru.md 'Возможности|Требования|Установка' \
|
||||
'Разработка|Документация|Поддержка|Лицензия'
|
||||
|
||||
if ! grep -qF '[Русский](README.ru.md)' README.md; then
|
||||
fail 'README.md does not link to README.ru.md'
|
||||
fi
|
||||
if ! grep -qF '[English](README.md)' README.ru.md; then
|
||||
fail 'README.ru.md does not link to README.md'
|
||||
fi
|
||||
|
||||
en="$(headings README.md | wc -l | tr -d ' ')"
|
||||
ru="$(headings README.ru.md | wc -l | tr -d ' ')"
|
||||
if [ "$en" != "$ru" ]; then
|
||||
fail "README.md has $en sections and README.ru.md has $ru"
|
||||
fi
|
||||
|
||||
for file in README.md README.ru.md; do
|
||||
if ! grep -q 'actions/workflows/.*badge\.svg' "$file"; then
|
||||
fail "$file has no CI badge"
|
||||
fi
|
||||
if ! grep -q 'img\.shields\.io/github/license\|License-\|license-' "$file"; then
|
||||
fail "$file has no license badge"
|
||||
fi
|
||||
if [ -f .github/workflows/release.yml ]; then
|
||||
if ! grep -q 'img\.shields\.io/github/v/release' "$file"; then
|
||||
fail "$file has no release badge"
|
||||
fi
|
||||
fi
|
||||
|
||||
# The curly quotes in the pattern are what it looks for, not a typo.
|
||||
# shellcheck disable=SC1112
|
||||
typographic="$(grep -n '—\|–\|→\|←\|«\|»\|“\|”\|‘\|’\|…' "$file" | head -n 3 || true)"
|
||||
if [ -n "$typographic" ]; then
|
||||
fail "$file has non-ASCII punctuation: $typographic"
|
||||
fi
|
||||
|
||||
# Relative links: [text](path) and [text](path#anchor), not URLs or anchors.
|
||||
links="$(grep -o '](\([^)#:]*\)[)#]' "$file" | sed 's/^](//; s/[)#]$//' | sort -u)"
|
||||
missing=""
|
||||
for target in $links; do
|
||||
[ -e "$target" ] || missing="$missing $target"
|
||||
done
|
||||
[ -z "$missing" ] || fail "$file links to missing files:$missing"
|
||||
done
|
||||
|
||||
[ "$status" -eq 0 ] && printf 'README layout OK: %s sections each\n' "$en"
|
||||
exit "$status"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
[ "${1:?usage: check-release-tag.sh TAG}" = "v$PKG_VERSION" ] || {
|
||||
printf 'release tag must be v%s\n' "$PKG_VERSION" >&2
|
||||
exit 1
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
. "$root/release.env"
|
||||
make_name="$(sed -n 's/^PKG_NAME:=//p' "$root/Makefile" | head -n 1)"
|
||||
make_version="$(sed -n 's/^PKG_VERSION:=//p' "$root/Makefile" | head -n 1)"
|
||||
make_arch="$(sed -n 's/^PKGARCH:=//p' "$root/Makefile" | head -n 1)"
|
||||
[ "$make_name" = "$PKG_NAME" ]
|
||||
[ "$make_version" = "$PKG_VERSION" ]
|
||||
[ "$make_arch" = "$PKG_ARCH" ]
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
cd "$root"
|
||||
./scripts/check-version-sync.sh
|
||||
./scripts/check-readme.sh
|
||||
./scripts/check-apk-trust.sh
|
||||
find scripts -type f -name '*.sh' -exec sh -n {} +
|
||||
find luci -type f -name '*.js' -exec node --check {} +
|
||||
node ./scripts/test-shared.js
|
||||
node ./scripts/test-overview-ui.js
|
||||
python3 -m py_compile scripts/pack-ipk.py
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
for path in Path('luci').glob('*.json'):
|
||||
json.loads(path.read_text())
|
||||
print('json OK:', path)
|
||||
PY
|
||||
./scripts/build-ipk.sh
|
||||
if find "${BUILD_DIR:-$root/build/ipk}/stage" -type f -name '._*' | grep -q .; then
|
||||
printf 'AppleDouble metadata leaked into staged package\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
first="$(sha256sum dist/*.ipk | awk '{print $1}')"
|
||||
./scripts/build-ipk.sh
|
||||
second="$(sha256sum dist/*.ipk | awk '{print $1}')"
|
||||
[ "$first" = "$second" ]
|
||||
git diff --check 2>/dev/null || true
|
||||
printf 'ci-check OK\n'
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic opkg package."""
|
||||
import gzip
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
|
||||
def normalize(info):
|
||||
info.uid = info.gid = info.mtime = 0
|
||||
info.uname = info.gname = "root"
|
||||
return info
|
||||
|
||||
|
||||
def archive(members):
|
||||
raw = io.BytesIO()
|
||||
with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as out:
|
||||
for name, path in members:
|
||||
info = normalize(out.gettarinfo(path, name))
|
||||
if info.isreg():
|
||||
with open(path, "rb") as source:
|
||||
out.addfile(info, source)
|
||||
else:
|
||||
out.addfile(info)
|
||||
compressed = io.BytesIO()
|
||||
with gzip.GzipFile(fileobj=compressed, mode="wb", mtime=0) as out:
|
||||
out.write(raw.getvalue())
|
||||
return compressed.getvalue()
|
||||
|
||||
|
||||
def walk(stage, control=False):
|
||||
result = []
|
||||
base = os.path.join(stage, "CONTROL") if control else stage
|
||||
for root, dirs, files in os.walk(base):
|
||||
dirs[:] = sorted(d for d in dirs if control or d != "CONTROL")
|
||||
rel = os.path.relpath(root, base)
|
||||
if rel != ".":
|
||||
result.append(("./" + rel, root))
|
||||
for name in sorted(files):
|
||||
result.append(("./" + os.path.join(rel, name).replace("./", "", 1),
|
||||
os.path.join(root, name)))
|
||||
return sorted(result)
|
||||
|
||||
|
||||
def control_field(path, field):
|
||||
with open(path, encoding="utf-8") as source:
|
||||
for line in source:
|
||||
if line.startswith(field + ":"):
|
||||
return line.split(":", 1)[1].strip()
|
||||
raise SystemExit("missing control field: " + field)
|
||||
|
||||
|
||||
stage, output = sys.argv[1:]
|
||||
control = os.path.join(stage, "CONTROL", "control")
|
||||
parts = {
|
||||
"debian-binary": b"2.0\n",
|
||||
"control.tar.gz": archive(walk(stage, True)),
|
||||
"data.tar.gz": archive(walk(stage, False)),
|
||||
}
|
||||
os.makedirs(output, exist_ok=True)
|
||||
name = "%s_%s_%s.ipk" % tuple(control_field(control, field) for field in
|
||||
("Package", "Version", "Architecture"))
|
||||
target = os.path.join(output, name)
|
||||
raw = io.BytesIO()
|
||||
with tarfile.open(fileobj=raw, mode="w", format=tarfile.GNU_FORMAT) as out:
|
||||
for part in ("debian-binary", "control.tar.gz", "data.tar.gz"):
|
||||
data = parts[part]
|
||||
info = normalize(tarfile.TarInfo("./" + part))
|
||||
info.mode, info.size = 0o644, len(data)
|
||||
out.addfile(info, io.BytesIO(data))
|
||||
with gzip.GzipFile(target, "wb", mtime=0) as out:
|
||||
out.write(raw.getvalue())
|
||||
print(target)
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# template: release-notes v3 (repo-templates)
|
||||
# Formatted to pass both `shfmt -i 2` and `shfmt -i 2 -bn -ci`, the styles
|
||||
# the consuming repositories check with; each gets this file verbatim.
|
||||
# Do not edit in place: change templates/shared/ in repo-templates
|
||||
# and run scripts/sync-templates.sh --update.
|
||||
#
|
||||
# Print release notes for a tag, built from the subjects of the commits since
|
||||
# the previous release tag. There is no changelog file: the commit subject is
|
||||
# the release note, so write it as the line a user should read.
|
||||
#
|
||||
# A commit whose subject is only a version number ("Release 1.2.0",
|
||||
# "release: v0.2.0") says nothing a user can read and is left out.
|
||||
#
|
||||
# A commit that touches only documentation, CI, tests or checks is listed
|
||||
# under "Documentation and tooling" rather than "Changes".
|
||||
#
|
||||
# Usage: scripts/release-notes.sh <tag> [previous-tag]
|
||||
#
|
||||
# The previous tag defaults to the nearest v* tag before <tag>. For a final
|
||||
# release, pre-release tags (_rc, _beta, _alpha) are skipped, so its notes
|
||||
# cover everything since the last final release rather than since its RC. The repository
|
||||
# for the compare link comes from GITHUB_REPOSITORY, or from the origin remote.
|
||||
# Needs the full history: check out with fetch-depth 0.
|
||||
|
||||
set -eu
|
||||
|
||||
[ $# -ge 1 ] || {
|
||||
printf 'usage: %s <tag> [previous-tag]\n' "$0" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
tag="$1"
|
||||
if [ $# -ge 2 ]; then
|
||||
previous="$2"
|
||||
elif printf '%s\n' "$tag" | grep -Eq '_(rc|beta|alpha)'; then
|
||||
previous="$(git describe --tags --abbrev=0 --match 'v*' "$tag^" 2>/dev/null || true)"
|
||||
else
|
||||
previous="$(git describe --tags --abbrev=0 --match 'v*' --exclude '*_rc*' \
|
||||
--exclude '*_beta*' --exclude '*_alpha*' "$tag^" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
repository="${GITHUB_REPOSITORY:-}"
|
||||
if [ -z "$repository" ]; then
|
||||
url="$(git remote get-url origin 2>/dev/null || true)"
|
||||
url="${url%.git}"
|
||||
repository="$(printf '%s\n' "$url" | sed -n 's#.*github\.com[:/]\(.*\)$#\1#p')"
|
||||
fi
|
||||
|
||||
if [ -n "$previous" ]; then
|
||||
range="$previous..$tag"
|
||||
else
|
||||
range="$tag"
|
||||
fi
|
||||
|
||||
# Subjects that carry nothing but a version number.
|
||||
bump='^(release|bump|version)[: ]+(to )?v?[0-9]+(\.[0-9]+)*([-_][a-z0-9]+)?$'
|
||||
|
||||
# Paths that never change what a user runs.
|
||||
tooling='^(docs/|\.github/|tests?/|scripts/(check|test|gen|ci)-|scripts/release-notes\.sh$|LICENSE|NOTICE|THIRD_PARTY|.*\.md$|\.[^/]+$)'
|
||||
|
||||
changes=""
|
||||
tooling_changes=""
|
||||
for commit in $(git rev-list --no-merges --reverse "$range"); do
|
||||
subject="$(git log -1 --format=%s "$commit")"
|
||||
if printf '%s\n' "$subject" | grep -Eiq "$bump"; then
|
||||
continue
|
||||
fi
|
||||
paths="$(git diff-tree --no-commit-id --name-only -r --root "$commit")"
|
||||
if printf '%s\n' "$paths" | grep -Evq "$tooling"; then
|
||||
changes="$changes- $subject
|
||||
"
|
||||
else
|
||||
tooling_changes="$tooling_changes- $subject
|
||||
"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$changes" ]; then
|
||||
printf '## Changes\n\n%s' "$changes"
|
||||
fi
|
||||
if [ -n "$tooling_changes" ]; then
|
||||
[ -z "$changes" ] || printf '\n'
|
||||
printf '## Documentation and tooling\n\n%s' "$tooling_changes"
|
||||
fi
|
||||
if [ -z "$changes$tooling_changes" ]; then
|
||||
printf 'Version update only; no other changes since %s.\n' "$previous"
|
||||
fi
|
||||
|
||||
if [ -n "$repository" ]; then
|
||||
if [ -n "$previous" ]; then
|
||||
printf '\n**Full diff**: https://github.com/%s/compare/%s...%s\n' \
|
||||
"$repository" "$previous" "$tag"
|
||||
else
|
||||
printf '\n**Full history**: https://github.com/%s/commits/%s\n' \
|
||||
"$repository" "$tag"
|
||||
fi
|
||||
fi
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
root="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
stage="${1:?usage: stage-package.sh STAGE_DIR}"
|
||||
. "$root/release.env"
|
||||
rm -rf "$stage"
|
||||
mkdir -p "$stage/CONTROL" "$stage/usr/share/rpcd/acl.d" \
|
||||
"$stage/usr/share/licenses/$PKG_NAME" \
|
||||
"$stage/www/luci-static/resources/wrqr" \
|
||||
"$stage/www/luci-static/resources/view/status/include"
|
||||
install -m 644 "$root/luci/acl.json" "$stage/usr/share/rpcd/acl.d/$PKG_NAME.json"
|
||||
install -m 644 "$root/luci/shared.js" "$stage/www/luci-static/resources/wrqr/shared.js"
|
||||
install -m 644 "$root/luci/overview.js" "$stage/www/luci-static/resources/view/status/include/15_wifi-qr.js"
|
||||
install -m 644 "$root/LICENSE" "$stage/usr/share/licenses/$PKG_NAME/LICENSE"
|
||||
cat >"$stage/CONTROL/control" <<EOF
|
||||
Package: $PKG_NAME
|
||||
Version: $PKG_VERSION
|
||||
Architecture: $PKG_ARCH
|
||||
Maintainer: nikitid
|
||||
Depends: luci-base, luci-lib-uqr
|
||||
Section: luci
|
||||
Priority: optional
|
||||
Description: Display active Wi-Fi access point QR codes on LuCI Status Overview.
|
||||
EOF
|
||||
cat >"$stage/CONTROL/postinst" <<'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT:-}" ] && exit 0
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache
|
||||
exit 0
|
||||
EOF
|
||||
cat >"$stage/CONTROL/prerm" <<'EOF'
|
||||
#!/bin/sh
|
||||
[ -n "${IPKG_INSTROOT:-}" ] && exit 0
|
||||
case "${1:-}" in upgrade) exit 0 ;; esac
|
||||
[ "${PKG_UPGRADE:-0}" = 1 ] && exit 0
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache
|
||||
exit 0
|
||||
EOF
|
||||
chmod 755 "$stage/CONTROL/postinst" "$stage/CONTROL/prerm"
|
||||
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
let replacements = 0;
|
||||
let qrRenders = 0;
|
||||
|
||||
function node(tag, attrs, children) {
|
||||
return {
|
||||
tag, attrs: attrs || {}, children: children || []
|
||||
};
|
||||
}
|
||||
|
||||
function evaluate(path, context) {
|
||||
return vm.runInNewContext(`(function(){${fs.readFileSync(path, 'utf8')}\n})()`, context);
|
||||
}
|
||||
|
||||
const common = evaluate('luci/shared.js', {
|
||||
baseclass: { extend: value => value }, encodeURIComponent
|
||||
});
|
||||
const rpcCalls = [];
|
||||
const context = {
|
||||
baseclass: { extend: value => value },
|
||||
common,
|
||||
dom: { content: (target, next) => { replacements++; target.children = next; } },
|
||||
uqr: { renderSVG: value => {
|
||||
qrRenders++;
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="148" height="148">${value}</svg>`;
|
||||
} },
|
||||
rpc: { declare: spec => (...args) => { rpcCalls.push([spec.object, spec.method, args]); return Promise.resolve({}); } },
|
||||
L: { resolveDefault: promise => promise },
|
||||
Promise,
|
||||
E: (tag, attrs, children) => node(tag, attrs, children)
|
||||
};
|
||||
const widget = evaluate('luci/overview.js', context);
|
||||
|
||||
const config = { values: {
|
||||
radio0: { '.type': 'wifi-device', band: '5g' },
|
||||
ap0: { '.type': 'wifi-iface', device: 'radio0', mode: 'ap', ssid: 'Home', encryption: 'sae-mixed', key: 'secret' }
|
||||
} };
|
||||
const running = { radio0: { up: true, interfaces: [
|
||||
{ section: 'ap0', ifname: 'phy0-ap0', config: { mode: 'ap' } }
|
||||
] } };
|
||||
|
||||
const firstRoot = widget.render([ config, running ]);
|
||||
if (replacements !== 1 || qrRenders !== 1)
|
||||
throw new Error('first render did not create exactly one QR snapshot');
|
||||
const qrMarkup = firstRoot.children[1].children[0].children[2].children[0].tag;
|
||||
if (!qrMarkup.includes('viewBox="0 0 148 148"') || qrMarkup.includes(' width="148"'))
|
||||
throw new Error('QR SVG was not converted to a scalable viewBox');
|
||||
|
||||
replacements = 0;
|
||||
qrRenders = 0;
|
||||
const secondRoot = widget.render([ config, running ]);
|
||||
if (secondRoot !== firstRoot)
|
||||
throw new Error('polling replaced the stable widget root');
|
||||
if (replacements !== 0 || qrRenders !== 0)
|
||||
throw new Error('unchanged poll mutated or regenerated the widget');
|
||||
|
||||
const stopped = { radio0: { up: false, interfaces: [] } };
|
||||
widget.render([ config, stopped ]);
|
||||
if (replacements !== 1)
|
||||
throw new Error('runtime change was not rendered');
|
||||
|
||||
widget.load().then(() => {
|
||||
if (JSON.stringify(rpcCalls.map(call => call.slice(0, 2))) !==
|
||||
JSON.stringify([ [ 'uci', 'get' ], [ 'luci-rpc', 'getWirelessDevices' ] ]))
|
||||
throw new Error('load did not refresh configuration and runtime state');
|
||||
console.log('overview UI tests OK');
|
||||
}).catch(error => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const vm = require('vm');
|
||||
|
||||
function load(path) {
|
||||
return vm.runInNewContext(`(function(){${fs.readFileSync(path, 'utf8')}\n})()`, {
|
||||
baseclass: { extend: value => value },
|
||||
encodeURIComponent
|
||||
});
|
||||
}
|
||||
|
||||
const common = load('luci/shared.js');
|
||||
|
||||
function equal(actual, expected, label) {
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected))
|
||||
throw new Error(`${label}: ${JSON.stringify(actual)} != ${JSON.stringify(expected)}`);
|
||||
}
|
||||
|
||||
equal(common.percentEncode('a;b\nя'), 'a%3Bb%0A%D1%8F', 'percent encoding');
|
||||
equal(common.payload({ ssid: 'Guest', encryption: 'none' }),
|
||||
'WIFI:S:Guest;;', 'open network');
|
||||
equal(common.payload({ ssid: 'Main', encryption: 'sae-mixed', key: 'secret' }),
|
||||
'WIFI:T:WPA;S:Main;P:secret;;', 'transition network');
|
||||
equal(common.payload({ ssid: 'Main', encryption: 'sae', key: 'secret', hidden: '1' }),
|
||||
'WIFI:T:WPA;R:0;S:Main;H:true;P:secret;;', 'WPA3 network');
|
||||
equal(common.payload({ ssid: 'Corp', encryption: 'wpa2', key: 'secret' }), null,
|
||||
'enterprise network');
|
||||
|
||||
const config = { values: {
|
||||
radio0: { '.type': 'wifi-device', band: '2g' },
|
||||
radio1: { '.type': 'wifi-device', band: '5g' },
|
||||
radio2: { '.type': 'wifi-device', band: '6g', disabled: '1' },
|
||||
ap0: { '.type': 'wifi-iface', device: 'radio0', mode: 'ap', ssid: 'Home', encryption: 'psk2', key: 'same' },
|
||||
ap1: { '.type': 'wifi-iface', device: 'radio1', mode: 'ap', ssid: 'Home', encryption: 'sae-mixed', key: 'same' },
|
||||
ap2: { '.type': 'wifi-iface', device: 'radio2', mode: 'ap', ssid: 'Off', encryption: 'sae', key: 'off' },
|
||||
sta0: { '.type': 'wifi-iface', device: 'radio1', mode: 'sta', ssid: 'Upstream', encryption: 'psk2', key: 'skip' },
|
||||
broken: { '.type': 'wifi-iface', device: 'radio1', mode: 'ap', ssid: 'Broken', encryption: 'psk2', key: 'skip' }
|
||||
} };
|
||||
const runtime = {
|
||||
radio0: { up: true, interfaces: [ { section: 'ap0', ifname: 'phy0-ap0', config: { mode: 'ap' } } ] },
|
||||
radio1: { up: true, interfaces: [
|
||||
{ section: 'ap1', ifname: 'phy1-ap0', config: { mode: 'ap' } },
|
||||
{ section: 'sta0', ifname: 'phy1-sta0', config: { mode: 'sta' } }
|
||||
] },
|
||||
radio2: { up: false, interfaces: [ { section: 'ap2', ifname: 'phy2-ap0', config: { mode: 'ap' } } ] }
|
||||
};
|
||||
|
||||
equal(common.collect(config, runtime), [ {
|
||||
ssid: 'Home',
|
||||
payload: 'WIFI:T:WPA;S:Home;P:same;;',
|
||||
bands: [ '2.4 GHz', '5 GHz' ]
|
||||
} ], 'active AP collection and deduplication');
|
||||
equal(common.collect(config.values, runtime).length, 1, 'RPC-unwrapped UCI response');
|
||||
|
||||
console.log('shared tests OK');
|
||||
Reference in new issue
Block a user