en debian

Empaquetar Stremio para Debian: un viaje hacia la integración del 100% de las librerías del sistema

Stremio para Debian: un viaje hacia la integración del 100% de las librerías del sistema

Cómo reemplacé todas las dependencias incluidas en una aplicación Qt5 compleja, y lo que aprendí sobre el desarrollo de parches, los bugs de hilos y el arte de depurar fallos en tiempo de ejecución

Por Juan Manuel Méndez Rey

Última actualización: October 3, 2025

Etiquetas: #debian #packaging #qt5 #technical-writing #system-libraries #debugging #free-software


TL;DR

Empaqueté Stremio para Debian reemplazando el 100% de sus dependencias incluidas (libmpv, librerías de Qt, OpenSSL) con librerías del sistema. Por el camino depuré cinco problemas críticos: el orden de inicialización de QtWebEngine, conflictos de hilos con SingleApplication, módulos QML que faltaban, variables de entorno de Node.js en QProcess y las trampas de los ficheros install de debhelper. ¿La lección de verdad? Repetí la creación de parches más de 5 veces porque probaba contra fuentes ya modificadas en lugar de contra el fuente original limpio. Este artículo comparte tanto las soluciones técnicas como la meta-lección sobre un flujo de trabajo eficiente para el desarrollo de parches, que podría haberme ahorrado el 70% del tiempo de desarrollo.

Conclusión clave: al empaquetar aplicaciones complejas, prueba tus parches contra el fuente original prístino en cada paso, no al final.


Estado del paquete (October 2025)

Este artículo documenta el trabajo técnico detrás del empaquetado de Stremio para Debian. El paquete ha logrado la integración del 100% de las librerías del sistema y actualmente está:

  • Trabajo técnico: completo y validado
  • ITP enviado: en revisión por el patrocinador Debian Developer
  • Estado: a la espera de la aprobación de la subida
  • Repositorio: salsa.debian.org/mendezr/stremio

Esto es un análisis técnico en profundidad de los retos y las soluciones, no un anuncio de la disponibilidad del paquete. El trabajo continúa a través del proceso de revisión de Debian.


Introducción

Cuando me propuse empaquetar Stremio, una aplicación de media center muy popular, para Debian, tenía un objetivo claro: lograr la integración del 100% de las librerías del sistema. Sin dependencias incluidas, sin submódulos de git, solo una integración limpia con el ecosistema de Debian. Lo que parecía una migración sencilla del sistema de compilación se convirtió en una inmersión profunda en los modelos de hilos de Qt5, el orden de inicialización en tiempo de ejecución y el arte sutil de crear parches mínimos y mantenibles.

Esta es la historia de ese viaje, los retos técnicos a los que me enfrenté y, quizá lo más importante, las lecciones que aprendí sobre el desarrollo eficiente de parches, que podrían haberme ahorrado días de trabajo repetido.

El reto: librerías del sistema o nada

El repositorio del fuente original de Stremio llegaba con varias dependencias incluidas como submódulos de git:

  • libmpv para la reproducción de vídeo
  • qthelper para utilidades de Qt
  • singleapplication para el comportamiento de instancia única
  • Librerías OpenSSL

El camino de Debian es claro: usar las librerías que proporciona el sistema. Esto no es solo pureza filosófica, se trata de actualizaciones de seguridad, gestión de dependencias e integración con el ecosistema más amplio.

El objetivo: reemplazar cada dependencia incluida por su equivalente en librería del sistema de Debian.

El resultado: un paquete .deb que funciona, con un binario optimizado de 293KB que usa el 100% de librerías del sistema.

El viaje: cinco grandes obstáculos técnicos, cada uno revelando conocimientos más profundos sobre la arquitectura de aplicaciones Qt5.

La primera victoria (que no lo fue)

El empaquetado inicial parecía sencillo. Modifiqué CMakeLists.txt para usar librerías del sistema:

find_package(PkgConfig REQUIRED)
pkg_check_modules(MPV REQUIRED mpv)
find_package(Qt5 REQUIRED COMPONENTS Core Gui Qml Quick WebEngine)
find_package(OpenSSL REQUIRED)

La compilación tuvo éxito. Las dependencias parecían perfectas:

