wandres.dev
LA BIBLIOTECA ESTÁNDAR · pequeña a propósito

El ecosistema: LuaRocks y el criterio de dependencia

Las bibliotecas que todo el mundo acaba usando, cómo funciona LuaRocks y dónde falla, y un criterio explícito para decidir cuándo depender, cuándo copiar el fichero dentro del proyecto y cuándo escribirlo uno mismo en un lenguaje sin biblioteca grande.

⏱ 18 min

Un lenguaje con biblioteca estándar pequeña traslada al programador una decisión que otros toman por él: qué código ajeno entra en el proceso. En Lua esa decisión se toma varias veces por proyecto y casi siempre sin método, lo que explica que dos programas equivalentes puedan no compartir ni una dependencia. Sin embargo el ecosistema no es un caos: hay un conjunto reducido de bibliotecas que aparecen una y otra vez, un gestor de paquetes con un modelo peculiar y limitaciones reales, y una práctica cultural —copiar el fichero dentro del proyecto— que en Lua es respetable y en otros lenguajes sería una herejía. Esta lección cierra el nivel con lo que la biblioteca estándar no puede darte: criterio.

🎯 Al terminar esta lección sabrás
  • Identificar las bibliotecas de facto del ecosistema y el hueco concreto que llena cada una.
  • Explicar el modelo de LuaRocks, sus árboles de instalación y sus tres limitaciones estructurales.
  • Aplicar un criterio explícito para aceptar, rechazar o interiorizar una dependencia.
  • Decidir entre depender, copiar el fichero dentro del proyecto o escribir el código propio.

Las bibliotecas que todo el mundo acaba usando

El ecosistema tiene un centro sorprendentemente estable. Estas son las piezas que aparecen en la inmensa mayoría de los proyectos serios, agrupadas por el hueco que tapan.

🧩

LPeg

Análisis sintáctico mediante gramáticas de expresión de análisis, escrita por el propio autor del lenguaje. Es lo más cercano a una biblioteca oficial fuera del núcleo, y la respuesta correcta cuando los patrones se quedan cortos.

🗃️

LuaFileSystem y luaposix

La primera añade recorrido de directorios, atributos y creación de carpetas. La segunda expone POSIX entero para quien acepte perder portabilidad a cambio de poder.

🔌

LuaSocket, LuaSec, luv

Red en dos niveles: sockets bloqueantes con cifrado encima, o el enlace con la biblioteca de eventos que sostiene el modelo asíncrono moderno.

🧪

busted, luassert, luacheck, luacov

Pruebas, aserciones, análisis estático y cobertura. Nada de esto está en el lenguaje y los cuatro son estándar de hecho.

A esa lista hay que sumar los analizadores de JSON, que son el caso paradigmático de la fragmentación: uno escrito en C con prioridad en la velocidad, otro en Lua puro para máxima portabilidad, otro pensado para no perder el orden de las claves. Ninguno es el oficial, y los tres representan a un valor ausente y a una tabla vacía de maneras incompatibles entre sí. También conviene conocer Penlight, que es un intento explícito de suministrar la biblioteca estándar grande que Lua no tiene, y cuya adopción divide al ecosistema precisamente por eso.

ℹ️
Antes de instalar nada, mira qué te da el anfitrión

Lua casi nunca se ejecuta solo. Si programas dentro de Neovim tienes vim.json, vim.uv, vim.fs, vim.system y vim.inspect ya cargados, y añadir una dependencia externa para eso es puro coste. Lo mismo vale en OpenResty, en un motor de videojuego o en un dispositivo empotrado. La primera pregunta ante cualquier necesidad no es qué biblioteca instalar, sino qué ofrece ya el programa que hospeda al intérprete.

LuaRocks: el gestor y sus límites

LuaRocks es el gestor de paquetes del ecosistema. Su unidad es la especificación de paquete, un fichero que es a la vez descripción y programa Lua: declara nombre, versión, dependencias con restricciones, licencia y el procedimiento de construcción. Ese procedimiento puede ser una simple copia de ficheros, una llamada a un sistema de construcción o la compilación de un módulo en C.

Los paquetes se instalan en un árbol. Hay un árbol del sistema, uno por usuario y, lo importante para un proyecto serio, uno local en el propio directorio de trabajo.

