Introduction
At some point you might want to split your work from a single monolithic project into a series of independent and smaller projects.
For example, you might want to have a folder somewhere in your system called “MyPackages” or something similar, that contains a series of folders, each one for a different packages. Also, each one of your packages could contain a different git repo, or all of them could live within a single mono-repo.
That’s actually the easy part, as it just involves moving some files around.
But how can you now interact with your code? You need to tell Julia that these projects exists, or otherwise Julia won’t know what to do.
So let’s dig into this.
For simplicity, let’s assume we only have two projects: myLib
and myApp
, and myApp
depends on myLib
.
Basic workflow with just include
If you are lazy, just include myLib
into myApp
with a long include
command, that could look like this:
myBase = "/home/martin/Desktop/MyPackages/"
include(myBase * "myLib/src/myLib.jl")
Or, alternatively, use a relative path like
include("../myLib.jl")
This will include all the code in myLib.jl
into your current project.
However, even if myLib.jl defines a module and exports, exports won’t work properly, and inside myApp you will need to do myLib.foo()
.
In order to get the exports right, you might also want to do:
using .myLib
Now this is fine, this will work with Revise and allow you to change things quickly.
This is perfectly fine as a first step towards better organizing your code. All the code from myLib
will be included into myApp
.
Limitations of just including everthing
Now, is there any problem with this approach? Are there any limitations of simply including all the code?
Actually, this is actually the same way that a C or C++ programmer would work, using their #include
statement. So it’s nothing to be terrible ashamed of.
The only problem with this is that you might be missing out on some modern tooling. For example, older languages like C/C++ and even Python gave rise to a notion called “dependency hell”, something unheard of in Julia — where the only “problem” we have is managing to understanding how a very well designed tool like Pkg actually works for various scenarios.
For example, let’s say that you stop developing myApp
for a while, and when you go back to it, it turns out that you made some breaking changes to myLib
and now myApp
doesn’t work. If you want to be able to run myApp
inmmediately without any refactoring, you would need to get the exact version of myLib
that you were using before.
This is something you could easily accompish if you were using Pkg: just add [email protected]
and you’re set! Then you can slowly work up to the new version of myLib
without losing access to your existing functionality.
What about testing in this setting? Let’s say that you continue developing myLib
and some tests of new functionality begin to fail. Now the tests in myApp
which don’t rely on the new functionality will fail, too, as automated testing is a feature of Pkg
.
What about sharing code with others, and integrating their changes? Everything is doable, but just more difficult without Pkg
.
Basic workflows with local packages
Ok, for some projects where we intend to make changes every now and then, or share code with others who might in turn make changes, getting some help from the package managers sounds like a good deal.
We will then need to learn how to use it with local packages.
Option 1: add
your local package
If LibA
is it’s own Git repo, we can then add it as an unregistered package in the same way that we would add an unregistered package hosted on Github, for example. Just give Pkg
a local path where you have a Git repo.
Remeber this could be an absolute or a relative path. Assuming myApp
also lives in the myPackages
folder, you can do something like:
]add ../myLib
Now, what happens if you change something in myLib
? The changes won’t be reflected immediately on myApp
. You would need to commit the changes on myLib
repo, and then update myLib
from myApp
.
So, this approach will only work for code that you are not expecting to change quickly.
Option 2: dev
your local package
Now, this is similar to the previous approach
]dev ../myLib
Should you push your “MyPackages” folder to your LOAD_PATH
?
You might get bored of having to deal with abosolute or relative paths, and might want to solve this issue once and for all. So you might have come accross LOAD_PATH
.
Yes, you can do:
myBase = "/home/martin/Desktop/MyPackages/"
push!(LOAD_PATH, myBase)
Congratulations, you can now directly do
using myLib
without any need for add
or dev
. And it will work if you need to call a function in myLib
directly from the REPL.
However, this doesn’t solve any issues with dependencies.
As soon as you need to precompile myApp
(which depends on myLib
) this will fail, as you’ll get an unresolved reference. It turns out that, while Julia can find myLib
, Pkg
cannot… unless you do run Pkg.add
.
However, things aren’t so easy, as Pkg.add
won’t work because it searches for a registry…
Creating a local registry
So, let’s say that you want to go a step further, and be able to use your local packages in the same way that public registered packages. The way to do this is by setting up a local registry. This could be useful to store propietary code within an organization, or to set up an internal server in order to get access to packages without an internet connection.
In order to do this, the basic package you might want to look into is LocalRegistry.jl.
Conclusion
Organizing your work into local packages can seem intimidating at first. While you might be tempted to use the simplest possible workflow (just include everything) investing a little time in learning new tools that integrate well with git and let you collaborate with more people will eventually pay off.