$ ldd build/stremio | head -5
    libQt5WebEngine.so.5 => /lib/x86_64-linux-gnu/libQt5WebEngine.so.5
    libQt5DBus.so.5 => /lib/x86_64-linux-gnu/libQt5DBus.so.5
    libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3
    libmpv.so.2 => /lib/x86_64-linux-gnu/libmpv.so.2

¡Perfecto! A publicar, ¿no?

Mal. Cuando de verdad ejecuté la aplicación:

Segmentation fault (core dumped)

Así empezó el trabajo de verdad.

Reto 1: el bug de inicialización de QtWebEngine

Síntoma: fallo de segmentación inmediato al lanzar la aplicación.

Primer intento de depuración: ejecutar con gdb y examinar la traza de la pila:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff5a2b3c4 in QQmlApplicationEngine::QQmlApplicationEngine() ()

El fallo ocurría durante la construcción de QQmlApplicationEngine. ¿Pero por qué? El mismo código funcionaba bien con las librerías incluidas.

La investigación: después de examinar la documentación de Qt5 WebEngine y de varios intentos fallidos de reorganizar el código, descubrí un requisito crítico de inicialización enterrado en la documentación de QtWebEngine:

QtWebEngine::initialize() debe llamarse antes del constructor de QApplication cuando se usa QML.

La configuración con las librerías incluidas satisfacía este orden por casualidad. Con librerías del sistema, la función main() por defecto lo incumplía:

// WRONG - causes crashes
int main(int argc, char *argv[]) {
    QApplication app(argc, argv);  // QApplication created first
    // QtWebEngine::initialize() never called!
    QQmlApplicationEngine engine;  // CRASH
}

La corrección (parche 0007-add-qtwebengine-initialize-fix.patch):

// CORRECT - initialize QtWebEngine before QApplication
int main(int argc, char *argv[]) {
    QtWebEngine::initialize();     // CRITICAL: Must be first!
    QApplication app(argc, argv);
    QQmlApplicationEngine engine;  // Now works
}

Lección: al reemplazar librerías incluidas por las del sistema, las suposiciones sobre el orden de inicialización pueden cambiar. Verifica siempre los requisitos de la secuencia de arranque.

Reto 2: la pesadilla de hilos de SingleApplication

Síntoma: después de arreglar la inicialización de QtWebEngine, la aplicación arrancaba pero fallaba inmediatamente con:

QObject: Cannot create children for a parent that is in a different thread.

El culpable: la librería del sistema libsingleapplication-dev versión 3.3.4.

Stremio necesita comportamiento de instancia única: cuando lo lanzas por segunda vez, debería activar la ventana existente en lugar de iniciar un proceso nuevo. El código del fuente original usaba una librería singleapplication incluida. El sistema Debian proporciona libsingleapplication-dev. Reemplazo perfecto, ¿no?

Mal otra vez.

La investigación: la librería SingleApplication del sistema monta un contexto de hilos que entra en conflicto con QQmlApplicationEngine. En concreto:

  1. El SingleApplication del sistema crea su mecanismo de IPC en un hilo de trabajo
  2. QQmlApplicationEngine espera crearse en el hilo principal
  3. El modelo de hilos de Qt5 no permite relaciones padre-hijo entre hilos para ciertos objetos QML

La versión incluida usaba un enfoque de hilos distinto que resultaba funcionar con QML.

Los comienzos en falso: probé:

  • Parchear SingleApplication para usar el hilo principal (rompía el IPC)
  • Retrasar la creación del motor QML (rompía la secuencia de arranque)
  • Varios apaños de afinidad de hilos de Qt (rompían otras cosas)

La solución: escribir una clase CompatibleSingleApp propia que proporcione una funcionalidad idéntica sin conflictos de hilos:

// compatible_singleapp.h
class CompatibleSingleApp : public QApplication {
    Q_OBJECT
public:
    CompatibleSingleApp(int &argc, char **argv);
    bool isPrimary() const;
    bool isSecondary() const;

signals:
    void instanceStarted();

private:
    QLocalServer *m_server;
    QString m_serverName;
    bool m_isPrimary;
};

Puntos destacados de la implementación (parche 0008-add-compatible-singleapp-implementation.patch):