luarocks init                 # crea un arbol local y un envoltorio del interprete
luarocks install lpeg         # instala en ese arbol, sin tocar el sistema
luarocks install busted --local
luarocks list
luarocks show lpeg
-- Las rutas de busqueda son datos ordinarios y se pueden inspeccionar
print(package.path)   -- donde se buscan los modulos en Lua
print(package.cpath)  -- donde se buscan los modulos en C

El modelo funciona, pero tiene tres limitaciones estructurales que conviene conocer antes de apoyarse en él.

La primera es la multiplicidad de versiones del lenguaje. Un paquete no se instala para Lua a secas, sino para una versión concreta, y el ecosistema real está repartido entre 5.1, 5.4 y LuaJIT, que declara compatibilidad con 5.1. Un mismo paquete puede existir, no existir o comportarse distinto según cuál uses, y los módulos escritos en C hay que compilarlos por separado para cada una.

La segunda es la compilación. En cuanto una dependencia contiene C, instalarla exige un compilador, las cabeceras de desarrollo del intérprete y, con frecuencia, las cabeceras de una biblioteca del sistema. Eso convierte una instalación trivial en una máquina de desarrollo en un problema real dentro de un contenedor mínimo o en una plataforma sin herramientas.

La tercera es la reproducibilidad. LuaRocks no fija por defecto el conjunto exacto de versiones instaladas de la forma en que lo hacen los ficheros de bloqueo de otros ecosistemas, de modo que dos instalaciones separadas en el tiempo pueden traer árboles de dependencias distintos si la especificación no pinta las versiones con precisión.

flowchart TD
A[Necesito una funcionalidad] --> B[La ofrece el programa anfitrion]
B -->|si| C[Usar la del anfitrion y no depender]
B -->|no| D[Esta en la biblioteca estandar]
D -->|si| C
D -->|no| E[Puedo escribirlo en menos de cien lineas]
E -->|si| F[Escribirlo y ser duenos del codigo]
E -->|no| G[Existe una biblioteca de referencia clara]
G -->|no| F
G -->|si| H[Es Lua puro y de un solo fichero]
H -->|si| I[Copiarla dentro del proyecto y fijar version]
H -->|no| J[Depender via LuaRocks y fijar version exacta]

Criterio para elegir una dependencia

En un lenguaje sin biblioteca grande, elegir bien es una habilidad técnica, no una cuestión de gusto. Estas son las preguntas que conviene hacerse, ordenadas por poder de descarte.

La primera es si la dependencia es de Lua puro o contiene C. Una dependencia de Lua puro se instala copiando ficheros, funciona en cualquier plataforma donde funcione el intérprete y se puede interiorizar en el proyecto. Una dependencia con C es más rápida y a menudo insustituible, pero introduce un requisito de construcción que se propagará a todos los que usen tu código.

La segunda es cuántas dependencias transitivas arrastra. En Lua la respuesta sana suele ser cero o una; un paquete que trae doce está importando una filosofía distinta de la del lenguaje.

La tercera, y la más específica de esta cultura, es si puedes leer su código fuente entero en una tarde. Una parte enorme del ecosistema son ficheros de doscientas a quinientas líneas de Lua legible. Leerlas antes de adoptarlas es factible, y cambia por completo la naturaleza de la relación: dejas de depender de una caja negra y pasas a usar código que entiendes.

La cuarta es la compatibilidad de versiones declarada, con las cuatro variantes que importan, y la quinta es la salud del mantenimiento: fecha del último cambio, número de personas con permiso de publicación, presencia de pruebas y de licencia explícita.

Existe además una prueba práctica que descarta más candidatos que todas las anteriores juntas: cargar la biblioteca y mirar qué deja detrás. Una dependencia sana define un módulo y devuelve una tabla. Una dependencia mal educada escribe en el entorno global, sustituye funciones de la biblioteca estándar o parchea metatablas compartidas, y esos efectos son invisibles hasta que rompen algo remoto.

-- Detectar globales nuevos creados al cargar un modulo
local antes = {}
for k in pairs(_G) do antes[k] = true end

local m = require("candidata")

for k in pairs(_G) do
  if not antes[k] then print("global nuevo:", k) end
end
print("devuelve:", type(m))   -- lo sano es table o function
💡
Comprueba también si toca la metatabla de las cadenas

Añadir métodos a la tabla string es una práctica tentadora y contaminante: cualquier cadena del proceso los hereda, incluidas las de otras bibliotecas. Comparar la lista de nombres de string antes y después de cargar una dependencia cuesta cuatro líneas y detecta de inmediato a los invasores.

