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) {
|
switch (installed) {
|
||||||
case 'installed':
|
case 'installed':
|
||||||
pkg.installed = true;
|
pkg.status = ['installed'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
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)
|
function display(pattern)
|
||||||
{
|
{
|
||||||
const src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode];
|
const src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode];
|
||||||
@@ -249,7 +302,7 @@ function display(pattern)
|
|||||||
const avail = packages.available.pkgs[name];
|
const avail = packages.available.pkgs[name];
|
||||||
const inst = packages.installed.pkgs[name];
|
const inst = packages.installed.pkgs[name];
|
||||||
|
|
||||||
if (!inst || !inst.installed)
|
if (!isPkgInstalled(inst))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (!avail || compareVersion(avail.version, pkg.version) <= 0)
|
if (!avail || compareVersion(avail.version, pkg.version) <= 0)
|
||||||
@@ -267,7 +320,7 @@ function display(pattern)
|
|||||||
}, _('Upgrade…'));
|
}, _('Upgrade…'));
|
||||||
}
|
}
|
||||||
else if (currentDisplayMode === 'installed') {
|
else if (currentDisplayMode === 'installed') {
|
||||||
if (!pkg.installed)
|
if (!isPkgInstalled(pkg))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ver = truncateVersion(pkg.version || '-');
|
ver = truncateVersion(pkg.version || '-');
|
||||||
@@ -282,14 +335,14 @@ function display(pattern)
|
|||||||
|
|
||||||
ver = truncateVersion(pkg.version || '-');
|
ver = truncateVersion(pkg.version || '-');
|
||||||
|
|
||||||
if (!inst || !inst.installed)
|
if (!isPkgInstalled(inst))
|
||||||
btn = E('div', {
|
btn = E('div', {
|
||||||
'class': 'btn cbi-button-action',
|
'class': 'btn cbi-button-action',
|
||||||
'data-package': name,
|
'data-package': name,
|
||||||
'data-action': 'install',
|
'data-action': 'install',
|
||||||
'click': handleInstall
|
'click': handleInstall
|
||||||
}, _('Install…'));
|
}, _('Install…'));
|
||||||
else if (inst.installed && inst.version != pkg.version)
|
else if (isPkgInstalled(inst) && inst.version !== pkg.version)
|
||||||
btn = E('div', {
|
btn = E('div', {
|
||||||
'class': 'btn cbi-button-positive',
|
'class': 'btn cbi-button-positive',
|
||||||
'data-package': name,
|
'data-package': name,
|
||||||
@@ -420,46 +473,24 @@ function orderOf(c)
|
|||||||
return c.charCodeAt(0) + 256;
|
return c.charCodeAt(0) + 256;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareVersion(val, ref)
|
function compareVersion(val, ref) {
|
||||||
{
|
// 1. Split into parts by dots or any non-digit sequences
|
||||||
let vi = 0, ri = 0;
|
const vParts = (val || '').split(/[^0-9]+/);
|
||||||
const isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
|
const rParts = (ref || '').split(/[^0-9]+/);
|
||||||
|
|
||||||
val = val || '';
|
// 2. Filter out empty strings caused by leading/trailing non-digits
|
||||||
ref = ref || '';
|
const v = vParts.filter(x => x.length > 0);
|
||||||
|
const r = rParts.filter(x => x.length > 0);
|
||||||
|
|
||||||
if (val === ref)
|
const maxLen = Math.max(v.length, r.length);
|
||||||
return 0;
|
|
||||||
|
|
||||||
while (vi < val.length || ri < ref.length) {
|
for (let i = 0; i < maxLen; i++) {
|
||||||
let first_diff = 0;
|
// 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)]) ||
|
if (vNum > rNum) return 1;
|
||||||
(ri < ref.length && !isdigit[ref.charAt(ri)])) {
|
if (vNum < rNum) return -1;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -485,7 +516,7 @@ function versionSatisfied(ver, ref, vop)
|
|||||||
return r > 0;
|
return r > 0;
|
||||||
|
|
||||||
case '=':
|
case '=':
|
||||||
return r == 0;
|
return r === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -496,7 +527,7 @@ function pkgStatus(pkg, vop, ver, info)
|
|||||||
info.errors = info.errors || [];
|
info.errors = info.errors || [];
|
||||||
info.install = info.install || [];
|
info.install = info.install || [];
|
||||||
|
|
||||||
if (pkg.installed) {
|
if (isPkgInstalled(pkg)) {
|
||||||
if (vop && !versionSatisfied(pkg.version, ver, vop)) {
|
if (vop && !versionSatisfied(pkg.version, ver, vop)) {
|
||||||
let repl = null;
|
let repl = null;
|
||||||
|
|
||||||
@@ -605,12 +636,17 @@ function renderDependencies(depends, info, flat)
|
|||||||
if (deps[i] === 'libc')
|
if (deps[i] === 'libc')
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (deps[i].match(/^(.+?)\s+\((<=|>=|<<|>>|<|>|=)(.+?)\)/)) {
|
// This regex handles "name", "name>=ver", and "name (>=ver)"
|
||||||
dep = RegExp.$1.trim();
|
const match = deps[i].match(/^([^><=~\s]+)\s*\(?([><=~]+)?\s*([^)]+)?\)?$/);
|
||||||
vop = RegExp.$2.trim();
|
|
||||||
ver = RegExp.$3.trim();
|
if (match) {
|
||||||
}
|
// Destructure the match array: [full_match, name, operator, version]
|
||||||
else {
|
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();
|
dep = deps[i].trim();
|
||||||
vop = ver = null;
|
vop = ver = null;
|
||||||
}
|
}
|
||||||
@@ -701,16 +737,16 @@ function handleInstall(ev)
|
|||||||
const i18n_packages = [];
|
const i18n_packages = [];
|
||||||
let i18n_tree;
|
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;
|
let i18n_filter;
|
||||||
|
|
||||||
if (luci_basename[1] == 'i18n') {
|
if (luci_basename[1] === 'i18n') {
|
||||||
const basenames = [];
|
const basenames = [];
|
||||||
|
|
||||||
for (let pkgname in packages.installed.pkgs) {
|
for (let pkgname in packages.installed.pkgs) {
|
||||||
const m = pkgname.match(/^luci-([^-]+)-(.+)$/);
|
const m = pkgname.match(/^luci-([^-]+)-(.+)$/);
|
||||||
|
|
||||||
if (m && m[1] != 'i18n')
|
if (m && m[1] !== 'i18n')
|
||||||
basenames.push(m[2]);
|
basenames.push(m[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -723,7 +759,7 @@ function handleInstall(ev)
|
|||||||
|
|
||||||
if (i18n_filter) {
|
if (i18n_filter) {
|
||||||
for (let pkgname in packages.available.pkgs)
|
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);
|
i18n_packages.push(pkgname);
|
||||||
|
|
||||||
const i18ncache = {};
|
const i18ncache = {};
|
||||||
@@ -876,9 +912,9 @@ function handleConfig(ev)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < partials.length; i++) {
|
for (let i = 0; i < partials.length; i++) {
|
||||||
if (partials[i].type == 'file') {
|
if (partials[i].type === 'file') {
|
||||||
if (L.hasSystemFeature('apk')) {
|
if (L.hasSystemFeature('apk')) {
|
||||||
if (partials[i].name == 'repositories')
|
if (partials[i].name === 'repositories')
|
||||||
files.push(base_dir + '/' + partials[i].name);
|
files.push(base_dir + '/' + partials[i].name);
|
||||||
} else if (partials[i].name.match(/\.conf$/)) {
|
} else if (partials[i].name.match(/\.conf$/)) {
|
||||||
files.push(base_dir + '/' + partials[i].name);
|
files.push(base_dir + '/' + partials[i].name);
|
||||||
@@ -1010,7 +1046,7 @@ function handlePkg(ev)
|
|||||||
|
|
||||||
const argv = [ cmd ];
|
const argv = [ cmd ];
|
||||||
|
|
||||||
if (cmd == 'remove')
|
if (cmd === 'remove')
|
||||||
argv.push('--force-removal-of-dependent-packages')
|
argv.push('--force-removal-of-dependent-packages')
|
||||||
|
|
||||||
if (rem && rem.checked)
|
if (rem && rem.checked)
|
||||||
@@ -1118,14 +1154,14 @@ function updateLists(data)
|
|||||||
|
|
||||||
return (data ? Promise.resolve(data) : downloadLists()).then(function(data) {
|
return (data ? Promise.resolve(data) : downloadLists()).then(function(data) {
|
||||||
const pg = document.querySelector('.cbi-progressbar');
|
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 };
|
.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.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));
|
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);
|
parsePackageData(data[1], packages.available);
|
||||||
parseList(data[2], packages.installed);
|
parsePackageData(data[2], packages.installed);
|
||||||
|
|
||||||
for (let pkgname in packages.installed.pkgs)
|
for (let pkgname in packages.installed.pkgs)
|
||||||
if (pkgname.indexOf('luci-i18n-base-') === 0)
|
if (pkgname.indexOf('luci-i18n-base-') === 0)
|
||||||
@@ -1169,12 +1205,12 @@ return view.extend({
|
|||||||
|
|
||||||
E('div', { 'class': 'controls' }, [
|
E('div', { 'class': 'controls' }, [
|
||||||
E('div', {}, [
|
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', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
|
||||||
]),
|
]),
|
||||||
|
|
||||||
E('div', {}, [
|
E('div', {}, [
|
||||||
E('label', {}, _('Filter') + ':'),
|
E('label', {'id': 'filter-label'}, _('Filter') + ':'),
|
||||||
E('span', { 'class': 'control-group' }, [
|
E('span', { 'class': 'control-group' }, [
|
||||||
E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'input': handleInput }),
|
E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'input': handleInput }),
|
||||||
E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ])
|
E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ])
|
||||||
@@ -1182,7 +1218,7 @@ return view.extend({
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
E('div', {}, [
|
E('div', {}, [
|
||||||
E('label', {}, _('Download and install package') + ':'),
|
E('label', {'id': 'download-label'}, _('Download and install package') + ':'),
|
||||||
E('span', { 'class': 'control-group' }, [
|
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('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') ])
|
E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall, 'disabled': isReadonlyView }, [ _('OK') ])
|
||||||
@@ -1190,7 +1226,7 @@ return view.extend({
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
E('div', {}, [
|
E('div', {}, [
|
||||||
E('label', {}, _('Actions') + ':'), ' ',
|
E('label', {'id': 'action-label'}, _('Actions') + ':'), ' ',
|
||||||
E('span', { 'class': 'control-group' }, [
|
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-positive', 'data-command': 'update', 'click': handlePkg, 'disabled': isReadonlyView }, [ _('Update lists…') ]), ' ',
|
||||||
E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload, 'disabled': isReadonlyView }, [ _('Upload Package…') ]), ' ',
|
E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload, 'disabled': isReadonlyView }, [ _('Upload Package…') ]), ' ',
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ fi
|
|||||||
case "$action" in
|
case "$action" in
|
||||||
list-installed)
|
list-installed)
|
||||||
if [ $ipkg_bin = "apk" ]; then
|
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
|
else
|
||||||
cat /usr/lib/opkg/status
|
cat /usr/lib/opkg/status
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
list-available)
|
list-available)
|
||||||
if [ $ipkg_bin = "apk" ]; then
|
if [ $ipkg_bin = "apk" ]; then
|
||||||
$ipkg_bin list --full 2>/dev/null
|
$ipkg_bin query --fields all --format json --available \* 2>/dev/null
|
||||||
else
|
else
|
||||||
lists_dir=$(sed -rne 's#^lists_dir \S+ (\S+)#\1#p' /etc/opkg.conf /etc/opkg/*.conf 2>/dev/null | tail -n 1)
|
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
|
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"
|
cmd="$ipkg_bin"
|
||||||
|
|
||||||
# APK have command renamed
|
# The apk package manager uses distinct commands for installing and removing packages.
|
||||||
if [ $ipkg_bin = "apk" ]; then
|
if [ $ipkg_bin = "apk" ]; then
|
||||||
case "$action" in
|
case "$action" in
|
||||||
install)
|
install)
|
||||||
action="add"
|
action="add"
|
||||||
;;
|
;;
|
||||||
update)
|
|
||||||
action="update"
|
|
||||||
;;
|
|
||||||
upgrade)
|
|
||||||
action="upgrade"
|
|
||||||
;;
|
|
||||||
remove)
|
remove)
|
||||||
action="del"
|
action="del"
|
||||||
;;
|
;;
|
||||||
@@ -54,6 +48,11 @@ case "$action" in
|
|||||||
if [ $ipkg_bin = "apk" ]; then
|
if [ $ipkg_bin = "apk" ]; then
|
||||||
while [ -n "$1" ]; do
|
while [ -n "$1" ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
--autoremove)
|
||||||
|
# Just shift and do nothing.
|
||||||
|
# 'apk del' already cleans up orphaned dependencies by default.
|
||||||
|
shift
|
||||||
|
;;
|
||||||
--force-removal-of-dependent-packages)
|
--force-removal-of-dependent-packages)
|
||||||
cmd="$cmd -r"
|
cmd="$cmd -r"
|
||||||
shift
|
shift
|
||||||
|
|||||||
Reference in New Issue
Block a user