66#include < QFileInfo>
77#include < QSettings>
88#include < QDir>
9- #include < QThread>
109#include < QUrl>
10+ #include < QThread>
1111#include < algorithm>
1212#include < climits>
1313
@@ -205,7 +205,7 @@ bool UpdateManager::applyUpdate() {
205205 Logger::instance ().info (" === Starting System Update Process ===" );
206206
207207 // Acquire lock file to prevent concurrent update runs.
208- // This matches the original nuts shell script behaviour.
208+ // This matches the original nuts behaviour.
209209 const QString lockPath = " /var/run/nuts-cpp.lock" ;
210210 QFile lockFile (lockPath);
211211 if (lockFile.exists ()) {
@@ -314,7 +314,7 @@ bool UpdateManager::applyUpdate() {
314314 Logger::instance ().success (" Cleanup crew finished." );
315315
316316 // Create nx-pkgmgr-policy symlinks.
317- Logger::instance ().info (" --- Step: Installing package manager policy symlinks ---" );
317+ Logger::instance ().info (" --- Step: Installing policy symlinks ---" );
318318 {
319319 QString output, error;
320320 QStringList aptTools = {" apt" , " apt-cache" , " apt-cdrom" , " apt-config" , " apt-get" , " apt-mark" };
@@ -563,7 +563,7 @@ bool UpdateManager::prepareUpdateTools() {
563563 }
564564
565565 m_pkgManagerPath = appRunPath;
566- Logger::instance ().info (" Using package manager : " + m_pkgManagerPath);
566+ Logger::instance ().info (" Using dpkg tooling at : " + m_pkgManagerPath);
567567
568568 // dpkg → AppRun (matching original: ln -svf "$AIPKG_MANAGER" /usr/bin/dpkg)
569569 bool lnOk = m_sysInterface->executeInOverlay ({" /usr/bin/ln" , " -svf" , appRunPath, " /usr/bin/dpkg" }, output, error);
@@ -664,7 +664,7 @@ bool UpdateManager::performPackageUpdates() {
664664 QString nvidiaDir = otaDir + " /nvidia" ;
665665
666666 Logger::instance ().info (" OTA updates dir: " + updatesDir);
667- Logger::instance ().info (" dpkg manager path: " + m_pkgManagerPath);
667+ Logger::instance ().info (" dpkg tooling path: " + m_pkgManagerPath);
668668
669669 // Detect NVIDIA on the host via /proc (shared with chroot).
670670 bool isNvidia = QDir (" /proc/driver/nvidia" ).exists ();
@@ -675,94 +675,105 @@ bool UpdateManager::performPackageUpdates() {
675675 }
676676 Logger::instance ().info (QString (" NVIDIA hardware: %1" ).arg (isNvidia ? " yes" : " no" ));
677677
678- // Verify the updates directory exists and list its contents
679- {
680- QString output, error;
681- bool dirExists = m_sysInterface->executeInOverlay (
682- {" /usr/bin/test" , " -d" , updatesDir}, output, error);
683- Logger::instance ().info (QString (" Updates dir exists: %1" ).arg (dirExists ? " yes" : " NO - this is a problem" ));
684- if (dirExists) {
685- m_sysInterface->executeInOverlay ({" /usr/bin/find" , updatesDir, " -name" , " *.deb" }, output, error);
686- Logger::instance ().debug (" Debs found:\n " + output.trimmed ());
687- if (output.trimmed ().isEmpty ())
688- Logger::instance ().warning (" No .deb files found in " + updatesDir);
689- }
678+ QString output, error;
679+
680+ // Verify the dpkg tooling is present inside the chroot before attempting anything.
681+ if (!m_sysInterface->executeInOverlay ({" /usr/bin/test" , " -f" , m_pkgManagerPath}, output, error)) {
682+ Logger::instance ().error (" dpkg tooling not found inside chroot: " + m_pkgManagerPath);
683+ return false ;
690684 }
685+ Logger::instance ().info (" dpkg tooling confirmed present: " + m_pkgManagerPath);
691686
692- // Phase 1: Unpack.
693- Logger::instance ().info (" Phase 1: Unpacking OTA content..." );
694- {
695- QString searchPaths = updatesDir;
696- if (isNvidia)
697- searchPaths += " " + nvidiaDir;
687+ // --- Phase 1: Collect .deb paths ---
688+ Logger::instance ().info (" --- Phase 1: Collecting .deb packages ---" );
698689
699- QString unpackCmd = QString (
700- " export DEBIAN_FRONTEND=noninteractive && "
701- " export TMPDIR=/tmp && "
702- " find %1 -name '*.deb' -print0 | "
703- " xargs -0 -n 120 %2 --force-all --unpack"
704- ).arg (searchPaths, m_pkgManagerPath);
690+ QStringList findArgs = {" /usr/bin/find" , updatesDir, " -name" , " *.deb" , " -print0" };
691+ if (isNvidia)
692+ findArgs << nvidiaDir;
705693
706- Logger::instance ().debug (" Unpack command: " + unpackCmd);
694+ bool findOk = m_sysInterface->executeInOverlay (findArgs, output, error);
695+ if (!findOk && output.trimmed ().isEmpty ()) {
696+ Logger::instance ().error (" find failed to enumerate .deb packages" );
697+ if (!error.trimmed ().isEmpty ()) Logger::instance ().error (" find stderr: " + error.trimmed ());
698+ return false ;
699+ }
707700
708- QString output, error;
709- bool ok = m_sysInterface->executeInOverlay ({" /bin/sh" , " -c" , unpackCmd}, output, error);
710- if (!output.trimmed ().isEmpty ()) Logger::instance ().debug (" unpack stdout: " + output.trimmed ());
711- if (!error.trimmed ().isEmpty ()) Logger::instance ().debug (" unpack stderr: " + error.trimmed ());
712- if (!ok) {
713- Logger::instance ().error (" Failed to unpack OTA content (exit non-zero)" );
701+ // Split null-delimited output into individual paths.
702+ QStringList debs = output.split (' \0 ' , Qt::SkipEmptyParts);
703+ Logger::instance ().info (QString (" Found %1 .deb package(s)." ).arg (debs.size ()));
704+ for (const QString& deb : debs)
705+ Logger::instance ().debug (" deb: " + deb);
706+
707+ if (debs.isEmpty ()) {
708+ Logger::instance ().warning (" No .deb packages found. Nothing to install." );
709+ return true ;
710+ }
711+
712+ // --- Phase 2: Unpack in batches of 120 ---
713+ Logger::instance ().info (" --- Phase 2: Unpacking packages ---" );
714+
715+ const int batchSize = 120 ;
716+ int totalBatches = (debs.size () + batchSize - 1 ) / batchSize;
717+
718+ for (int i = 0 ; i < debs.size (); i += batchSize) {
719+ QStringList batch = debs.mid (i, batchSize);
720+ int batchNum = (i / batchSize) + 1 ;
721+ Logger::instance ().info (QString (" Unpacking batch %1/%2 (%3 package(s))..." )
722+ .arg (batchNum).arg (totalBatches).arg (batch.size ()));
723+
724+ QStringList unpackArgs = {m_pkgManagerPath, " --force-all" , " --unpack" };
725+ unpackArgs << batch;
726+
727+ if (!m_sysInterface->executeInOverlay (unpackArgs, output, error)) {
728+ Logger::instance ().error (QString (" Unpack failed on batch %1/%2" ).arg (batchNum).arg (totalBatches));
729+ if (!error.trimmed ().isEmpty ()) Logger::instance ().error (" unpack stderr: " + error.trimmed ());
714730 return false ;
715731 }
716- Logger::instance ().success (" Phase 1 unpack complete. " );
732+ Logger::instance ().success (QString ( " Batch %1/%2 unpacked. " ). arg (batchNum). arg (totalBatches) );
717733 }
718734
719- // Phase 2: Configure loop until audit reports clean.
720- Logger::instance ().info (" Phase 2: Configuring packages..." );
721- int maxPasses = 15 ;
722- int pass = 1 ;
735+ Logger::instance ().success (" All packages unpacked." );
736+
737+ // --- Phase 3: Configure + audit loop ---
738+ Logger::instance ().info (" --- Phase 3: Configuring packages ---" );
739+
740+ const int maxPasses = 15 ;
723741 QString lastAudit;
724742
725- while ( pass <= maxPasses) {
726- Logger::instance ().info (QString (" Configuration pass %1/%2" ).arg (pass).arg (maxPasses));
743+ for ( int pass = 1 ; pass <= maxPasses; ++pass ) {
744+ Logger::instance ().info (QString (" Configuration pass %1/%2... " ).arg (pass).arg (maxPasses));
727745
728- QString output, error;
729- QStringList confArgs;
730- confArgs << " /bin/sh" << " -c"
731- << QString (" export DEBIAN_FRONTEND=noninteractive && export TMPDIR=/tmp && "
732- " %1 --force-all --configure -a" ).arg (m_pkgManagerPath);
733- bool confOk = m_sysInterface->executeInOverlay (confArgs, output, error);
734- if (!output.trimmed ().isEmpty ()) Logger::instance ().debug (" configure stdout: " + output.trimmed ());
735- if (!error.trimmed ().isEmpty ()) Logger::instance ().debug (" configure stderr: " + error.trimmed ());
736- Logger::instance ().debug (QString (" configure exit: %1" ).arg (confOk ? " 0" : " non-zero (expected, --force-all)" ));
737-
738- QStringList auditArgs;
739- auditArgs << m_pkgManagerPath << " --audit" ;
740- m_sysInterface->executeInOverlay (auditArgs, output, error);
741- QString currentAudit = output.trimmed ();
746+ // --configure -a: configure all unpacked packages; ignore non-zero exit (may be partial)
747+ m_sysInterface->executeInOverlay ({m_pkgManagerPath, " --force-all" , " --configure" , " -a" }, output, error);
742748
743- if (!currentAudit.isEmpty ())
744- Logger::instance ().info (" dpkg --audit output:\n " + currentAudit);
749+ // --audit: report packages in inconsistent state
750+ m_sysInterface->executeInOverlay ({m_pkgManagerPath, " --audit" }, output, error);
751+ QString currentAudit = output.trimmed ();
745752
746753 if (currentAudit.isEmpty ()) {
747- Logger::instance ().success (" Package configuration converged after " + QString::number (pass) + " pass(es)." );
748- m_sysInterface->executeInOverlay ({" /usr/bin/rm" , " -rf" , " /tmp/pkgman-extracted" }, output, error);
749- return true ;
754+ Logger::instance ().success (QString (" Package configuration converged after %1 pass(es)." ).arg (pass));
755+ break ;
756+ }
757+
758+ Logger::instance ().info (" dpkg --audit output:\n " + currentAudit);
759+
760+ if (pass > 1 && currentAudit == lastAudit) {
761+ Logger::instance ().error (" Package configuration stuck — no progress between passes." );
762+ return false ;
750763 }
751764
752- if (currentAudit == lastAudit && pass > 1 ) {
753- Logger::instance ().error (" Package configuration stuck — no progress between passes. Aborting." );
754- Logger::instance ().error (" Final dpkg --audit:\n " + currentAudit);
765+ if (pass == maxPasses) {
766+ Logger::instance ().error (QString (" Package configuration failed to converge after %1 passes." ).arg (maxPasses));
755767 return false ;
756768 }
757769
758770 lastAudit = currentAudit;
759- pass++;
760771 QThread::sleep (1 );
761772 }
762773
763- Logger::instance (). error ( QString ( " Package configuration failed to converge after %1 passes. " ). arg (maxPasses) );
764- Logger::instance ().error ( " Final dpkg --audit: \n " + lastAudit );
765- return false ;
774+ m_sysInterface-> executeInOverlay ({ " /usr/bin/rm " , " -rf " , " /tmp/pkgman-extracted " }, output, error );
775+ Logger::instance ().success ( " Package updates applied successfully. " );
776+ return true ;
766777}
767778
768779bool UpdateManager::runCleanupCrew () {
@@ -813,7 +824,7 @@ bool UpdateManager::runCleanupCrew() {
813824 if (!output.trimmed ().isEmpty ()) Logger::instance ().info (" CCU stdout: " + output.trimmed ());
814825 if (!error.trimmed ().isEmpty ()) Logger::instance ().info (" CCU stderr: " + error.trimmed ());
815826 if (!ok) {
816- Logger::instance ().error (" Cleanup crew script exited with non-zero status" );
827+ Logger::instance ().error (" Cleanup crew exited with non-zero status" );
817828 return false ;
818829 }
819830 Logger::instance ().success (" Cleanup crew completed successfully." );
0 commit comments