CompatibleSingleApp::CompatibleSingleApp(int &argc, char **argv)
    : QApplication(argc, argv), m_isPrimary(false) {

    m_serverName = "stremio-" + QString(qgetenv("USER"));

    // Try to connect to existing instance
    QLocalSocket socket;
    socket.connectToServer(m_serverName);

    if (socket.waitForConnected(500)) {
        // Secondary instance - notify primary and exit
        m_isPrimary = false;
        socket.write("ACTIVATE");
        socket.waitForBytesWritten();
        return;
    }

    // Primary instance - create server
    m_isPrimary = true;
    m_server = new QLocalServer(this);
    m_server->listen(m_serverName);

    connect(m_server, &QLocalServer::newConnection, this, [this]() {
        QLocalSocket *client = m_server->nextPendingConnection();
        connect(client, &QLocalSocket::readyRead, [this, client]() {
            QByteArray data = client->readAll();
            if (data == "ACTIVATE") {
                emit instanceStarted();
            }
            client->deleteLater();
        });
    });
}

Resultado: comportamiento de instancia única perfecto usando QApplication puro (sin conflictos de hilos) con QLocalSocket/QLocalServer para el IPC.

Tamaño del binario: 424KB en debug frente a 293KB en release, ambos usando el 100% de librerías del sistema.

Lección clave: las librerías del sistema pueden tener detalles de implementación distintos (como los modelos de hilos) aunque proporcionen la misma API. A veces una implementación mínima propia es más limpia que parchear alrededor de las incompatibilidades.

Reto 3: los módulos QML que faltaban

Síntoma: después de arreglar tanto los problemas de inicialización como los de hilos, la aplicación arrancaba pero mostraba una pantalla negra con errores en la consola:

module "QtWebEngine" is not installed
module "QtWebChannel" is not installed
module "Qt.labs.platform" is not installed

El problema: los módulos QML de Qt5 son paquetes separados de tiempo de ejecución en Debian, y qtdeclarative5-dev no los arrastra automáticamente.

La investigación: el código QML de Stremio importa numerosos módulos de Qt:

import QtQuick 2.12
import QtQuick.Controls 2.12
import QtWebEngine 1.10
import QtWebChannel 1.0
import Qt.labs.platform 1.1
import Qt.labs.settings 1.0

Cada uno requiere un paquete de Debian separado.

La solución: un mapeo exhaustivo de dependencias en debian/control:

Depends: ${shlibs:Depends}, ${misc:Depends},
         qml-module-qtwebengine,
         qml-module-qtwebchannel,
         qml-module-qt-labs-platform,
         qml-module-qtquick-controls,
         qml-module-qtquick-dialogs,
         qml-module-qt-labs-settings,
         qml-module-qt-labs-folderlistmodel,
         qtbase5-dev-tools

Lección: al empaquetar aplicaciones Qt QML, sigue cada sentencia import de QML hasta su paquete de Debian correspondiente. apt-file search es tu amigo:

$ apt-file search QtWebEngine.qml
qml-module-qtwebengine: /usr/lib/x86_64-linux-gnu/qt5/qml/QtWebEngine/...

Reto 4: el misterio del servidor de streaming

Síntoma: la GUI carga perfectamente, pero al intentar reproducir contenido:

Error while starting streaming server
tcp: Connection to tcp://127.0.0.1:11470 failed: Connection refused

La investigación: Stremio incluye un componente de servidor Node.js (server.js) para el streaming. El registro del proceso del shell mostraba:

TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
    at Object.join (node:path:1292:7)

La causa raíz: el QProcess de Qt no hereda las variables de entorno por defecto. El servidor Node.js esperaba que HOME, USER y PWD estuvieran disponibles, pero no lo estaban.

La corrección (parche 0011-fix-qprocess-environment-for-server-launch.patch):

// stremioprocess.cpp
void Process::start() {
    // Set up environment variables for Node.js server
    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();

    if (!env.contains("HOME")) {
        env.insert("HOME",
            QStandardPaths::writableLocation(QStandardPaths::HomeLocation));
    }
    if (!env.contains("USER")) {
        env.insert("USER", qgetenv("USER"));
    }
    if (!env.contains("PWD")) {
        env.insert("PWD", QDir::currentPath());
    }

    this->setProcessEnvironment(env);
    QProcess::start();
}

