7 puntos por GN⁺ 2025-05-10 | 3 comentarios | Compartir por WhatsApp
  • El sistema de gestión de dependencias de Rust hace que el desarrollo sea cómodo, pero la cantidad y la calidad de las dependencias son motivo de preocupación
  • Incluso un crate bien utilizado puede no estar actualizado, así que a veces conviene más implementarlo uno mismo
  • Después de agregar crates conocidos como Axum y Tokio, el número total de líneas de código incluyendo dependencias llegó a 3.6 millones, algo difícil de manejar
  • El código que realmente escribí es de apenas unas 1,000 líneas, pero en la práctica es imposible revisar y auditar todo el código de alrededor
  • No hay una solución clara sobre si se debe ampliar la biblioteca estándar de Rust ni sobre cómo implementar la infraestructura central, y toda la comunidad debe pensar junta en el equilibrio entre rendimiento, seguridad y mantenibilidad

Panorama general del problema de dependencias en Rust

  • Rust es mi lenguaje favorito, y tanto la comunidad como la usabilidad del lenguaje son excelentes
  • La productividad de desarrollo es alta, pero últimamente me han surgido preocupaciones en cuanto a la gestión de dependencias

Ventajas de los crates de Rust y Cargo

  • Con Cargo es posible gestionar paquetes y automatizar tareas de compilación, lo que mejora mucho la productividad
  • Es fácil moverse entre varios sistemas operativos y arquitecturas, y no hace falta preocuparse por administrar archivos manualmente ni configurar herramientas de build
  • Se puede empezar a escribir código de inmediato sin preocuparse aparte por la gestión de paquetes

Desventajas de la gestión de crates en Rust

  • Como se presta menos atención a la gestión de paquetes, se termina descuidando la estabilidad
  • Por ejemplo, usé el crate dotenv y me enteré mediante un Security Advisory de que ya no tenía mantenimiento
  • Consideré un crate alternativo (dotenvy), pero terminé implementando por mi cuenta solo lo necesario en unas 35 líneas
  • Como los problemas de paquetes sin mantenimiento ocurren con frecuencia en muchos lenguajes, el fondo del problema es que hay situaciones donde las dependencias son inevitables

El aumento explosivo del volumen de código causado por las dependencias

  • Estoy usando paquetes importantes y de buena calidad del ecosistema Rust como Tokio y Axum
  • Como dependencias agregué Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing y tracing-subscriber
  • El objetivo principal es un servidor web, descompresión de archivos y logging, así que el proyecto en sí es simple
  • Usé la función cargo vendor para descargar localmente todos los crates dependientes
  • Al analizar las líneas de código con tokei, el total incluyendo dependencias llegó a aproximadamente 3.6 millones de líneas (sin contar los crates vendorized, unas 11,136 líneas)
  • Como referencia, se dice que todo el kernel de Linux tiene unas 27.8 millones de líneas, así que mi pequeño proyecto equivale a una séptima parte de eso
  • El código real que escribí es de apenas unas 1,000 líneas
  • Supervisar y auditar esa enorme cantidad de código dependiente es, en la práctica, imposible

Reflexiones sobre una solución

  • Por ahora no hay una solución clara
  • Algunos proponen ampliar la biblioteca estándar como en Go, pero eso también genera nuevos problemas, como una mayor carga de mantenimiento
  • Rust busca alto rendimiento, seguridad y modularidad, y como apunta a competir en embebidos y con C++, hay que ser prudentes al ampliar la biblioteca estándar
  • Por ejemplo, incluso un runtime tan sofisticado como Tokio se mantiene de forma muy activa en GitHub y Discord
  • En la práctica, implementar por cuenta propia infraestructura clave como un runtime asíncrono o un servidor web es demasiado para un desarrollador individual
  • Incluso un servicio grande como Cloudflare usa directamente dependencias de tokio y crates.io, y no está claro con qué frecuencia las auditan
  • Clickhouse también ha mencionado problemas relacionados con el tamaño de los binarios y la cantidad de crates
  • Con Cargo es difícil identificar con precisión las líneas de código que terminan incluidas en el binario final, y también existe la limitación de que se incluya código innecesario según la plataforma
  • Al final, la realidad es que no queda más que preguntarle a toda la comunidad por una respuesta

