mirror of
https://github.com/openwrt/luci.git
synced 2026-04-15 10:51:51 +00:00
package-manager: migrate to apk-tools JSON API
Replace legacy opkg status parsing with native apk-tools JSON queries. This modernizes the backend calls and improves dependency resolution. - Implement 'apk query --format json' for package data retrieval. - Add robust regex for versioned dependencies (e.g., name>=version). - Update version comparison to handle APK date-based revisions. - Fix label-input associations to resolve accessibility warnings. - Retain deprecated opkg fallback logic for LuCI master. Signed-off-by: Konstantin Glukhov <KGlukhov@Hotmail.com>
This commit is contained in:
committed by
Paul Donald
parent
8420a723f6
commit
1624418f64
@@ -152,7 +152,7 @@ function parseList(s, dest)
|
||||
|
||||
switch (installed) {
|
||||
case 'installed':
|
||||
pkg.installed = true;
|
||||
pkg.status = ['installed'];
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -201,6 +201,59 @@ function parseList(s, dest)
|
||||
}
|
||||
}
|
||||
|
||||
function parseApkQueryJson(s, dest) {
|
||||
// Parse the raw JSON text string into an array
|
||||
let rawData;
|
||||
try {
|
||||
rawData = JSON.parse(s);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse APK JSON:", e);
|
||||
return;
|
||||
}
|
||||
// Ensure rawData is actually an array before iterating
|
||||
if (!Array.isArray(rawData))
|
||||
return;
|
||||
// Ensure our storage objects exist on the destination
|
||||
dest.pkgs = dest.pkgs || {};
|
||||
dest.providers = dest.providers || {};
|
||||
|
||||
// Single pass through each item in the new JSON array
|
||||
for (const item of rawData) {
|
||||
|
||||
// Rename 'file-size' to 'size' while pulling out 'name'
|
||||
const { name, 'file-size': size, ...attributes } = item;
|
||||
|
||||
// Construct the package object with the expected 'size' key
|
||||
const pkg = { name, size, ...attributes };
|
||||
|
||||
// Add/Update the package in the main map
|
||||
dest.pkgs[name] = pkg;
|
||||
|
||||
// Determine all provided names (a package always provides itself)
|
||||
const provides = [name, ...(Array.isArray(pkg.provides) ? pkg.provides : [])];
|
||||
for (const p of provides) {
|
||||
dest.providers[p] = dest.providers[p] || [];
|
||||
if (!dest.providers[p].includes(pkg))
|
||||
dest.providers[p].push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parsePackageData(s, dest) {
|
||||
if (!s) return;
|
||||
|
||||
// Check if the input is JSON (starts with [)
|
||||
if (s.trim().charAt(0) === '[') {
|
||||
parseApkQueryJson(s, dest);
|
||||
} else {
|
||||
parseList(s, dest);
|
||||
}
|
||||
}
|
||||
|
||||
function isPkgInstalled(pkg) {
|
||||
return pkg && Array.isArray(pkg.status) && pkg.status.includes('installed');
|
||||
}
|
||||
|
||||
function display(pattern)
|
||||
{
|
||||
const src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode];
|
||||
@@ -249,7 +302,7 @@ function display(pattern)
|
||||
const avail = packages.available.pkgs[name];
|
||||
const inst = packages.installed.pkgs[name];
|
||||
|
||||
if (!inst || !inst.installed)
|
||||
if (!isPkgInstalled(inst))
|
||||
continue;
|
||||
|
||||
if (!avail || compareVersion(avail.version, pkg.version) <= 0)
|
||||
@@ -267,7 +320,7 @@ function display(pattern)
|
||||
}, _('Upgrade…'));
|
||||
}
|
||||
else if (currentDisplayMode === 'installed') {
|
||||
if (!pkg.installed)
|
||||
if (!isPkgInstalled(pkg))
|
||||
continue;
|
||||
|
||||
ver = truncateVersion(pkg.version || '-');
|
||||
@@ -282,14 +335,14 @@ function display(pattern)
|
||||
|
||||
ver = truncateVersion(pkg.version || '-');
|
||||
|
||||
if (!inst || !inst.installed)
|
||||
if (!isPkgInstalled(inst))
|
||||
btn = E('div', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'data-package': name,
|
||||
'data-action': 'install',
|
||||
'click': handleInstall
|
||||
}, _('Install…'));
|
||||
else if (inst.installed && inst.version != pkg.version)
|
||||
else if (isPkgInstalled(inst) && inst.version !== pkg.version)
|
||||
btn = E('div', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'data-package': name,
|
||||
@@ -420,46 +473,24 @@ function orderOf(c)
|
||||
return c.charCodeAt(0) + 256;
|
||||
}
|
||||
|
||||
function compareVersion(val, ref)
|
||||
{
|
||||
let vi = 0, ri = 0;
|
||||
const isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
|
||||
function compareVersion(val, ref) {
|
||||
// 1. Split into parts by dots or any non-digit sequences
|
||||
const vParts = (val || '').split(/[^0-9]+/);
|
||||
const rParts = (ref || '').split(/[^0-9]+/);
|
||||
|
||||
val = val || '';
|
||||
ref = ref || '';
|
||||
// 2. Filter out empty strings caused by leading/trailing non-digits
|
||||
const v = vParts.filter(x => x.length > 0);
|
||||
const r = rParts.filter(x => x.length > 0);
|
||||
|
||||
if (val === ref)
|
||||
return 0;
|
||||
const maxLen = Math.max(v.length, r.length);
|
||||
|
||||
while (vi < val.length || ri < ref.length) {
|
||||
let first_diff = 0;
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
// Convert to integer (base 10) to automatically ignore leading zeros
|
||||
const vNum = parseInt(v[i] || 0, 10);
|
||||
const rNum = parseInt(r[i] || 0, 10);
|
||||
|
||||
while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
|
||||
(ri < ref.length && !isdigit[ref.charAt(ri)])) {
|
||||
const vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
|
||||
if (vc !== rc)
|
||||
return vc - rc;
|
||||
|
||||
vi++; ri++;
|
||||
}
|
||||
|
||||
while (val.charAt(vi) === '0')
|
||||
vi++;
|
||||
|
||||
while (ref.charAt(ri) === '0')
|
||||
ri++;
|
||||
|
||||
while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
|
||||
first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
|
||||
vi++; ri++;
|
||||
}
|
||||
|
||||
if (isdigit[val.charAt(vi)])
|
||||
return 1;
|
||||
else if (isdigit[ref.charAt(ri)])
|
||||
return -1;
|
||||
else if (first_diff)
|
||||
return first_diff;
|
||||
if (vNum > rNum) return 1;
|
||||
if (vNum < rNum) return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -485,7 +516,7 @@ function versionSatisfied(ver, ref, vop)
|
||||
return r > 0;
|
||||
|
||||
case '=':
|
||||
return r == 0;
|
||||
return r === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -496,7 +527,7 @@ function pkgStatus(pkg, vop, ver, info)
|
||||
info.errors = info.errors || [];
|
||||
info.install = info.install || [];
|
||||
|
||||
if (pkg.installed) {
|
||||
if (isPkgInstalled(pkg)) {
|
||||
if (vop && !versionSatisfied(pkg.version, ver, vop)) {
|
||||
let repl = null;
|
||||
|
||||
@@ -605,12 +636,17 @@ function renderDependencies(depends, info, flat)
|
||||
if (deps[i] === 'libc')
|
||||
continue;
|
||||
|
||||
if (deps[i].match(/^(.+?)\s+\((<=|>=|<<|>>|<|>|=)(.+?)\)/)) {
|
||||
dep = RegExp.$1.trim();
|
||||
vop = RegExp.$2.trim();
|
||||
ver = RegExp.$3.trim();
|
||||
}
|
||||
else {
|
||||
// This regex handles "name", "name>=ver", and "name (>=ver)"
|
||||
const match = deps[i].match(/^([^><=~\s]+)\s*\(?([><=~]+)?\s*([^)]+)?\)?$/);
|
||||
|
||||
if (match) {
|
||||
// Destructure the match array: [full_match, name, operator, version]
|
||||
const [, matchedDep, matchedVop, matchedVer] = match;
|
||||
dep = (matchedDep || '').trim();
|
||||
vop = (matchedVop || '').trim() || null;
|
||||
ver = (matchedVer || '').trim() || null;
|
||||
} else {
|
||||
// Fallback if the string is just a plain name with no operators
|
||||
dep = deps[i].trim();
|
||||
vop = ver = null;
|
||||
}
|
||||
@@ -701,16 +737,16 @@ function handleInstall(ev)
|
||||
const i18n_packages = [];
|
||||
let i18n_tree;
|
||||
|
||||
if (luci_basename && (luci_basename[1] != 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
|
||||
if (luci_basename && (luci_basename[1] !== 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
|
||||
let i18n_filter;
|
||||
|
||||
if (luci_basename[1] == 'i18n') {
|
||||
if (luci_basename[1] === 'i18n') {
|
||||
const basenames = [];
|
||||
|
||||
for (let pkgname in packages.installed.pkgs) {
|
||||
const m = pkgname.match(/^luci-([^-]+)-(.+)$/);
|
||||
|
||||
if (m && m[1] != 'i18n')
|
||||
if (m && m[1] !== 'i18n')
|
||||
basenames.push(m[2]);
|
||||
}
|
||||
|
||||
@@ -723,7 +759,7 @@ function handleInstall(ev)
|
||||
|
||||
if (i18n_filter) {
|
||||
for (let pkgname in packages.available.pkgs)
|
||||
if (pkgname != pkg.name && pkgname.match(i18n_filter))
|
||||
if (pkgname !== pkg.name && pkgname.match(i18n_filter))
|
||||
i18n_packages.push(pkgname);
|
||||
|
||||
const i18ncache = {};
|
||||
@@ -876,9 +912,9 @@ function handleConfig(ev)
|
||||
}
|
||||
|
||||
for (let i = 0; i < partials.length; i++) {
|
||||
if (partials[i].type == 'file') {
|
||||
if (partials[i].type === 'file') {
|
||||
if (L.hasSystemFeature('apk')) {
|
||||
if (partials[i].name == 'repositories')
|
||||
if (partials[i].name === 'repositories')
|
||||
files.push(base_dir + '/' + partials[i].name);
|
||||
} else if (partials[i].name.match(/\.conf$/)) {
|
||||
files.push(base_dir + '/' + partials[i].name);
|
||||
@@ -1010,7 +1046,7 @@ function handlePkg(ev)
|
||||
|
||||
const argv = [ cmd ];
|
||||
|
||||
if (cmd == 'remove')
|
||||
if (cmd === 'remove')
|
||||
argv.push('--force-removal-of-dependent-packages')
|
||||
|
||||
if (rem && rem.checked)
|
||||
@@ -1118,14 +1154,14 @@ function updateLists(data)
|
||||
|
||||
return (data ? Promise.resolve(data) : downloadLists()).then(function(data) {
|
||||
const pg = document.querySelector('.cbi-progressbar');
|
||||
const mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' }))
|
||||
const mount = L.toArray(data[0].filter(function(m) { return m.mount === '/' || m.mount === '/overlay' }))
|
||||
.sort(function(a, b) { return a.mount > b.mount })[0] || { size: 0, free: 0 };
|
||||
|
||||
pg.firstElementChild.style.width = Math.floor(mount.size ? (100 / mount.size) * (mount.size - mount.free) : 100) + '%';
|
||||
pg.setAttribute('title', _('%s used (%1024mB used of %1024mB, %1024mB free)').format(pg.firstElementChild.style.width, mount.size - mount.free, mount.size, mount.free));
|
||||
|
||||
parseList(data[1], packages.available);
|
||||
parseList(data[2], packages.installed);
|
||||
parsePackageData(data[1], packages.available);
|
||||
parsePackageData(data[2], packages.installed);
|
||||
|
||||
for (let pkgname in packages.installed.pkgs)
|
||||
if (pkgname.indexOf('luci-i18n-base-') === 0)
|
||||
@@ -1169,12 +1205,12 @@ return view.extend({
|
||||
|
||||
E('div', { 'class': 'controls' }, [
|
||||
E('div', {}, [
|
||||
E('label', {}, _('Disk space') + ':'),
|
||||
E('label', {'id': 'disk-space-label'}, _('Disk space') + ':'),
|
||||
E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
|
||||
]),
|
||||
|
||||
E('div', {}, [
|
||||
E('label', {}, _('Filter') + ':'),
|
||||
E('label', {'id': 'filter-label'}, _('Filter') + ':'),
|
||||
E('span', { 'class': 'control-group' }, [
|
||||
E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'input': handleInput }),
|
||||
E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ])
|
||||
@@ -1182,7 +1218,7 @@ return view.extend({
|
||||
]),
|
||||
|
||||
E('div', {}, [
|
||||
E('label', {}, _('Download and install package') + ':'),
|
||||
E('label', {'id': 'download-label'}, _('Download and install package') + ':'),
|
||||
E('span', { 'class': 'control-group' }, [
|
||||
E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev) { if (ev.keyCode === 13) handleManualInstall(ev) }, 'disabled': isReadonlyView }),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall, 'disabled': isReadonlyView }, [ _('OK') ])
|
||||
@@ -1190,7 +1226,7 @@ return view.extend({
|
||||
]),
|
||||
|
||||
E('div', {}, [
|
||||
E('label', {}, _('Actions') + ':'), ' ',
|
||||
E('label', {'id': 'action-label'}, _('Actions') + ':'), ' ',
|
||||
E('span', { 'class': 'control-group' }, [
|
||||
E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handlePkg, 'disabled': isReadonlyView }, [ _('Update lists…') ]), ' ',
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload, 'disabled': isReadonlyView }, [ _('Upload Package…') ]), ' ',
|
||||
|
||||
@@ -14,14 +14,14 @@ fi
|
||||
case "$action" in
|
||||
list-installed)
|
||||
if [ $ipkg_bin = "apk" ]; then
|
||||
$ipkg_bin list -I --full 2>/dev/null
|
||||
$ipkg_bin query --fields all --format json --installed --from system \* 2>/dev/null
|
||||
else
|
||||
cat /usr/lib/opkg/status
|
||||
fi
|
||||
;;
|
||||
list-available)
|
||||
if [ $ipkg_bin = "apk" ]; then
|
||||
$ipkg_bin list --full 2>/dev/null
|
||||
$ipkg_bin query --fields all --format json --available \* 2>/dev/null
|
||||
else
|
||||
lists_dir=$(sed -rne 's#^lists_dir \S+ (\S+)#\1#p' /etc/opkg.conf /etc/opkg/*.conf 2>/dev/null | tail -n 1)
|
||||
find "${lists_dir:-/usr/lib/opkg/lists}" -type f '!' -name '*.sig' | xargs -r gzip -cd
|
||||
@@ -31,18 +31,12 @@ case "$action" in
|
||||
(
|
||||
cmd="$ipkg_bin"
|
||||
|
||||
# APK have command renamed
|
||||
# The apk package manager uses distinct commands for installing and removing packages.
|
||||
if [ $ipkg_bin = "apk" ]; then
|
||||
case "$action" in
|
||||
install)
|
||||
action="add"
|
||||
;;
|
||||
update)
|
||||
action="update"
|
||||
;;
|
||||
upgrade)
|
||||
action="upgrade"
|
||||
;;
|
||||
remove)
|
||||
action="del"
|
||||
;;
|
||||
@@ -54,6 +48,11 @@ case "$action" in
|
||||
if [ $ipkg_bin = "apk" ]; then
|
||||
while [ -n "$1" ]; do
|
||||
case "$1" in
|
||||
--autoremove)
|
||||
# Just shift and do nothing.
|
||||
# 'apk del' already cleans up orphaned dependencies by default.
|
||||
shift
|
||||
;;
|
||||
--force-removal-of-dependent-packages)
|
||||
cmd="$cmd -r"
|
||||
shift
|
||||
|
||||
Reference in New Issue
Block a user