Resultado: el servidor arranca con éxito:

hls executables located -> { ffmpeg: '/usr/bin/ffmpeg', ffsplit: null }
Using app path -> /home/user/.stremio-server
EngineFS server started at http://127.0.0.1:11470

Lección: al lanzar procesos desde aplicaciones Qt, configura explícitamente el entorno. No des por hecho que los procesos hijos heredan las variables de entorno del padre.

Reto 5: las trampas de la estructura de empaquetado de Debian

Síntoma: el paquete se compila con éxito, pero los ficheros se instalan en lugares equivocados o con nombres equivocados.

El problema: entender mal el comportamiento de los ficheros .install de debhelper.

Lo que yo pensaba:

# debian/stremio.install
build/stremio usr/bin/stremio-bin    # Install as /usr/bin/stremio-bin

Lo que pasó de verdad:

/usr/bin/stremio-bin/stremio    # Created DIRECTORY, file inside!

La revelación: en los ficheros .install de debhelper:

  • Ruta que termina en / → instala los ficheros DENTRO de ese directorio usando los nombres originales
  • Ruta SIN / → crea un directorio con ese nombre e instala los ficheros dentro

La solución correcta (implementación real):

# debian/stremio.install
# Binary installed to /usr/libexec (FHS 3.0 compliance for helper executables)
build/stremio usr/libexec/stremio/

# Wrapper script becomes the primary user-facing command
debian/stremio-wrapper usr/bin/

# Desktop file for application menu integration
debian/stremio.desktop usr/share/applications/

# Application icons (multiple resolutions for different contexts)
icons/smartcode-stremio_16.png usr/share/icons/hicolor/16x16/apps/
icons/smartcode-stremio_64.png usr/share/icons/hicolor/64x64/apps/
# ... (additional icon sizes)

¿Por qué esta estructura?

  1. /usr/libexec/stremio/: ubicación moderna del FHS 3.0 para ejecutables internos que los usuarios no deben llamar directamente
  2. Script envolvente en /usr/bin/stremio: establece variables de entorno (como QTWEBENGINE_DISABLE_SANDBOX=1) antes de lanzar el binario real
  3. Barras finales: instalan los ficheros DENTRO de los directorios usando los nombres de fichero originales, algo crítico para colocarlos bien

Lección: lee con atención la documentación de debhelper. Los pequeños detalles de sintaxis (¡las barras finales!) tienen consecuencias grandes. El empaquetado moderno de Debian también sigue los estándares del FHS 3.0 y coloca los binarios auxiliares en /usr/libexec/ en lugar de /usr/bin/.

La meta-lección: desarrollo eficiente de parches

Los retos técnicos eran difíciles, pero los hice más duros con un flujo de trabajo ineficiente. Creaba parches, los probaba, descubría que fallaban sobre el fuente original limpio y los rehacía: cinco veces.

El problema: probaba los parches contra fuentes ya modificadas, no contra el fuente original prístino.

Estrategia del sistema de compilación: parchea CMakeLists.txt primero

Principio crítico: prioriza siempre los parches del sistema de compilación sobre las modificaciones del código fuente.

Al reemplazar dependencias incluidas por librerías del sistema, los primeros parches deberían dirigirse a CMakeLists.txt:

# Patch 0005-cmake-system-libraries-v4.4.169.patch
find_package(PkgConfig REQUIRED)
pkg_check_modules(MPV REQUIRED mpv)
find_package(Qt5 REQUIRED COMPONENTS Core Gui Qml Quick WebEngine DBus)
find_package(OpenSSL REQUIRED)

Por qué importa: los parches más pequeños y centrados, que tratan la integración del sistema de compilación por separado de los cambios en el código fuente, son más fáciles de mantener y de revisar.

Preferencia de sistema de compilación: usamos primero qmake para generar los makefiles (el sistema de compilación tradicional de Stremio) y después aseguramos la compatibilidad con CMake. El fichero stremio.pro y el flujo de release.makefile tenían prioridad para las compilaciones del paquete.

El antipatrón

  1. Modificar los ficheros fuente directamente para arreglar el problema
  2. Generar parches desde el estado modificado
  3. Intentar aplicar los parches al fuente original limpio
  4. Los parches fallan (falta contexto, números de línea equivocados, faltan dependencias)
  5. Repetir

