diff --git a/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js b/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js index 48476dbe167f..787598089fa1 100644 --- a/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js +++ b/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js @@ -152,7 +152,7 @@ function parseList(s, dest) switch (installed) { case 'installed': - pkg.installed = true; + pkg.status = ['installed']; break; } break; @@ -201,6 +201,48 @@ function parseList(s, dest) } } +function parseApkQueryJson(s, dest) { + // Parse the raw JSON text string into an array + const rawData = JSON.parse(s); + + // 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) { + const { name, ...attributes } = item; + + // Define the package object (merging name back in) + const pkg = { name, ...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 +291,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 +309,7 @@ function display(pattern) }, _('Upgrade…')); } else if (currentDisplayMode === 'installed') { - if (!pkg.installed) + if (!isPkgInstalled(pkg)) continue; ver = truncateVersion(pkg.version || '-'); @@ -282,14 +324,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 +462,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 }; - - val = val || ''; - ref = ref || ''; - - if (val === ref) - return 0; - - while (vi < val.length || ri < ref.length) { - let first_diff = 0; - - 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; +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]+/); - vi++; ri++; - } - - while (val.charAt(vi) === '0') - vi++; + // 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); - while (ref.charAt(ri) === '0') - ri++; + const maxLen = Math.max(v.length, r.length); - while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) { - first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri)); - vi++; ri++; - } + 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); - 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 +505,7 @@ function versionSatisfied(ver, ref, vop) return r > 0; case '=': - return r == 0; + return r === 0; } return false; @@ -496,7 +516,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 +625,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 +726,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 +748,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 +901,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 +1035,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 +1143,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 +1194,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 +1207,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 +1215,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…') ]), ' ', diff --git a/applications/luci-app-package-manager/root/usr/libexec/package-manager-call b/applications/luci-app-package-manager/root/usr/libexec/package-manager-call index 3a4175783a0b..b641483e50ee 100755 --- a/applications/luci-app-package-manager/root/usr/libexec/package-manager-call +++ b/applications/luci-app-package-manager/root/usr/libexec/package-manager-call @@ -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