3 comentarios

 
codemasterkimc 2025-05-11

Si corres Trivy, hay muchos menos hallazgos high o critical y es más seguro que js NPM o Java Maven; entonces, ¿qué es lo que este artículo quiere afirmar sobre Rust?

 
GN⁺ 2025-05-10
Opiniones de Hacker News
  • En mi opinión, cualquier sistema donde sea "fácil" agregar dependencias y no haya penalización por tamaño o costo termina inevitablemente en problemas de dependencias. Si ves cómo se distribuía el software en los últimos 40 años, en los 80 las bibliotecas se compraban, y en entornos con restricciones de espacio se seleccionaban solo las partes necesarias. Hoy en día seguimos apilando bibliotecas sobre más bibliotecas. Puedes usar import foolib en una línea y a nadie le importa qué hay dentro. En cada nivel solo necesitas como 5% de la funcionalidad, pero mientras más profundo es el árbol, más código inútil se acumula. Al final un binario simple termina pesando 500 MiB, y acabas trayendo una dependencia solo para formatear un número. Go y Rust fomentan meterlo todo en un solo archivo, así que si quieres usar solo una parte terminas en una situación incómoda. A largo plazo, la solución real sería un rastreo ultrafino de símbolos/dependencias, donde cada función/tipo declare exactamente qué elementos necesita, para usar solo el código exacto y desechar el resto. Personalmente no me encanta la idea, pero no se me ocurre otra forma de resolver el sistema actual, que raspa el universo entero desde el árbol de dependencias
    • Puede que no sepa mucho por ser estudiante universitario, pero el compilador de Rust ya detecta código, variables y funciones no utilizadas. La mayoría de los IDE también pueden hacerlo en muchos lenguajes. Entonces, ¿no bastaría con eliminar esas partes? El código no usado no se compila
    • De hecho, trabajando en Rust con una biblioteca de árbol de dependencias relativamente pesada (Xilem), intenté recortar con feature flags, pero casi todas las dependencias realmente tenían que quedarse según la funcionalidad necesaria (vulkan, decodificación PNG, unicode shaping, etc.). Las dependencias innecesarias eran sobre todo muy pequeñas, y solo pude quitar serde_json con una modificación menor. Las dependencias más grandes (winit/wgpu, etc.) requieren cambios estructurales, así que no se pueden sacar fácilmente
    • Go o C# (.NET) son buenos contraejemplos. Tienen una gestión de paquetes y un ecosistema tan eficaces como Rust o JS (Node), pero relativamente no sufren tanto de dependency hell. Es porque la biblioteca estándar es excelente. La amplitud de una biblioteca estándar es algo en lo que solo grandes empresas como Google o Microsoft pueden invertir
    • Entonces, ¿por qué los compiladores actuales no eliminan el código que no se usa?
    • Antes se generaba un archivo .o por función y se agrupaban en un archivo .a, y el linker extraía solo las funciones necesarias. También se hacía namespacing con cosas como foolib_do_thing(). Hoy, con algo parecido al patrón god object, todas las funciones viven en un objeto de nivel superior, así que al importar foolib te traes todo. En ese estado, al linker le resulta difícil determinar qué funciones son realmente imprescindibles. En cambio, Go tiene una eliminación de código muerto excelente, así que si no se usa, se corta del binario final
    • Los compiladores y linkers modernos ya hacen extracción de símbolos y eliminación de código muerto, y Rust también lo soporta con proyectos como min-sized-rust
    • Antes se gestionaban las bibliotecas incluyendo todo dentro del proyecto e integrándolo directamente en los archivos de build. Requiere mucho esfuerzo y es incómodo, pero te obliga a interactuar mucho más a fondo que simplemente agregando una línea en un archivo de deps
    • Go en realidad no insiste en un solo archivo; también soporta con facilidad la separación lógica en varios archivos. Esa parte me gusta mucho
    • Dotnet ya está materializando esta idea con Trimming y Ahead Of Time Compilation. Otros lenguajes podrían aprender de Dotnet
    • Con LTO (Link Time Optimization), este problema queda completamente resuelto desde la perspectiva del tamaño del binario. Las partes no usadas se eliminan con la optimización. El tiempo de build sigue siendo un problema
    • Yo diría que el problema no es la biblioteca en sí, sino la falta de visibilidad sobre qué tanto se usa realmente dentro después de agregar una dependencia. Hace falta un entorno donde sea fácil obtener retroalimentación sobre rendimiento por paquete, proporción de código sobrante en el build, etc.
    • El lenguaje Unison adoptó algo parcialmente parecido a esta idea. Cada función se define según una estructura AST y se trae/reutiliza desde un registro global basado en hashes
    • En lugar del mantenimiento distribuido de pequeños fragmentos de bibliotecas tipo isEven, isOdd o leftpad en npm, una gran biblioteca de propósito general mantenida por un equipo federado ofrece mucho más futuro y continuidad
    • En vez de perseguir símbolos/dependencias ultrafinos, otra idea es construir módulos ultrafinos y aprovechar los sistemas existentes de tree-shaking
    • La forma real en que Go maneja dependencias se acerca al ideal descrito en el post original. Un módulo es un conjunto de paquetes, y al hacer vendoring parece incluir solo los paquetes y símbolos realmente usados (aunque no sé si funciona exactamente a nivel de símbolo)
    • El sistema de módulos de JS sí soporta precisamente ese tipo de gestión ultrafina de símbolos y tree shaking
    • La idea original propuesta de dependencias ultrafinas ya está resuelta en Rust con section splitting como --gc-sections
    • Rust es un lenguaje donde las imports finas funcionan muy bien mediante la división de API con crate features. Es distinto de Go
    • Dependiendo de la arquitectura, por ejemplo en un thick client centrado en lo local, aunque la instalación inicial pese 800 MB, no hay problema si luego en uso real solo necesita comunicación de red muy restringida. Incluso se pueden tolerar grandes dependencias repetitivas para colaboración en la UI
    • La mejor forma de reutilizar código es precisamente usar estas dependencias. Optimiza solo donde de verdad haga falta
    • Ya en los 80 el concepto de componentes de software reutilizables se volvió realidad a través de lenguajes como Objective-C. Uno de los grandes éxitos de Rust es que esa componentización de software también se adoptó ampliamente en un lenguaje de programación de sistemas
    • Con tree shaking, el problema de tamaño/inflado de código se puede resolver hasta cierto punto (en servidores ni se preocupan por eso). El problema más grave es el riesgo de la cadena de suministro y la seguridad. Cuanto más grande es una empresa, más suele haber procesos de aprobación para usar open source. Aumentar la granularidad no sirve de mucho en seguridad si 1000 funcionalidades vienen de 1000 autores de NPM distintos
    • Aunque en cada capa de abstracción del paquete solo se use 50%, en cada nivel el tamaño total crece al doble de lo realmente necesario. En 3 niveles, 88% del código es inútil. Ejemplo: la calculadora de Windows 11 trae bibliotecas innecesarias, hasta herramientas de recuperación de cuenta. Es un caso donde la facilidad de agregar funcionalidades termina aumentando la complejidad
    • Estoy de acuerdo en que la acumulación de dependencias es un problema. La mejor defensa posible hoy es gestionar las dependencias del sistema con una disciplina extrema. En vez de traer una biblioteca externa por una sola función de 10 líneas, a veces hasta copio y pego el código directamente. Un ecosistema de bibliotecas sano es la excepción. A los ingenieros junior que agregan dependencias sin control muchas veces hay que frenarlos de inmediato
    • Hace tiempo que no veía a alguien hablar con tanta seguridad sin siquiera conocer lo básico de Rust
    • Gracias a la eliminación de código muerto, en lenguajes compilados como Rust un árbol de dependencias grande no se traduce en binarios inflados
  • El problema que siento en el ecosistema npm es que muchos desarrolladores agregan dependencias sin pensar realmente en el diseño. Por ejemplo, una biblioteca glob debería ser solo una función sencilla de globbing, pero el autor también empaqueta una herramienta de línea de comandos y mete un parser grande como dependencia. Eso provoca advertencias frecuentes de "dependency out-of-date". También se discute cuál debería ser el alcance de responsabilidad de una biblioteca glob. Hacer solo pattern matching sobre strings da un diseño más flexible, por ejemplo para tests o para abstraer el sistema de archivos. Mucha gente quiere bibliotecas todopoderosas que "hagan de todo", pero mientras más hacen, mayores son los efectos secundarios. Sospecho que Rust no será tan diferente
    • El criterio de diseño importa, y un buen lenguaje no debería ni apoyar ni estorbar demasiado esas preferencias del desarrollador. Rust, Zig y C son así. Estadísticamente, el problema aparece menos. Cuando se junta una "multitud" de desarrolladores, surge un "modelo bazar" donde cualquiera apila crates libremente. Al final me gustaría que Rust también tuviera una biblioteca estándar oficial con namespaces bien ordenados y una configuración con "baterías incluidas" (por ejemplo algo como stdlib::data_structures::automata::weighted_finite_state_transducer). Como el lenguaje ya incorpora control de versiones y compatibilidad hacia atrás, espero que siga mejorando
    • La función glob de POSIX en realidad recorre el sistema de archivos. Para matching de strings está fnmatch. Lo ideal sería que fnmatch estuviera en un módulo separado y fuera una dependencia de glob. Intentar implementar glob directamente es bastante difícil; hay requisitos complejos como la estructura de directorios, brace expansion y otros, así que hace falta una combinación bien diseñada de funciones
    • En Rust, el borrow checker ha servido como una especie de escudo frente a desarrolladores con poco criterio de diseño. No está claro cuánto tiempo seguirá teniendo ese efecto
    • Una gran ventaja de Rust es que, en general, sus desarrolladores son competentes y la calidad de los crates suele ser alta
    • Bun también incluye funcionalidad de glob
  • No hace falta señalar específicamente a Rust: los problemas de dependencias y los ataques a la cadena de suministro ya son una realidad. Si se diseñara un lenguaje nuevo, habría que incorporar un "capability system" para aislar de forma segura todo el árbol de bibliotecas. Por ejemplo, al diseñar una biblioteca para cargar imágenes, en lugar de aceptar archivos podría aceptar solo streams, o marcar explícitamente que "no tiene permiso para abrir archivos", bloqueando en tiempo de compilación el uso de funciones peligrosas. En ecosistemas existentes no es fácil, pero si se hace bien se puede minimizar la superficie de ataque. Incluso una cultura de minimizar dependencias difícilmente resuelve el problema de raíz, y ni siquiera lenguajes como Go están libres de ataques a la cadena de suministro
    • Hace falta difundir activamente una cultura Sans-IO, es decir, diseño donde la dependencia no haga IO directamente. También haría falta una cultura donde, al anunciar una biblioteca nueva, se señale si implementa IO directo. Claro, el escrutinio masivo por sí solo no basta, pero sería bueno que el principio Sans-IO se extendiera
    • Como ejemplo está WUFFS, un lenguaje de propósito especial. En la práctica ni siquiera puede imprimir Hello world y no tiene tipo string. En cambio, está especializado en parsear formatos de archivo no confiables. Necesitamos más lenguajes de propósito especial así. Son rápidos y no presentan riesgos, por lo que también se pueden reducir chequeos innecesarios
    • Java y .NET Framework ya tenían hace décadas funciones de partial trust/capabilities, pero no se usaron ampliamente y terminaron abandonándose
    • En Rust hay una tendencia parecida, al menos un poco. Con #![deny(unsafe_code)] puedes hacer que el uso de código unsafe cause error de compilación y notificarlo al usuario. No es una verificación coercitiva total, porque si se permite explícitamente, se puede seguir usando código unsafe. Uno podría imaginar un capability system que controle de forma transitiva funciones de la biblioteca estándar, como una especie de feature flag
    • Me gustaría construir algo así yo mismo, y espero que algún día se haga realidad. En Rust es parcialmente posible rastrear capabilities con herramientas basadas en linter. Hace falta resolver los problemas de unsoundness del compilador
    • Introducir una imposición completamente estática en lenguajes o ecosistemas existentes es difícil, pero incluso con validación en runtime se obtiene la mayor parte del beneficio. Si compilas el código de una biblioteca desde fuente, podrías envolver cada system call con chequeos de permisos. En caso de violación, se haría panic, y haría falta el esfuerzo de escribir/distribuir un capability profile por biblioteca. Algo parecido ya se ha demostrado en TypeScript
    • Haskell realiza este enfoque hasta cierto punto con el IO monad. Las funciones que no pueden hacer IO quedan restringidas por la firma de tipos
    • Según yo, para un sistema así habría que cambiar por completo la forma de comunicarse con el OS. La trampa es que incluso leer de un stream puede terminar usando system calls de lectura de archivos
    • Existe un proyecto llamado Capslock que funciona en Go de una manera cercana a esto
    • Si restringes desde el programa de entrada que las bibliotecas no puedan importar APIs del sistema, entonces basta con inyección de dependencias para pasar capabilities. Se puede diseñar incluso con lenguajes actuales, pero el problema práctico es que rompe compatibilidad con bibliotecas existentes
    • Me pregunto si alguna vez se ha implementado algo parecido a esta idea. En los lenguajes actuales parece muy difícil de aplicar
    • No se puede con un solo lenguaje; hace falta un ecosistema multilenguaje
    • En el ecosistema TypeScript, por ejemplo, si estás en un entorno sin clases para operaciones de archivos, la compilación falla y la restricción se aplica de forma natural
  • Es un problema general del desarrollo moderno de software. La barrera de entrada es baja y se reutiliza mucho código existente. Una dependencia, al final, es código no confiable. Si no hay una solución técnica, alguien tiene que seguir revisando código, manteniéndolo y sosteniendo sistemas de confianza sociales y legales. Si se mete todo en la stdlib de Rust, el equipo central tendría que responsabilizarse por todo ese código, así que la carga de mantenimiento crecería mucho
    • La gravedad aparente varía según el lenguaje. Los lenguajes con bibliotecas estándar potentes tienen ventaja porque pueden hacer mucho con pocas dependencias externas. En lenguajes con poca funcionalidad base, como JS/Node, depender de paquetes externos es lo normal. Que algo sea "ligero" no siempre es bueno
    • Creo que Rust necesita integrar más cosas en la biblioteca estándar. Go tiene una biblioteca estándar excelente, mientras que en Rust incluso funcionalidades básicas (web, tls, x509, base64, etc.) implican sufrimiento a la hora de elegir y gestionar bibliotecas
    • Gilad Bracha propuso un enfoque interesante para hacer sandboxing de bibliotecas de terceros: eliminar import y hacer todo mediante inyección de dependencias. Si no inyectas algo como un subsistema de IO, el código de terceros jamás puede acceder a eso. Si quieres dar solo capacidad de lectura, puedes inyectar solo un wrapper de lectura. Aun así, en programación de sistemas hay límites (por ejemplo por unsafe code)
    • También se propuso una estructura tipo QubesOS: correr cada biblioteca en un entorno aislado, tu propio código en dom0, cada biblioteca en una template VM separada, y usar namespaces de red para la comunicación. En industrias sensibles, eso sí sería práctico
    • Por lo que he visto, no es que hoy hagamos cosas más complejas, sino que hacemos las mismas cosas de forma más compleja. El objetivo en sí no se volvió más difícil
    • En realidad la situación varía por lenguaje. En C/C++, agregar dependencias es difícil, y si además quieres soporte multiplataforma es todavía más molesto, así que este mismo problema no aparece tanto
    • El verdadero problema es el inflado de código innecesario. Casi todos los proyectos están llenos de complejidad innecesaria y sobreingeniería. Ese es el problema de esta industria
  • blessed.rs recomienda una lista de bibliotecas útiles que son difíciles de meter en la biblioteca estándar. Me gusta porque, gracias a ese sistema, la mayoría de los paquetes quedan limitados a propósitos específicos y se pueden gestionar mejor
    • También vale la pena recomendar cargo-vet. Permite rastrear y definir paquetes confiables, desde aquellos que requieren una auditoría experta antes de importarse, hasta políticas semi-YOLO como "confiemos nomás en los paquetes mantenidos por los maintainers de tokio". Es un poco más formal que blessed.rs y sirve bien para compartir dentro del equipo una lista oficial o cuasi estándar
    • Ojalá existiera un sistema así en Python
    • Lo revisé y de verdad parece un proyecto de recomendaciones muy bueno
  • Desde el incidente de leftpad quedó una percepción negativa de los package managers. Pero algo como tokio en la práctica es casi una función a nivel de lenguaje, así que si el OP cree que también habría que auditar por cuenta propia todo Go entero o hasta el V8 de Node, eso no es realista
    • De hecho, alguien sí audita tokio de forma constante. No es mucha gente, pero alguien lo hace
    • El hecho de que cargo incluya ambas versiones cuando dos dependencias usan versiones distintas es algo que cargo soporta de manera bastante particular
  • Las feature flags en los paquetes de cargo son realmente una gran ventaja. Yo mismo suelo enviar PRs para esconder dependencias innecesarias detrás de esas flags. Con cargo tree puedes ver fácilmente el árbol de dependencias. Una vista de líneas de código que realmente terminan en el binario no tiene mucho sentido. Muchas funciones terminan inlineadas y casi todo se fusiona dentro de main
    • Es una lástima que npm no tenga feature flags. Me pregunto si habrá algún package manager que sí las soporte. Quiero aislar dentro de bibliotecas internas el código dependiente de cierto framework para extenderlo
  • A mí me pasa algo parecido. Agregar dependencias con Cargo es tan fácil que, aunque yo solo trate de ser cuidadoso, con unas cuantas que metas ya vienen arrastrando decenas de dependencias transitivas. Tampoco es realista decir que entonces no se usen. En C++ esto pasa menos. Rust tiene muchísima fragmentación en paquetes pequeños, así que a veces se siente como traer código aleatorio de internet. Me gusta Rust, pero no me gusta esta estructura
    • En un post enlazado desde el subreddit de Rust decían que en C++ las dependencias se ven menos porque la mayoría se distribuyen como bibliotecas dinámicas. Incluso se podría considerar una ventaja depender de la capacidad del package manager del sistema operativo para manejar estabilidad y seguridad. Estaría bien que Rust adoptara хотя sea un concepto de extensión de biblioteca estándar
    • Como las dependencias de C++ son complejas y los sistemas de build son un desastre, me parece mejor tener dependencias al estilo Rust aunque sean menos estables. De hecho, las dependencias transitivas de C++ están aún más ocultas porque suelen venir precompiladas
    • En Rust, la división en paquetes pequeños no es tanto una "filosofía" como una consecuencia de la velocidad de build. Cuando un proyecto crece, terminas dividiéndolo en crates. No por abstracción, sino porque el rendimiento de compilación te obliga a reorganizarlo así
    • No hace falta aceptar automáticamente el argumento de "entonces no lo uses directamente". Vale la pena pensarlo un poco más
    • C++ y CMake son tan difíciles que en la práctica mucha gente termina no usándolos
  • Para bibliotecas centrales uso bibliotecas open source, y para funciones pequeñas copio y pego código tomando como referencia open source y lo mantengo directamente en mi código. El código se vuelve algo más grande de lo necesario, pero se reduce la carga de revisar código externo y la exposición a la cadena de suministro. Las bibliotecas grandes siguen siendo un problema, claro, pero tampoco puedes escribir todo tú mismo. No es un problema exclusivo de Rust, sino uno general
  • En el pasado, en otros lenguajes, para sistemas importantes yo definía una política de mínimos de módulos/paquetes, y todos los paquetes usados se movían a un repositorio interno de la empresa, donde se auditaban por rama y por actualización. En áreas como frontend eso es prácticamente imposible de gestionar con tanta rigidez. Últimamente, con las ruidosas herramientas y modelos open source de IA, están apareciendo preocupaciones parecidas sobre gestión de dependencias. Incluso haciendo proyectos personales en Rust, lo que más me incomoda es la explosión de dependencias en bibliotecas de UI/async. Si una sola se vuelve vulnerable, ya quedaste expuesto; es solo cuestión de tiempo
    • Lo realista es conectar el sistema de CI/CD solo con el repositorio interno oficial. Los desarrolladores pueden instalar lo que quieran en local, pero los commits no autorizados quedan bloqueados por el servidor de build
    • Existen RFCs para intentar resolver el riesgo de seguridad, pero por razones culturales (supongo) no hay cambios drásticos
    • Una de las cosas geniales de Rust es que incluso async puede implementarse directamente del modo que quieras. No quedas atado a una implementación específica
 
iolothebard 2025-05-11

No es un problema exclusivo de Rust.
Es una ventaja compartida y a la vez un problema potencial de todos los lenguajes que tienen repositorios públicos de paquetes y gestores de paquetes con soporte para dependencias transitivas.
Al final, quienes las usan tienen que usarlas bien…
A pesar del incidente de leftpad en Node&npm, no ha cambiado nada.