El flujo de trabajo eficiente que debería haber usado

# 1. Start with clean upstream
git checkout v4.4.169

# 2. Create isolated test environment
cp -r . /tmp/patch-test/
cd /tmp/patch-test/

# 3. Fix ONE issue, test, generate patch
# (fix QtWebEngine initialization)
mkdir build && cd build && cmake .. && make    # Test build
cd .. && git diff > 0001-qtwebengine-init.patch

# 4. Apply patch to clean upstream, fix next issue
git checkout v4.4.169
patch -p1 < 0001-qtwebengine-init.patch
# (fix next issue)
git diff > 0002-next-fix.patch

# 5. Final validation: apply all patches to clean upstream
git checkout v4.4.169
for patch in *.patch; do
    patch -p1 < $patch || exit 1
done
mkdir build && cd build && cmake .. && make

Lista de comprobación del análisis de dependencias que ojalá hubiera usado desde el principio:

## Pre-Patch Analysis Template

### Files to Modify:
- [ ] main.cpp - entry point changes
- [ ] mainapplication.h - class definitions, includes
- [ ] CMakeLists.txt - build system
- [ ] compatible_singleapp.h/cpp - new files

### Dependency Chain:
1. main.cpp includes → mainapplication.h
2. mainapplication.h includes → singleapplication.h (to replace)
3. CMakeLists.txt references → SingleApplication library
4. Qt MOC will process → Q_OBJECT classes (check conflicts!)

### Build Test Plan:
- [ ] Clean cmake build
- [ ] ldd dependency verification
- [ ] Runtime basic functionality

Tiempo ahorrado si lo hubiera hecho desde el principio: una reducción de ~70% del tiempo de desarrollo de parches.

Idea clave: entiende las dependencias entre ficheros y el sistema de compilación ANTES de hacer cambios. Prueba los parches contra el fuente original limpio en cada paso, no solo al final.

El conjunto completo de parches

La solución final que funciona consta de 11 parches:

  1. 0001-Fix-server.js-path-for-FHS-compliance.patch – Ubicación del servidor
  2. 0002-disable-server-download.patch – Usar el Node.js del sistema
  3. 0004-minimal-qthelper-integration.patch – Utilidades de Qt del sistema
  4. 0005-cmake-system-libraries-v4.4.169.patch – Integración de MPV y OpenSSL
  5. 0007-add-qtwebengine-initialize-fix.patch – Crítico: inicialización de QtWebEngine
  6. 0008-add-compatible-singleapp-implementation.patch – Crítico: instancia única propia
  7. 0009-remove-system-singleapplication-add-compatible.patch – Integración de la compilación
  8. 0010-fix-qmake-install-paths.patch – Correcciones de la ubicación de instalación
  9. 0011-fix-qprocess-environment-for-server-launch.patch – Crítico: entorno del servidor

Flujo de trabajo de validación

El flujo de validación final asegura que los parches funcionan sobre el fuente original limpio, usando el flujo de importación de GBP (git-buildpackage) para construir correctamente el paquete de Debian:

# Step 1: Create pristine test environment with GBP structure
git clone --branch v4.4.169 https://github.com/Stremio/stremio-shell.git /tmp/validation
cd /tmp/validation
cp -r /path/to/debian .

# Step 2: Apply all patches using quilt
export QUILT_PATCHES=debian/patches
quilt push -a

# Step 3: Test local build first (fastest iteration)
QT_DEFAULT_MAJOR_VERSION=5 dpkg-buildpackage -us -uc

# Step 4: Verify dependencies
ldd debian/stremio/usr/libexec/stremio/stremio | head -5
# Should show: libQt5WebEngine.so.5, libcrypto.so.3, libmpv.so.2