⚠️
Una biblioteca abandonada de Lua puro no es lo mismo que una abandonada con C

Si un módulo de Lua puro deja de mantenerse y es pequeño, el coste de adoptarlo es bajo: lo copias, lo corriges y sigues. Si el que deja de mantenerse es un enlace con una biblioteca de C, cada nueva versión del sistema, del compilador o del intérprete puede romper la compilación, y arreglarlo exige competencias que quizá no tengas. La distinción debería pesar más de lo que suele pesar.

Depender, copiar o escribir

La práctica de copiar un módulo dentro del propio proyecto está mal vista en muchos ecosistemas y en Lua es no solo aceptable sino frecuentemente la decisión correcta. La razón es de proporciones: cuando la dependencia es un fichero de trescientas líneas que puedes leer, revisar y probar, mantener un gestor de paquetes, un árbol de instalación y un procedimiento de despliegue para administrarla es más complejo que el propio código que administra.

-- Un proyecto que interioriza sus dependencias pequenas
-- proyecto/
--   init.lua
--   vendor/json.lua      -- copiado, con version y licencia anotadas arriba
--   vendor/inspect.lua

local json = require("proyecto.vendor.json")

La disciplina que hace legítima esta práctica cabe en cuatro reglas. Anota en la cabecera del fichero copiado su origen, su versión exacta y su licencia. No lo modifiques salvo que sea imprescindible, y si lo haces, deja constancia del cambio en el mismo sitio. Escríbele pruebas propias, porque a partir de ese momento el código es tuyo. Y revisa periódicamente si el original corrigió algo que te afecta.

La tercera vía es escribirlo. En un lenguaje cuyas primitivas de tabla, cadena y función son tan expresivas, una cantidad sorprendente de necesidades comunes se resuelve en menos de cien líneas: una copia profunda, una división de cadenas, una cola, un conjunto, una caché de tamaño acotado, un serializador para el subconjunto de datos que tu programa realmente maneja. Escribir esas cien líneas suele salir más barato a medio plazo que negociar la interfaz, las suposiciones y el ciclo de vida de una biblioteca ajena que resuelve además veinte casos que no tienes.

El criterio es la biblioteca estándar que Lua no te dio

La conclusión del nivel entero se puede formular así: Lua no te entrega una biblioteca grande, te entrega el trabajo de decidir. Y ese trabajo, que en un lenguaje con baterías incluidas está delegado en un comité que ya eligió por ti, aquí es una competencia profesional que se puede hacer bien o mal. Hacerlo bien significa mantener un inventario consciente de lo que entra en tu proceso, saber para cada dependencia por qué está, quién la mantiene y qué costaría sustituirla, y ser capaz de justificar cada una ante alguien que pregunte. Hacerlo mal significa acumular paquetes por la vía de la mínima resistencia hasta que el proyecto depende de un árbol que nadie ha leído, con dos analizadores de JSON incompatibles cargados a la vez y un módulo en C que solo compila en la máquina del programador que se fue. La diferencia entre ambos desenlaces no la decide el gestor de paquetes, la decide el criterio, y por eso este nivel termina aquí y no en una lista de comandos. Hay además una consecuencia que solo se aprecia con los años: como el conjunto de dependencias de un proyecto Lua bien llevado es pequeño y comprensible, el proyecto entero sigue siendo comprensible. Se puede auditar, portar a otra versión del lenguaje, empotrar en otro anfitrión o resucitar una década después, porque no hay debajo una torre de abstracciones que nadie recuerda cómo se montó. Esa propiedad —que un sistema completo quepa en una cabeza— es exactamente la misma que hacía posible recorrer la biblioteca estándar entera en la primera lección de este nivel. No es una coincidencia: es la misma idea aplicada a otra escala, y es, en último término, lo que uno compra cuando elige Lua.

⚔️ Auditar el árbol
  1. Enumera las dependencias de tu proyecto actual y clasifícalas en Lua puro y con C; anota para cada una sus dependencias transitivas.
  2. Elige la más pequeña, lee su código fuente entero y decide si tiene sentido interiorizarla.
  3. Ejecuta luarocks init en un proyecto vacío, instala dos paquetes y examina cómo cambian las rutas de búsqueda de módulos.
  4. Toma una necesidad que hoy resuelves con una biblioteca y escribe tu propia versión mínima; compara líneas, capacidades y riesgo.
  5. Redacta en media página la política de dependencias de tu proyecto y las condiciones que debe cumplir un paquete para entrar.