**IAIC** Paquetes y Entornos en Julia Cuando empieces a usar Julia, entrarás rápidamente en contacto con Pkg.jl, su gestor de paquetes. Es razonablemente fácil instalar unos pocos paquetes y empezar a usar Julia. Pero leyendo preguntas en Slack y Discourse, muchos usuarios sólo empiezan a entender relativamente tarde lo que hacen comandos como `instantiate`. Este post debería enseñarte cómo puedes ir más allá de un entorno global desordenado y hacia versiones locales bien empaquetadas que te permitan colaborar más eficazmente y hacer que tus resultados sean reproducibles. ## Empezando Cuando instalas Julia por primera vez, no hay entornos. Digamos que has instalado Julia 1.8. Cuando inicies la REPL con el comando `julia`, verás el prompt estándar ```terminal julia> ``` Desde ahí, se accede al modo REPL de Pkg.jl tecleando `]`. El prompt cambiará para indicarlo, mostrando el nombre del entorno activo: ```terminal (@v1.8) pkg> ``` Pero espera, ¿no acabo de decir que todavía no hay entornos? Es cierto, no debería haber ningún archivo de entorno en tu ordenador. Puedes comprobar la carpeta `.julia/environments`, debería estar vacía. Esta carpeta contiene todos tus entornos "compartidos". Puedes decir que un entorno es compartido por el carácter `@` en el prompt del paquete REPL, en nuestro caso `@v1.8` porque usamos Julia 1.8. (Ten en cuenta que si ya tienes una carpeta llamada `v1.8`, o cualquier versión de Julia que estés usando, puedes simplemente renombrarla a `v1.8_deactivated` o algo así y reiniciar Julia para los propósitos de este tutorial). Si se activa un nuevo entorno, esto todavía no crea ningún archivo, y es por eso que no tenemos ningún archivo en `.julia/environments`, todavía. Sólo aparecen una vez que realmente haces algo con tu entorno. Podemos probar esto rápidamente. Activa un nuevo entorno escribiendo `(@v1.8) pkg> activate MyEnvironment`. No verás que se crea ningún fichero nuevo en tu directorio de trabajo, como he dicho esto sólo ocurre una vez que manipulas un entorno. Pero el prompt habrá cambiado: ```terminal (@v1.8) pkg> activate MyEnvironment Activating new project at `~/MyEnvironment` (MyEnvironment) pkg> ``` Volvamos al entorno "principal" `@v1.8` por ahora. El propósito de los entornos compartidos es que puedas activarlos fácilmente desde cualquier directorio de trabajo porque empiezan por `@`, y Pkg sabe que debe buscarlos en `.julia/environments`. Para todos los demás entornos, puedes activarlos por su nombre si te encuentras en el directorio donde se crearon, o tienes que especificar la ruta completa. Así que activamos de nuevo `@v1.8` tecleando: ```terminal (MyEnvironment) pkg> activate @v1.8 Activating new project at `~/.julia/environments/v1.8` (@v1.8) pkg> ``` Como ves, Pkg nos dijo que estábamos activando un nuevo proyecto (otra palabra para entorno), porque como vimos antes, ningún fichero existía realmente, todavía. Hay otro atajo para activar el entorno principal, que es `activate` sin argumento: ```terminal (@v1.8) pkg> activate Activating new project at `~/.julia/environments/v1.8` (@v1.8) pkg> ``` ## Añadir un paquete Vamos a añadir nuestro primer paquete a nuestro entorno compartido `@v1.8`. Para esto, usamos el comando `add`. Elijo el paquete `MacroTools` porque tiene pocas dependencias. ```terminal (@v1.8) pkg> add MacroTools Updating registry at `~/.julia/registries/General.toml` Resolving package versions... Updating `~/.julia/environments/v1.8/Project.toml` [1914dd2f] + MacroTools v0.5.9 Updating `~/.julia/environments/v1.8/Manifest.toml` [1914dd2f] + MacroTools v0.5.9 [2a0f44e3] + Base64 [d6f4376e] + Markdown [9a3f8284] + Random [ea8e919c] + SHA v0.7.0 [9e88b42a] + Serialization ``` Como probablemente sabrás, después de hacer esto, el código de `MacroTools` se habrá descargado en tu sistema y podrás utilizarlo en tu propio código: ```terminal julia> using MacroTools ``` ¿Qué ocurrió realmente cuando ejecutamos el comando `add`? En la primera línea, puedes ver que se ha actualizado el registro general. El registro general ([https://github.com/JuliaRegistries/General](https://github.com/JuliaRegistries/General)) es una lista de paquetes de terceros que están disponibles al público, donde cada paquete lista todas sus dependencias y versiones. Puede haber otros registros, incluso privados, pero el registro general es el principal y el único relevante para la mayoría de los usuarios. Pkg ha actualizado por primera vez esta lista en nuestro ordenador cuando ejecutamos `add MacroTools` para que conozca las versiones más recientes de todos los paquetes del ecosistema. Puedes echarle un vistazo en `.julia/registries/` si quieres. Después de actualizar el registro, Pkg está `Resolviendo versiones de paquetes`. Primero, la última versión de `MacroTools` en el momento de escribir esto, `v0.5.9`, fue añadida a `~/.julia/environments/v1.8/Project.toml`. Así que en este momento, finalmente se está creando el archivo de entorno del que hablaba antes. Podemos ver el contenido de `Project.toml`: ```toml [deps] MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" ``` Así que esto sólo dice que nuestro entorno tiene una dependencia declarada, que es `MacroTools.jl`. La cadena UUID `1914dd2f-81c6...` está ahí porque ese es el identificador único *real* del paquete porque, por ejemplo, si otro hipotético registro tuviera un `MacroTools` diferente, entonces al menos podrías especificar el que realmente quieres a través del UUID. Pkg también actualizó el archivo `~/.julia/environments/v1.8/Manifest.toml`. Echemos un vistazo a este: ```toml # This file is machine-generated - editing it directly is not advised julia_version = "1.8.0-rc3" manifest_format = "2.0" project_hash = "e39ab6d265da4acedccb7411db33219b8d7db4fc" [[deps.Base64]] uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" [[deps.MacroTools]] deps = ["Markdown", "Random"] git-tree-sha1 = "3d3e902b31198a27340d0bf00d6ac452866021cf" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.9" [[deps.Markdown]] deps = ["Base64"] uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" [[deps.Random]] deps = ["SHA", "Serialization"] uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" [[deps.Serialization]] uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" ``` El `Manifest.toml` lista todos los paquetes que se instalaron, mientras que el `Project.toml` sólo lista la dependencia `MacroTools`. Puedes pensar en los dos archivos de esta manera: - `Project.toml`: Lo que quieres. - `Manifest.toml`: Lo que obtienes. El `Project.toml` es siempre el primer fichero que se edita cuando se hacen cambios en el entorno, ya sea a través del Pkg REPL o manualmente. El `Manifest.toml` es entonces el resultado de un cálculo que intenta encontrar versiones compatibles de todos los paquetes especificados en el `Project.toml` y sus dependencias. Ten en cuenta que `Project.toml` puede en principio especificar demandas imposibles, como dos paquetes que requieren dependencias incompatibles. Un `Manifest.toml` sin embargo debe estar siempre en un estado válido, si no se puede resolver una configuración válida de dependencias de paquetes, simplemente obtendrá un error. Podemos ver el grafo de dependencias que Pkg resolvió en el `Manifest.toml`, si nos fijamos en los campos `deps`: ```{dot} digraph G { MacroTools -> Markdown MacroTools -> Random Markdown -> Base64 Random -> SHA Random -> Serialization MacroTools [label="MacroTools v0.5.9", style=filled, fillcolor="slategray2"] SHA [label="SHA v0.7.0", style=filled, fillcolor="tomato"] Markdown [style=filled, fillcolor="tomato"] Random [style=filled, fillcolor="tomato"] Serialization [style=filled, fillcolor="tomato"] Base64 [style=filled, fillcolor="tomato"] } ``` El nodo azul se refiere al paquete externo `MacroTools`, que tiene una versión. Los nodos rojos son bibliotecas estándar. Las bibliotecas estándar se envían con Julia, por lo que normalmente no tienen su propia versión y sólo cambian con cada versión de Julia. Puedes ver todas las bibliotecas estándar en el [repositorio de Julia en GitHub](https://github.com/JuliaLang/julia/tree/master/stdlib). Las bibliotecas estándar pueden depender de otras bibliotecas estándar. SHA es una librería estándar inusual porque está alojada externamente y tiene una versión. Pero la versión sigue siendo fija para cada versión de Julia a través de un archivo `.version` como [el de SHA](https://github.com/JuliaLang/julia/blob/master/stdlib/SHA.version), por lo que la mayoría de los usuarios pueden tratarla como una biblioteca estándar normal. En este ejemplo, MacroTools depende sólo de librerías estándar, pero la mayoría de los otros paquetes dependen también de otros paquetes externos. Puedes comprobar las versiones de las librerías en tu `Project.toml` y `Manifest.toml` con el comando `status` o `st`. Con la opción `-m` puedes ver las entradas de `Manifest.toml`, que pueden ser importantes para comprobar qué dependencias se han resuelto. ```terminal (@v1.8) pkg> st Status `~/.julia/environments/v1.8/Project.toml` [1914dd2f] MacroTools v0.5.9 (@v1.8) pkg> st -m Status `~/.julia/environments/v1.8/Manifest.toml` [1914dd2f] MacroTools v0.5.9 [2a0f44e3] Base64 [d6f4376e] Markdown [9a3f8284] Random [ea8e919c] SHA v0.7.0 [9e88b42a] Serialization (@v1.8) pkg> st -m SHA Status `~/.julia/environments/v1.8/Manifest.toml` [ea8e919c] SHA v0.7.0 ``` ## Números de versión Para entender cómo se resuelven las dependencias, tienes que entender [SemVer versioning](https://semver.org). SemVer significa versionado semántico, lo que significa que los números de versión deben ser significativos, no sólo etiquetas aleatorias. Las tres partes del número de versión son `major`.`minor`.`patch`. En Julia, todas las versiones más recientes con la misma versión principal deben tener APIs públicas compatibles, por lo que el código que funciona con `v1.2.3` también debería funcionar con `v1.20.5`. La excepción es la versión mayor `0`, donde cada nueva versión menor puede considerarse potencialmente rompedora. Así, el código que funciona con `v0.2.4` debería seguir funcionando con `v0.2.13` pero no necesariamente con `v0.3.0`. Esto se debe a que los desarrolladores de paquetes quieren ser capaces de hacer cambios de ruptura incluso si no han llevado su paquete a `v1.0` todavía, una versión que normalmente lleva implícita la estabilidad de la API pública y que a menudo sólo se alcanza después de que el paquete haya existido durante un tiempo. ## Añadir variantes específicas de un paquete Hasta ahora, sólo hemos utilizado el comando `add MacroTools`, que trajo la última versión `v0.5.9` a nuestro entorno. A veces, sin embargo, quieres una variante específica de un paquete. No tiene que ser necesariamente una versión específica. También puede ser un commit o una rama de un determinado repositorio. Vamos a probar esto con `MacroTools`. Podemos instalar la versión `v0.5.1` utilizando la sintaxis `@`: ```terminal (@v1.8) pkg> add MacroTools@0.5.1 Resolving package versions... Updating `~/.julia/environments/v1.8/Project.toml` ⌃ [1914dd2f] ↓ MacroTools v0.5.9 ⇒ v0.5.1 Updating `~/.julia/environments/v1.8/Manifest.toml` ⌅ [00ebfdb7] + CSTParser v2.5.0 ⌅ [34da2185] + Compat v2.2.1 ⌅ [864edb3b] + DataStructures v0.17.20 ⌃ [1914dd2f] ↓ MacroTools v0.5.9 ⇒ v0.5.1 [bac558e1] + OrderedCollections v1.4.1 [0796e94c] + Tokenize v0.5.24 [0dad84c5] + ArgTools v1.1.1 [56f22d72] + Artifacts [ade2ca70] + Dates [8bb1440f] + DelimitedFiles [8ba89e20] + Distributed [f43a241f] + Downloads v1.6.0 [7b1f6079] + FileWatching [b77e0a4c] + InteractiveUtils [b27032c2] + LibCURL v0.6.3 [76f85450] + LibGit2 [8f399da3] + Libdl [37e2e46d] + LinearAlgebra [56ddb016] + Logging [a63ad114] + Mmap [ca575930] + NetworkOptions v1.2.0 [44cfe95a] + Pkg v1.8.0 [de0858da] + Printf [3fa0cd96] + REPL [1a1011a3] + SharedArrays [6462fe0b] + Sockets [2f01184e] + SparseArrays [10745b16] + Statistics [fa267f1f] + TOML v1.0.0 [a4e569a6] + Tar v1.10.0 [8dfed614] + Test [cf7118a7] + UUIDs [4ec0a83e] + Unicode [e66e0078] + CompilerSupportLibraries_jll v0.5.2+0 [deac9b47] + LibCURL_jll v7.83.1+1 [29816b5a] + LibSSH2_jll v1.10.2+0 [c8ffd9c3] + MbedTLS_jll v2.28.0+0 [14a3606d] + MozillaCACerts_jll v2022.2.1 [4536629a] + OpenBLAS_jll v0.3.20+0 [83775a58] + Zlib_jll v1.2.12+3 [8e850b90] + libblastrampoline_jll v5.1.1+0 [8e850ede] + nghttp2_jll v1.47.0+0 [3f19e933] + p7zip_jll v17.4.0+0 Info Packages marked with ⌃ and ⌅ have new versions available, but those with ⌅ cannot be upgraded. To see why use `status --outdated -m` ``` Puedes ver que tenemos una tonelada de nuevas dependencias. Esto ocurrió porque `MacroTools` consiguió reducir mucho el número de paquetes de los que depende con el tiempo, por lo que la versión anterior tira de mucho más. Si echamos un vistazo a la nueva entrada `Manifest.toml` para `MacroTools`, vemos: ```toml [[deps.MacroTools]] deps = ["CSTParser", "Compat", "DataStructures", "Test", "Tokenize"] git-tree-sha1 = "d6e9dedb8c92c3465575442da456aec15a89ff76" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.1" ``` Esto demuestra que incluso entre versiones de parches de un paquete, que deberían seguir la misma API pública, las dependencias pueden cambiar mucho. Si miras `Project.toml`, verás que no ha cambiado. El requisito de versión sólo se aplicó durante la resolución de esta dependencia, y no se recordará ni se aplicará de nuevo en futuras operaciones de Pkg. Vamos a probar otra sintaxis, que es la de elegir una confirmación o rama específica de un repositorio. En este caso, usamos el commit `639d1a6`, pero también podríamos usar algo como `master` para obtener el último commit de esa rama. ```terminal (@v1.8) pkg> add MacroTools#639d1a6 Resolving package versions... Updating `~/.julia/environments/v1.8/Project.toml` [1914dd2f] ~ MacroTools v0.5.1 ⇒ v0.5.9 `https://github.com/FluxML/MacroTools.jl.git#639d1a6` Updating `~/.julia/environments/v1.8/Manifest.toml` [00ebfdb7] - CSTParser v2.5.0 [34da2185] - Compat v2.2.1 [864edb3b] - DataStructures v0.17.20 [1914dd2f] ~ MacroTools v0.5.1 ⇒ v0.5.9 `https://github.com/FluxML/MacroTools.jl.git#639d1a6` [bac558e1] - OrderedCollections v1.4.1 [0796e94c] - Tokenize v0.5.24 [0dad84c5] - ArgTools v1.1.1 [56f22d72] - Artifacts [ade2ca70] - Dates [8bb1440f] - DelimitedFiles [8ba89e20] - Distributed [f43a241f] - Downloads v1.6.0 [7b1f6079] - FileWatching [b77e0a4c] - InteractiveUtils [b27032c2] - LibCURL v0.6.3 [76f85450] - LibGit2 [8f399da3] - Libdl [37e2e46d] - LinearAlgebra [56ddb016] - Logging [a63ad114] - Mmap [ca575930] - NetworkOptions v1.2.0 [44cfe95a] - Pkg v1.8.0 [de0858da] - Printf [3fa0cd96] - REPL [1a1011a3] - SharedArrays [6462fe0b] - Sockets [2f01184e] - SparseArrays [10745b16] - Statistics [fa267f1f] - TOML v1.0.0 [a4e569a6] - Tar v1.10.0 [8dfed614] - Test [cf7118a7] - UUIDs [4ec0a83e] - Unicode [e66e0078] - CompilerSupportLibraries_jll v0.5.2+0 [deac9b47] - LibCURL_jll v7.83.1+1 [29816b5a] - LibSSH2_jll v1.10.2+0 [c8ffd9c3] - MbedTLS_jll v2.28.0+0 [14a3606d] - MozillaCACerts_jll v2022.2.1 [4536629a] - OpenBLAS_jll v0.3.20+0 [83775a58] - Zlib_jll v1.2.12+3 [8e850b90] - libblastrampoline_jll v5.1.1+0 [8e850ede] - nghttp2_jll v1.47.0+0 [3f19e933] - p7zip_jll v17.4.0+0 ``` Ten en cuenta que aunque la versión impresa para MacroTools es de nuevo `0.5.9`, esto no significa necesariamente que estemos en el mismo commit que el apuntado por la versión `0.5.9` en el registro. Sólo significa que Pkg clonó el repositorio, comprobó el commit con el hash `639d1a6` y encontró el especificador de versión `0.5.9` en el propio fichero `Project.toml` de MacroTools. Por lo tanto, infinitas versiones de código de un paquete pueden ser tratadas como la versión `X.Y.Z` por Pkg, pero `X.Y.Z` sólo se refiere a exactamente una versión. Es importante recordar esta distinción al desarrollar y editar un paquete, pero hablaremos de ello más adelante. Si echamos otro vistazo a nuestro `Manifest.toml`, podemos ver la siguiente entrada para MacroTools: ```toml [[deps.MacroTools]] deps = ["Markdown", "Random"] git-tree-sha1 = "465a4803356bcb11f6eb97df992680f13a9ba776" repo-rev = "639d1a6" repo-url = "https://github.com/FluxML/MacroTools.jl.git" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" version = "0.5.9" ``` Esta vez, se registró la URL del repositorio, así como la revisión, que fue `639d1a6`. Esto se debe a que tan pronto como especifica una revisión en el comando `add`, Pkg sabe que está operando fuera del registro, por lo que no puede confiar en la información del repositorio almacenada allí para MacroTools. (Ten en cuenta que también puedes instalar paquetes no registrados, o bifurcaciones de paquetes registrados de esta manera, haciendo `add https://the_url_to_the_git_repository`). El manifiesto necesita almacenar la url y la revisión para que el proyecto pueda ser reproducido por otra persona. Veamos cómo es reproducir un entorno. ## Reproducir un entorno Digamos que has escrito un código que depende de la versión específica de `MacroTools` que añadimos mediante `add MacroTools#639d1a6` y quieres que tu colega pueda ejecutar ese código con los paquetes exactos instalados que usamos en su momento. ¿Qué ficheros se necesitan para reproducir el estado de tu entorno? No sólo `Project.toml` contiene esta información: ```julia [deps] MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" ``` Necesitamos enviar también el `Manifest.toml` porque registra las versiones exactas de todos los paquetes. Imaginemos que somos nuestro colega, y que acabamos de recibir un archivo `Project.toml` y `Manifest.toml`. ¿Cómo conseguimos instalar el entorno? Vamos a copiar los archivos en una nueva carpeta que llamaremos `ColleagueEnv` en nuestro directorio de trabajo actual. Para simular que somos el colega que acaba de usar Julia por primera vez, también borramos la carpeta `.julia/packages/MacroTools` en la que estaba almacenado el código fuente descargado de `MacroTools`. Ten en cuenta que esto significa que el código fuente no forma parte de un entorno, sino que se almacena de forma centralizada. Sería bastante derrochador descargar los mismos fuentes una y otra vez sólo porque estás utilizando diferentes entornos locales. Ahora reiniciemos Julia y activemos el entorno `ColleagueEnv`: ```terminal (@v1.8) pkg> activate ./ColleagueEnv Activating project at `~/ColleagueEnv` ``` Podemos comprobar los paquetes instalados mediante `st -m`: ```terminal (ColleagueEnv) pkg> st -m Status `~/ColleagueEnv/Manifest.toml` → [1914dd2f] MacroTools v0.5.9 `https://github.com/FluxML/MacroTools.jl.git#master` [2a0f44e3] Base64 [d6f4376e] Markdown [9a3f8284] Random [ea8e919c] SHA v0.7.0 [9e88b42a] Serialization Info Packages marked with → are not downloaded, use `instantiate` to download ``` Si ahora intentáramos ejecutar un código utilizando `MacroTools`, ocurriría lo siguiente: ```terminal julia> using MacroTools ERROR: ArgumentError: Package MacroTools [1914dd2f-81c6-5fcd-8719-6d5c9610ff09] is required but does not seem to be installed: - Run `Pkg.instantiate()` to install all recorded dependencies. Stacktrace: [1] _require(pkg::Base.PkgId) @ Base ./loading.jl:1306 [2] _require_prelocked(uuidkey::Base.PkgId) @ Base ./loading.jl:1200 [3] macro expansion @ ./loading.jl:1180 [inlined] [4] macro expansion @ ./lock.jl:223 [inlined] [5] require(into::Module, mod::Symbol) @ Base ./loading.jl:1144 ``` Así que tenemos que seguir el consejo ya impreso dos veces para nosotros, y llamar a `instantiate`. Esto descargará todo lo especificado en el `Manifest.toml` exactamente como se registró allí. De hecho, puedes estar seguro de que es exactamente lo mismo porque el `Manifest.toml` almacena los hashes del árbol git de cada dependencia. A menos que alguien borre estas partes específicas del repositorio, podrás descargar el código fuente exactamente como estaba: ```terminal (ColleagueEnv) pkg> instantiate Precompiling project... ✓ MacroTools 1 dependency successfully precompiled in 1 seconds (ColleagueEnv) pkg> st -m Status `~/ColleagueEnv/Manifest.toml` [1914dd2f] MacroTools v0.5.9 `https://github.com/FluxML/MacroTools.jl.git#master` [2a0f44e3] Base64 [d6f4376e] Markdown [9a3f8284] Random [ea8e919c] SHA v0.7.0 [9e88b42a] Serialization ``` Ahora ya no recibimos una advertencia, nuestras dependencias se han descargado correctamente. Encontrarás `MacroTools` descargado en la carpeta `.julia/packages` de nuevo. ## Paquetes y entornos Los paquetes y los entornos normales son bastante similares. Cada paquete debe tener un `Project.toml` que especifique su nombre, UUID, versión y dependencias. La forma más fácil de hacer un paquete para probar esto, es usar el comando `generate` del Pkg REPL. Reiniciemos Julia y eliminemos `MacroTools` de nuestro entorno principal para que quede vacío: ```terminal (@v1.8) pkg> rm MacroTools Updating `~/.julia/environments/v1.8/Project.toml` [1914dd2f] - MacroTools v0.5.9 `https://github.com/FluxML/MacroTools.jl.git#639d1a6` Updating `~/.julia/environments/v1.8/Manifest.toml` [1914dd2f] - MacroTools v0.5.9 `https://github.com/FluxML/MacroTools.jl.git#639d1a6` [2a0f44e3] - Base64 [d6f4376e] - Markdown [9a3f8284] - Random [ea8e919c] - SHA v0.7.0 [9e88b42a] - Serialization ``` Ahora, generamos un nuevo paquete llamado `MyPackage`: ```terminal (@v1.8) pkg> generate MyPackage Generating project MyPackage: MyPackage/Project.toml MyPackage/src/MyPackage.jl ``` Como puede ver, se generó un archivo `Project.toml` en el directorio `MyPackage`. Echemos un vistazo a este: ```toml name = "MyPackage" uuid = "025f59cc-7e1c-467d-8f56-70157e1cbbbb" authors = ["Your Name "] version = "0.1.0" ``` La única diferencia de un entorno de paquete básico a un entorno normal son esos cuatro campos. Si queremos usar `MacroTools` en nuestro paquete, podemos añadirlo manualmente a una sección `deps` en el `Project.toml`, o usamos el Pkg REPL. Para ello, primero activamos el paquete como entorno, y luego añadimos `MacroTools`. ```terminal (@v1.8) pkg> activate MyPackage/ Activating project at `~/MyPackage` (MyPackage) pkg> add MacroTools Updating registry at `~/.julia/registries/General.toml` Resolving package versions... Installed MacroTools ─ v0.5.9 Updating `~/MyPackage/Project.toml` [1914dd2f] + MacroTools v0.5.9 Updating `~/MyPackage/Manifest.toml` [1914dd2f] + MacroTools v0.5.9 [2a0f44e3] + Base64 [d6f4376e] + Markdown [9a3f8284] + Random [ea8e919c] + SHA v0.7.0 [9e88b42a] + Serialization Precompiling project... ✓ MacroTools ✓ MyPackage 2 dependencies successfully precompiled in 1 seconds ``` El `Project.toml` ahora tiene este aspecto: ```toml name = "MyPackage" uuid = "025f59cc-7e1c-467d-8f56-70157e1cbbbb" authors = ["Your Name "] version = "0.1.0" [deps] MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" ``` En el fichero `MyPackage/src/MyPackage.jl`, ya podemos importar `MacroTools` y trabajar con él. Vamos a cambiar el contenido de ese archivo a: ```julia module MyPackage import MacroTools function test() MacroTools.@capture :(1 + 2) x_ + y_ @show x @show y return end end # module MyPackage ``` Ahora podemos `importar` o `usar` nuestro paquete y verificar que `MacroTools` puede ser usado por su código fuente: ```terminal julia> using MyPackage julia> MyPackage.test() x = 1 y = 2 ``` ¡Funcionó! ## El comando `develop` Reiniciemos Julia ahora, lo que nos devolverá al entorno principal. Intentemos de nuevo importar nuestro paquete: ```terminal julia> using MyPackage ERROR: ArgumentError: Package MyPackage not found in current path. - Run `import Pkg; Pkg.add("MyPackage")` to install the MyPackage package. Stacktrace: [1] macro expansion @ ./loading.jl:1163 [inlined] [2] macro expansion @ ./lock.jl:223 [inlined] [3] require(into::Module, mod::Symbol) @ Base ./loading.jl:1144 ``` Esto no funciona, porque nuestro entorno principal no tiene `MyPackage` instalado. Puedes usar `MyPackage` siempre que tengas activado su propio entorno, pero fuera de él no es visible. Podemos cambiar eso usando el comando `develop` o `dev` del Pkg REPL, que instalará y rastreará `MyPackage`: ```terminal (@v1.8) pkg> dev ./MyPackage/ Resolving package versions... Updating `~/.julia/environments/v1.8/Project.toml` [025f59cc] + MyPackage v0.1.0 `../../../MyPackage` Updating `~/.julia/environments/v1.8/Manifest.toml` [1914dd2f] + MacroTools v0.5.9 [025f59cc] + MyPackage v0.1.0 `../../../MyPackage` [2a0f44e3] + Base64 [d6f4376e] + Markdown [9a3f8284] + Random [ea8e919c] + SHA v0.7.0 [9e88b42a] + Serialization ``` Genial, ha funcionado. Ahora podemos acceder a `MyPackage` de nuevo: ```terminal julia> using MyPackage [ Info: Precompiling MyPackage [025f59cc-7e1c-467d-8f56-70157e1cbbbb] julia> MyPackage.test() x = 1 y = 2 ``` ¿Por qué querríamos hacer esto, usar un entorno separado para acceder al entorno de nuestro paquete? Es porque probablemente queramos utilizar otros paquetes que no deberían ser dependencias de `MyPackage` mientras lo desarrollamos. Por ejemplo, un paquete de análisis de datos podría desarrollarse mientras se utiliza un paquete como [RDatasets.jl](https://github.com/JuliaStats/RDatasets.jl) que suministra conjuntos de datos de prueba. ## Límites de compatibilidad Actualmente, `MyPackage` no especifica las versiones de `MacroTools` con las que es compatible. Esto no suele ser una buena idea, de hecho, el registro general no permite registrar paquetes que no especifiquen los límites superiores de compatibilidad para cada dependencia. Eso tiene sentido porque no sabes si tu paquete será compatible con todas las versiones futuras de tus dependencias, así que tiene sentido poner un tope a la compatibilidad hasta el punto en que hayas comprobado que todo funciona. Imaginemos que probamos nuestro paquete sólo hasta la versión `0.5.8` de `MacroTools`. Podemos escribir este requisito en el `Project.toml` de `MyPackage`: ```toml name = "MyPackage" uuid = "025f59cc-7e1c-467d-8f56-70157e1cbbbb" authors = ["Your Name "] version = "0.1.0" [deps] MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" [compat] MacroTools = "<0.5.9" ``` Pero este cambio no es recogido automáticamente por el entorno `@v1.8`. Para volver a calcular el gráfico de dependencias y desechar el antiguo `Manifest.toml`, podemos ejecutar `update` o `up`: ```terminal (@v1.8) pkg> up Updating registry at `~/.julia/registries/General.toml` Installed MacroTools ─ v0.5.8 No Changes to `~/.julia/environments/v1.8/Project.toml` Updating `~/.julia/environments/v1.8/Manifest.toml` ⌃ [1914dd2f] ↓ MacroTools v0.5.9 ⇒ v0.5.8 Info Packages marked with ⌃ have new versions available Precompiling project... ✓ MacroTools ✓ MyPackage 2 dependencies successfully precompiled in 2 seconds ``` Como puede ver, el paquete MacroTools se ha actualizado correctamente a `v0.5.8`. ¿Qué pasaría si ahora intentáramos instalar `v0.5.9` en el entorno principal? ```terminal (@v1.8) pkg> add MacroTools@0.5.9 Resolving package versions... ERROR: Unsatisfiable requirements detected for package MacroTools [1914dd2f]: MacroTools [1914dd2f] log: ├─possible versions are: 0.4.3-0.5.9 or uninstalled ├─restricted to versions 0.0.0-0.5.8 by MyPackage [025f59cc], leaving only versions 0.4.3-0.5.8 │ └─MyPackage [025f59cc] log: │ ├─possible versions are: 0.1.0 or uninstalled │ └─MyPackage [025f59cc] is fixed to version 0.1.0 └─restricted to versions 0.5.9 by an explicit requirement — no versions left ``` Se produce un conflicto de compatibilidad de versiones. No es posible conciliar el requisito de añadir `MacroTools` `0.5.9` con el hecho de que sólo se permite llegar hasta `0.5.8` para `MyPackage`. Los conflictos de compatibilidad son la principal razón por la que deberías dar a cada uno de tus proyectos su propio entorno local en lugar de usar siempre el global. En primer lugar, cuantos más paquetes instales es cada vez más probable que acabes con versiones antiguas o conflictos de compatibilidad. En segundo lugar, si inadvertidamente actualizas tu entorno principal para el proyecto B, pero más tarde vuelves al proyecto A, podría muy bien ser que las nuevas versiones de los paquetes sean ahora incompatibles con el código que escribiste para A en su momento. No querrás enredar todos tus proyectos juntos, así que simplemente crea entornos separados para cada uno. ## Diferencias entre `add` y `develop` En nuestros ejemplos, hemos visto los comandos `add` y `develop` cuando se trata de paquetes que queremos instalar. Tanto `add` como `dev` pueden utilizarse para incluir paquetes en su entorno. Ambos pueden tomar como fuentes una ruta local, o el nombre de un paquete registrado, o un enlace a un repositorio. Entonces, ¿por qué tenemos dos comandos separados pero similares? La diferencia es que `add` trata los paquetes como recursos fijos de sólo lectura. Cuando `add` un paquete, no importa si es local o remoto, cada versión es copiada a una carpeta Julia interna donde no debe ser modificada nunca más. Cuando `dev` un paquete local, se queda donde está y puede ser modificado por ti. Sólo tienes que acordarte de reiniciar Julia para cargar cualquier cambio que hagas (a menos que puedan ser auto-recargados por [Revise.jl](https://github.com/timholy/Revise.jl), que deberías revisar si aún no lo has visto), y `update` el entorno en caso de que hagas cambios en el `Project.toml` como nuevas dependencias o límites de compatibilidad. Cuando se utiliza `dev`, se dice que Julia *rastrea* ese paquete. Como se espera que hagas cambios en el código cuando usas `dev`, Julia copia los paquetes que `desarrollas` por primera vez especificando su nombre o URL (por ejemplo `dev MacroTools` o `dev https://github.com/FluxML/MacroTools.jl`) en una carpeta especial en `.julia/dev`. Aquí, puedes acceder a ellos con el editor de tu elección, hacer cambios y sincronizarlos con GitHub u otros sistemas de control de versiones. Ten en cuenta que si haces `dev MacroTools` en un entorno local, y más tarde `dev MacroTools` en un entorno diferente, Julia por defecto volverá a utilizar el mismo repositorio en `.julia/dev`. Si el primer entorno ya era bastante antiguo y no has realizado los nuevos cambios en un tiempo, ¡probablemente te sorprenderás de obtener esa versión antigua cuando ejecutes `dev` en el nuevo entorno! Por lo tanto, puede tener sentido usar copias locales separadas de los paquetes en los que quieras trabajar si prevés que vas a trabajar en ellos en varios contextos diferentes. Para ello, puedes clonar un repositorio en una carpeta local, por ejemplo con `git clone https://github.com/FluxML/MacroTools.jl` y luego ejecutar `dev the_local_folder`. La otra opción es ejecutar `dev --local MacroTools`, que copia el paquete desarrollado en el directorio de trabajo, no en `.julia/dev`. ## Entornos apilados Hay otro comportamiento de los entornos que es potencialmente muy confuso para los principiantes, y se llama *entornos apilados*. Vamos a reiniciar Julia de nuevo, y añadir el paquete `Infiltrator` al entorno principal: ```terminal (@v1.8) pkg> add Infiltrator Updating registry at `~/.julia/registries/General.toml` Resolving package versions... Updating `~/.julia/environments/v1.8/Project.toml` [5903a43b] + Infiltrator v1.6.1 Updating `~/.julia/environments/v1.8/Manifest.toml` [5903a43b] + Infiltrator v1.6.1 [b77e0a4c] + InteractiveUtils [3fa0cd96] + REPL [6462fe0b] + Sockets [cf7118a7] + UUIDs [4ec0a83e] + Unicode ``` Ahora activamos el entorno de `MyPackage`: ```terminal (@v1.8) pkg> activate MyPackage Activating project at `~/MyPackage` ``` Veamos qué ocurre si cargamos `Infiltrator`: ```terminal julia> using Infiltrator ``` Ha funcionado. ¿Pero por qué? `Infiltrator` no está disponible en el Project.toml de `MyPackage`. La razón es que Julia puede importar paquetes de múltiples entornos al mismo tiempo. Esto depende de la variable global `LOAD_PATH`. Echemos un vistazo: ```terminal julia> LOAD_PATH 3-element Vector{String}: "@" "@v#.#" "@stdlib" ``` La primera entrada, `"@"`, significa *entorno activo*, por lo que el primer lugar donde Julia buscó `Infiltrator` fue en el entorno de `MyPackage`, donde no lo encontró. La segunda entrada, `"@v#.#"` significa *el entorno compartido de esta versión de Julia*, en nuestro caso es `@v1.8`. Por eso sigo llamando a esto el entorno *principal*, no sólo porque es el predeterminado, sino también porque siempre está disponible por defecto debido a la configuración `LOAD_PATH`. Aquí es donde Julia encontró `Infiltrator` y lo cargó. La tercera entrada, `"@stdlib"`, se refiere a la lista de bibliotecas estándar que pertenece a la versión actual de Julia. Esta es la razón por la que podemos hacer `using Statistics` o `using REPL` en una nueva sesión de Julia, incluso si nuestro entorno principal está vacío. Una confusión potencial proviene del hecho de que un usuario puede activar un entorno de paquete y trabajar allí bajo la suposición de que sólo las dependencias del paquete están disponibles para importar. Esto podría ocultar el hecho de que algunos de los paquetes incluidos se extraen del entorno principal, lo que dará lugar a un error más tarde, si el paquete se instala en un nuevo entorno y ya no puede acceder a la dependencia. Y hay otro caso aún más complicado. Digamos que desarrollas `DevelopingPackage` que depende de `HelperDependency` en la versión `3.0`. Pero para depurar, también has instalado `DebugPackage` en tu entorno principal, que también depende de `HelperDependency`, pero con la versión `2.0`. Ahora, el `Manifest.toml` del entorno principal listará `HelperDependency v2.0` y tu entorno de desarrollo de paquetes listará `HelperDependency v3.0`. Si arrancas Julia en el entorno principal y escribes `using DebugPackage`, Julia también cargará `HelperDependency` en la versión `2.0` en segundo plano. Ahora, cambia de entorno y escribe `using DevelopingPackage`. Julia intentará ahora cargar `DevelopingPackage` pero no puede cargar `HelperDependency v3.0` porque `v2.0` ya está cargada. Ahora obtendrás un comportamiento indefinido, porque las versiones de los paquetes pueden ser lo suficientemente similares como para que no notes nada, pero pueden ser tan diferentes que obtengas `UndefVarErrors` o fallos sutiles en el comportamiento. Debido a estos problemas comunes, la mejor práctica es utilizar el entorno principal con moderación. `Infiltrator` es un buen ejemplo de un paquete que probablemente puede instalar allí sin problemas. Es un paquete de depuración y rara vez se necesita como dependencia de otro paquete, y no tiene dependencias de bibliotecas no estándar, lo que significa que no hay un problema de carga de dependencia mencionado anteriormente. Por otro lado es muy útil tenerlo disponible sin mayor esfuerzo al desarrollar, por lo que puede ser una cuestión de conveniencia. Si realmente quieres asegurarte de que sólo los paquetes de tu proyecto actualmente activado son accesibles, puedes eliminar otras entradas de la ruta de carga. Para demostrarlo, si vaciamos la ruta de carga no podremos cargar ningún paquete, ni `Infiltrator` de `@v1.8`, ni `MyPackage` de nuestro entorno activo, ni la librería estándar `Statistics`: ```terminal julia> empty!(LOAD_PATH) String[] julia> using Infiltrator │ Package Infiltrator not found, but a package named Infiltrator is available │ from a registry. │ Install package? │ (MyPackage) pkg> add Infiltrator └ (y/n/o) [y]: ERROR: ArgumentError: Package Infiltrator not found in current path, maybe you meant `import/using .Infiltrator`. - Otherwise, run `import Pkg; Pkg.add("Infiltrator")` to install the Infiltrator package. julia> using MyPackage ERROR: ArgumentError: Package MyPackage not found in current path. - Run `import Pkg; Pkg.add("MyPackage")` to install the MyPackage package. julia> using Statistics ERROR: ArgumentError: Package Statistics not found in current path. - Run `import Pkg; Pkg.add("Statistics")` to install the Statistics package. ``` ## Entornos temporales Una cosa que es útil para pruebas rápidas son los entornos temporales. Si lees acerca de un nuevo paquete y quieres probarlo rápidamente, pero no quieres desordenar tu entorno principal o abarrotar tu directorio de trabajo con archivos de entorno, puedes usar `activate --temp`: ```terminal (v1.8) pkg> activate --temp Activating new project at `/var/folders/z5/r5q6djwn5g10k3w279bn37700000gn/T/jl_e4klnB` (jl_e4klnB) pkg> ``` Este entorno sólo existirá hasta que el proceso Julia salga, por lo que es perfecto para ejecutar algo una vez y luego olvidarse de ello. ## Conclusión Este ha sido un breve recorrido por `Pkg` y sus principales funciones. Espero que haya quedado más claro cómo funcionan los entornos y por qué no deberías confiar en un único entorno global. Es bastante fácil crear un entorno por proyecto y los archivos de texto creados no ocupan espacio, así que no hay inconveniente en hacerlo. Para más información, puedes consultar algunas de estas fuentes: - [Pkg.jl documentation](https://pkgdocs.julialang.org/v1/). - [DrWatson.jl](https://juliadynamics.github.io/DrWatson.jl/stable/), una herramienta que intenta facilitar el proceso de creación de proyectos reproducibles. - [TestEnv.jl](https://github.com/JuliaTesting/TestEnv.jl), que ayuda con el problema de los entornos apilados en el contexto de las pruebas (no tratado aquí). - [PkgTemplates.jl](https://github.com/invenia/PkgTemplates.jl), que facilita la creación de paquetes.