# Step 5: Test with pbuilder (clean chroot environment)
sudo pbuilder update
sudo pbuilder build ../*.dsc

# Step 6: Test with sbuild (production-grade build)
# WARNING: Qt5/WebEngine packages consume significant space
# Typical requirement: 4-6GB build space (overlayfs in tmpfs)
# Solution: Use machine with 16GB+ RAM or configure sbuild on disk

sbuild -d unstable ../*.dsc
# If sbuild fails with "No space left on device":
# - Switch to larger machine (16GB+ RAM recommended)
# - Or configure sbuild to use disk instead of tmpfs

Consideraciones del entorno de compilación

Requisitos de memoria para aplicaciones Qt5:

  • dpkg-buildpackage: ~2GB RAM
  • pbuilder: ~4GB RAM
  • sbuild con overlayfs en tmpfs: 6-8GB RAM (Qt5WebEngine consume mucha memoria)

Nuestra solución: después de agotar el espacio en máquinas de 8GB durante sbuild, migramos a una máquina de 32GB. Esto es típico de las aplicaciones Qt5/WebEngine: prueba siempre la capacidad de sbuild antes de comprometerte con una infraestructura de compilación.

Resultado: binario optimizado de 293KB, 100% librerías del sistema, funcionalidad completa incluido el streaming.

Lecciones para otros empaquetadores

Conclusiones técnicas

  1. El orden de inicialización importa: las librerías del sistema pueden tener requisitos de arranque distintos a los de las incluidas. Verifica siempre las secuencias de inicialización.
  2. Los modelos de hilos varían: incluso librerías con APIs idénticas pueden usar enfoques de hilos distintos. Vigila los errores de creación de objetos entre hilos.
  3. Las variables de entorno no son automáticas: QProcess y mecanismos parecidos necesitan una configuración explícita del entorno.
  4. Los módulos QML son paquetes separados: sigue cada import de QML hasta su dependencia de paquete de Debian.
  5. Las implementaciones propias ganan a los parches complejos: a veces escribir 100 líneas de código limpio es mejor que un parche de 500 líneas a una librería incompatible.

Conclusiones sobre el proceso

  1. Prueba siempre los parches contra el fuente original limpio: nunca generes parches desde fuentes ya modificadas.
  2. Mapea las dependencias antes de programar: entiende las relaciones entre ficheros y el sistema de compilación antes de hacer cambios.
  3. Una corrección, un parche, una prueba: el desarrollo incremental evita fallos en cascada.
  4. Documenta las suposiciones: lo que funciona «por accidente» con las librerías incluidas puede fallar con las del sistema.
  5. Valida por completo: prueba los parches en entornos aislados antes de declararlos «listos».

Conclusión

Empaquetar Stremio para Debian me enseñó mucho más que los internos de Qt5 y la integración del sistema de compilación. Reveló lo fácil que es caer en flujos de trabajo ineficientes cuando no damos un paso atrás para examinar nuestro proceso.

El logro técnico: un paquete de Debian totalmente funcional que usa el 100% de librerías del sistema allí donde el fuente original usaba dependencias incluidas: binario de 293KB, cero submódulos y paridad completa de características.

El logro de verdad: aprender que el cómo de resolver problemas importa tanto como el qué. El desarrollo eficiente de parches no es solo cuestión de habilidad técnica, sino de un flujo de trabajo disciplinado, pensamiento sistemático y una autoevaluación honesta.

¿Haría algo distinto? Por supuesto. Usaría el flujo de validación desde el primer día, mapearía las dependencias antes de programar y probaría cada parche contra el fuente original limpio de inmediato.

¿Pero habría aprendido estas lecciones sin cometer los errores? Probablemente no.

Agradecimientos

Gracias al equipo de Stremio por crear un software magnífico, a la comunidad Debian por mantener unos estándares altos, a mi amigo Arturo (un Debian Developer) que, conociendo mi pasión por Debian, me animó a empezar a trabajar como Debian Maintainer, y a cada empaquetador que ha documentado sus luchas: vuestras historias de guerra hacen las nuestras más fáciles.


Estado del proyecto (a fecha de October 3, 2025)

Nota: este artículo documenta el proceso técnico y los retos. La aceptación del paquete está pendiente de la revisión de Debian. Se publicarán actualizaciones de estado conforme continúe el proceso de revisión.


Este artículo forma parte de mi camino hacia convertirme en Debian Developer. Si te interesa el empaquetado en Debian o tienes preguntas sobre los detalles técnicos, escríbeme sin problema.

Comentar en Mastodon

Etiquetas

  • #debian
  • #gnu
  • #linux
  • #packaging
  • #stremio