Las rutas: package.path y package.cpath
Cómo se traduce un nombre con puntos en una ruta de fichero: plantillas con interrogante, el separador que anuncia package.config, las bibliotecas en C y su función de apertura, y un método ordenado para depurar un módulo que no aparece.
Entre el nombre que escribes en require y el fichero que acaba ejecutándose hay una traducción mecánica, determinista y de una simplicidad casi decepcionante: dos sustituciones de texto sobre una lista de plantillas. No hay resolución relativa al fichero que llama, no hay directorio de proyecto, no hay node_modules ni nada equivalente. Hay una cadena de plantillas, un carácter comodín y una regla de primera coincidencia. Quien domina esas tres piezas deja de sufrir para siempre el error de módulo no encontrado, porque el error deja de ser un misterio y pasa a ser una lista de intentos que puede leer.
- Leer
package.pathypackage.cpathcomo listas de plantillas y no como listas de directorios. - Traducir a mano un nombre con puntos hasta la ruta concreta que Lua abrirá.
- Explicar cómo el buscador de C deduce el nombre de la función de apertura de una biblioteca.
- Diagnosticar con
package.searchpathun módulo que no se encuentra, sin recurrir al ensayo y error.
Plantillas, no directorios
package.path es una única cadena que contiene varias plantillas separadas por punto y coma. Cada plantilla es una ruta de fichero en la que un signo de interrogación marca el lugar donde se insertará el nombre del módulo.
print(package.path)
--> ./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;...
La distinción entre plantilla y directorio no es pedante. Una plantilla puede tener el comodín en cualquier posición, puede contener más de un componente de ruta y puede añadir sufijos: ./libs/?/principal.lua es perfectamente válida. La plantilla ./?/init.lua es la que hace posible que un directorio entero se comporte como un módulo, y es el motivo de que un paquete pueda repartirse en varios ficheros con un init.lua como puerta de entrada.
Los caracteres que gobiernan todo esto no están fijados en la especificación del lenguaje sino en la compilación del intérprete, y se anuncian en package.config, una cadena de varias líneas que casi nadie mira.
for linea in package.config:gmatch("[^\n]+") do print(linea) end
--> / separador de directorios
--> ; separador de plantillas
--> ? marca de sustitucion
--> ! directorio del ejecutable, solo en Windows
--> - marca de ignorar, para el buscador de C
Las dos primeras líneas son las que hacen portable el código que manipula rutas: construir package.path concatenando barras a mano funciona hasta que alguien ejecuta tu programa en un sistema que no las usa.
De un nombre con puntos a una ruta
El punto en un nombre de módulo no denota pertenencia ni jerarquía: es solo un carácter que el buscador sustituye por el separador de directorios antes de aplicar las plantillas. require("app.red.cliente") no significa que exista un módulo app ni que app.red esté cargado. Significa, literalmente, que se buscará app, luego el separador, luego red, luego el separador, luego cliente.
flowchart TB A[nombre app punto red punto cliente] --> B[cada punto pasa a separador de directorios] B --> C[queda app red cliente] C --> D[la interrogacion de cada plantilla se sustituye por eso] D --> E[primera plantilla probada] E -->|el fichero existe| F[se compila y se ejecuta ese chunk] E -->|no existe| G[siguiente plantilla de la lista] G --> E G -->|se agotan| H[se acumula un mensaje por cada intento]
Que gane la primera plantilla que produzca un fichero legible tiene un efecto que conviene tener siempre presente: el orden de package.path es una política de precedencia. Anteponer un directorio propio permite sombrear un módulo instalado en el sistema sin desinstalarlo, y también permite romper un programa ajeno por accidente si el nombre elegido colisiona.
La modificación se hace por concatenación, y hay dos formas con significados distintos.
-- Prioridad sobre lo ya existente
package.path = "./lib/?.lua;./lib/?/init.lua;" .. package.path
-- Recurso de ultimo momento, despues de todo lo demas
package.path = package.path .. ";./extras/?.lua"
Desde fuera del programa se controla con las variables de entorno LUA_PATH y LUA_CPATH, que sustituyen al valor por defecto. El doble punto y coma es la pieza que hace usable esa sustitución: se expande al valor por defecto compilado en el intérprete.
LUA_PATH="./lib/?.lua;;" lua main.lua
Sin el doble punto y coma, el valor por defecto desaparece y dejarán de encontrarse los módulos instalados en el sistema. Es el origen de más de la mitad de los fallos que aparecen solo dentro de un contenedor o de un servicio.
Queda un detalle que muerde en cuanto el programa cambia de directorio de trabajo. Las plantillas que empiezan por punto y separador son relativas al directorio actual del proceso, no al del script ni al del intérprete. Un programa que funciona lanzado desde su carpeta deja de encontrar sus propios módulos si se lanza desde otra, y el remedio no es reordenar plantillas sino construirlas a partir de una raíz absoluta calculada al arrancar.
local raiz = os.getenv("MI_APP_RAIZ") or "."
package.path = raiz .. "/lib/?.lua;" .. raiz .. "/lib/?/init.lua;" .. package.path
Módulos en C y la función de apertura
package.cpath funciona exactamente igual, con plantillas y comodín, pero apunta a bibliotecas dinámicas con la extensión propia de la plataforma. La diferencia interesante no está en la ruta sino en lo que ocurre después de abrir el fichero, porque una biblioteca dinámica no devuelve nada por sí sola: hay que llamar a una función concreta dentro de ella.
Ese nombre se deduce del nombre del módulo con dos reglas. Los puntos se convierten en guiones bajos, y al resultado se le antepone luaopen_. Así, require("lpeg") busca luaopen_lpeg, y require("socket.core") busca luaopen_socket_core.
print(package.cpath)
--> ./?.so;/usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/loadall.so
La quinta línea de package.config, el guion, cubre un caso especial de este mecanismo: si el nombre del módulo contiene un guion, el buscador de C prueba primero la función de apertura correspondiente a la parte anterior al guion. Eso permite que una misma biblioteca conviva en varias versiones instaladas en paralelo, con nombres como mimodulo-2.1, sin que el nombre de la función C cambie.
El cuarto buscador, el llamado todo en uno, existe para el caso en que un paquete distribuye varios submódulos dentro de una sola biblioteca compartida. Ante socket.core y tras fallar el buscador de C con la ruta completa, se prueba a cargar la biblioteca de la raíz socket y a llamar dentro de ella a luaopen_socket_core. Es la razón de que un único fichero binario pueda satisfacer una familia entera de nombres.
Conviene retener también que la carga de una biblioteca dinámica pasa por el enlazador del sistema y no por Lua. Los errores de esa fase —una dependencia ausente, una versión incompatible de la biblioteca de C, un símbolo sin resolver— llegan como texto del sistema operativo dentro del mensaje de require, y se distinguen del fallo de búsqueda en que mencionan el fichero por su nombre: en ese caso la ruta era correcta y el problema es otro.
Los dos buscadores son independientes y consultan cadenas distintas. Añadir un directorio a package.path no hace que se encuentren las bibliotecas que hay en él, y es un error tan frecuente como silencioso: el mensaje seguirá diciendo que no encuentra el módulo, y la línea que falta es la del intento con la extensión dinámica. Cuando instales algo con componentes en C, ajusta las dos cadenas.
Depurar un módulo que no aparece
La función que usan los buscadores está expuesta y puedes llamarla tú. package.searchpath recibe un nombre y una cadena de plantillas, y devuelve la ruta encontrada o bien nil y el mismo texto de intentos fallidos que verías en el error.
local ruta, intentos = package.searchpath("app.red.cliente", package.path)
if ruta then
print("encontrado en " .. ruta)
else
print(intentos)
end
Con esa herramienta, el diagnóstico se vuelve un procedimiento de cuatro pasos y deja de ser adivinación. Primero se comprueba el nombre exacto, incluidas mayúsculas: la traducción del nombre a ruta respeta las diferencias que el sistema de ficheros respete, y un módulo que funciona en un sistema insensible falla en cuanto se despliega en otro que no lo es. Segundo, se imprime package.path completo en el punto de fallo, no en el intérprete interactivo, porque puede haber sido modificado por cualquier dependencia cargada antes. Tercero, se llama a package.searchpath y se lee la lista de intentos línea a línea, que dice qué rutas se probaron y en qué orden. Cuarto, si el módulo tiene parte en C, se repite todo con package.cpath.
El nombre viaja como cadena, los puntos se vuelven separadores de directorio, la interrogante de cada plantilla se sustituye por el resultado y gana la primera plantilla que abra un fichero. package.path sirve a los ficheros de Lua y package.cpath a las bibliotecas dinámicas, cuya función de apertura se llama luaopen_ seguido del nombre con los puntos vueltos guiones bajos. package.searchpath te enseña todo el proceso sin que tengas que adivinarlo.
Queda un quinto caso que ningún paso anterior detecta: el módulo sí se encuentra, pero es otro. Ocurre cuando una plantilla anterior lo sombrea, y la única señal fiable es la ruta real de carga.
local m, origen = require("app.red.cliente")
print(origen) --> desde 5.4, la ruta exacta que se cargo
En Node, en Python o en Rust, el nombre de un módulo se resuelve en relación con quien lo pide: el sistema conoce el fichero que está ejecutando la importación y busca a partir de él, subiendo directorios o consultando el paquete al que pertenece. Lua no hace nada de eso, y la diferencia es estructural. El buscador de Lua no sabe quién llamó a require; recibe una cadena y una lista de plantillas globales al proceso, y ahí acaba su información. La consecuencia inmediata es que un módulo no puede pedir a su vecino por una ruta relativa, y por tanto la posición de un fichero en el árbol no forma parte de su identidad. Un módulo se identifica solo por el nombre canónico bajo el que todo el programa acuerda pedirlo, y ese acuerdo no lo garantiza nada más que la disciplina del equipo. Esto parece una regresión y hay un sentido en el que lo es: se pierde la refactorización mecánica, se pierde la detección de dependencias por análisis del árbol, y mover un directorio rompe cosas que ningún compilador avisará. Pero hay que ver lo que se compra a cambio, porque es exactamente lo que hace de Lua un lenguaje empotrable. Un buscador que dependiese del fichero que llama exigiría que todo módulo viniese de un fichero, y en un intérprete empotrado la mayoría no viene de ninguno: vienen de la memoria, de un archivo comprimido, de una base de datos o de una tabla registrada desde C. El espacio de nombres plano y absoluto es lo único que permite tratar por igual a todas esas procedencias. Lua no eligió peor que Node; eligió el sistema de nombres que sobrevive cuando no hay sistema de ficheros.
- Imprime
package.path,package.cpathypackage.config, y escribe a mano la lista completa de ficheros que se probarían pararequire("a.b.c"). - Crea un directorio con
init.luadentro y compruébalo con la plantilla adecuada. Explica qué plantilla exacta lo encontró. - Coloca dos ficheros con el mismo nombre en dos directorios distintos y reordena
package.pathpara que gane uno u otro. Verifica el resultado con el segundo valor de retorno derequire. - Lanza el programa con
LUA_PATHsin el doble punto y coma y observa qué módulos dejan de encontrarse. Repítelo con él. - Escribe una función que reciba un nombre de módulo y devuelva la lista de rutas candidatas sin abrir ningún fichero, y contrástala con la salida de
package.searchpath.