How to Create a Julia Package from Scratch

How to Create a Julia Package from Scratch

Master the tools and techniques to create a Julia package.

This post was written by Steven Whitaker.

The Julia programming language is a high-level language that is known, at least in part, for its excellent package manager and outstanding composability. (See another blog post that illustrates this composability.)

Julia makes it super easy for anybody to create their own package. Julia's package manager enables easy development and testing of packages. The ease of package development encourages developers to split reusable chunks of code into individual packages, further enhancing Julia's composability.

In this post, we will learn what comprises a Julia package. We will also discuss tools that automate the creation of packages. Finally, we will talk about the basics of package development and walk through how to publish (register) a package for others to use.

This post assumes you are comfortable navigating the Julia REPL. If you need a refresher, check out our post on the Julia REPL.

Components of a Package

Packages are easy enough to use: just install them with add PkgName in the package prompt and then run using PkgName in the julia prompt. But what actually goes into a package?

Packages must follow a specific directory structure and include certain information to be recognized as a package by Julia.

Suppose we are creating a package called PracticePackage.jl. First, we create a directory called PracticePackage. This directory is the package root. Within the root directory we need a file called Project.toml and another directory called src.

The Project.toml requires the following information:

name = "PracticePackage"
uuid = "11111111-2222-3333-aaaa-bbbbbbbbbbbb"
authors = ["Your Name <youremail@email.com>"]
version = "0.1.0"
  • uuid stands for universally unique identifier, and can be generated in Julia with using UUIDs; uuid4(). The purpose of a UUID is to allow different packages of the same name to coexist.
  • version should be set to whatever version is appropriate for your package, typically "0.1.0" or "1.0.0" for an initial release. The versioning of Julia packages follows SemVer.
  • The Project.toml will also include information about package dependencies, but more on that later.

The src directory requires one Julia file named PracticePackage.jl that defines a module named PracticePackage:

module PracticePackage

# Package code goes here.

end

So, the directory structure of the package looks like the following:

PracticePackage
├── Project.toml
└── src
    └── PracticePackage.jl

And that's all there is to a package! (Well, at least minimally.)

Some Technicalities

Feel free to skip this section, but if you are curious about some technicalities for what comprises a valid package, read on.

  • The Project.toml only needs the name and uuid fields for Julia to recognize the package. Without the version field, Julia treats the version as v0.0.0.
    • However, the version and authors fields are needed to register the package.
  • The name of the package root directory doesn't matter, meaning it doesn't have to match the package name. However, the name field in Project.toml does have to match the name of the module defined in src/PracticePackage.jl, and the file name of src/PracticePackage.jl also has to match.
    • For example, we could change the name of the package by setting name = "Oops" in Project.toml, renaming src/PracticePackage.jl to src/Oops.jl, and defining module Oops in that file. We would not have to rename the package root directory from PracticePackage to Oops (though that would be a good idea to avoid confusion).

Automatically Generating Packages

The basic structure of a package is pretty simple, so there ought to be a way to automate it, right? (I mean, who wants to manually generate a UUID?) Good news: package creation can be automated!

Package generate Command

Julia comes with a generate package command built-in. First, change directories to where the package root directory should live, then run generate in the Julia package prompt:

pkg> generate PracticePackage

This command creates the package root directory PracticePackage and the Project.toml and src/PracticePackage.jl files. Some notes:

  • The Project.toml is pre-filled with the correct fields and values, including an automatically generated UUID. When I ran generate on my computer, it also pre-filled the authors field with my name and email from my ~/.gitconfig file.
  • src/PracticePackage.jl is pre-filled with a definition for the module PracticePackage. It also defines a function greet in the module, but typically you will replace that with your own code.

PkgTemplates.jl

The generate command works fine, but it's barebones. For example, if you are planning on hosting your package on GitHub, you might want to include a GitHub Action for continuous integration (CI), so it would be nice to automate the creation of the appropriate .yml file. This is where PkgTemplates.jl comes in.

PkgTemplates.jl is a normal Julia package, so install it as usual and run using PkgTemplates. Then we can create our PracticePackage.jl:

t = Template(; dir = ".")
t("PracticePackage")

Running this code creates the package with the following directory structure:

PracticePackage
├── .git
│   ⋮
├── .github
│   ├── dependabot.yml
│   └── workflows
│       ├── CI.yml
│       ├── CompatHelper.yml
│       └── TagBot.yml
├── .gitignore
├── LICENSE
├── Manifest.toml
├── Project.toml
├── README.md
├── src
│   └── PracticePackage.jl
└── test
    └── runtests.jl

As you can see, PkgTemplates.jl automatically generates a lot of files that aid in following package development best practices, like adding CI and tests.

Note that many options can be supplied to Template to customize what files are generated. See the PkgTemplates.jl docs for all the options.

Checklist of settings

Basic Package Development

Once your package is set up, the next step is to actually add code. Add the functions, types, constants, etc. that your package needs directly in the PracticePackage module in src/PracticePackage.jl, or add additional files in the src directory and include them in the module. (See a previous blog post for more information about modules, though note that using modules directly works slightly differently than using packages.)

To add dependencies for your package to use, you will need to activate your project's package environment and then add packages. For example, if you want your package to use the DataFrames.jl package, start Julia and navigate to your package root directory. Then, activate the package environment and add the package:

(@v1.X) pkg> activate .

(PracticePackage) pkg> add DataFrames

After this, you will be able to include using DataFrames in your package code to enable the functionality provided by DataFrames.jl.

Adding packages after activating the package environment edits the package's Project.toml file. It adds a [deps] section that lists the added packages and their UUIDs. In the example above, adding DataFrames.jl adds the following lines to the Project.toml file:

[deps]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"

(And (PracticePackage) pkg> rm DataFrames would remove the DataFrames = ... line, so it is best not to edit the [deps] section manually.)

Finally, to try out your package, activate your package environment (as above) and then load your package as usual:

julia> using PracticePackage # No need to `add PracticePackage` first.

Note that by default Julia will have to be restarted to reload any changes you make to your package code. If you want to avoid restarting Julia whenever you make changes, check out Revise.jl.

Publishing/Registering a Package

Once your package is in working order, it is natural to want to publish the package for others to use.

A package can be published by registering it in a package registry, which basically is a map that tells the Julia package manager where to find a package so it can be downloaded.

Treasure map

The General registry is the largest registry as well as the default registry used by Julia; most, if not all, of the most popular open-source packages (DataFrames.jl, Plots.jl, StaticArrays.jl, ModelingToolkit.jl, etc.) exist in General. Once a package is registered in General, it can be installed with pkg> add PracticePackage.

(Note that if registering a package is not desired for some reason, a package can be added via URL, e.g., pkg> add https://github.com/username/PracticePackage.jl, assuming the package is in a public git repository. However, the package manager has limited ability to manage packages added in this way; in particular, managing package versions must be done manually.)

The most common way to register a package in General is to use Registrator.jl as a GitHub App. See the README for detailed instructions, but the process basically boils down to:

  1. Write/test package code.
  2. Update the version field in the Project.toml (e.g., to "0.1.0" or "1.0.0" for the first registered version).
  3. Add a comment with @JuliaRegistrator register to the latest commit that should be included in the registered version of the package.

Note that there are additional steps for preparing a package for publishing that we did not discuss in this post (such as specifying compatible versions of Julia and package dependencies). Refer to the General registry's documentation and links therein for details.

Summary

In this post, we discussed creating Julia packages. We learned what comprises a package, how to automate package creation, and how to register a package in Julia's General registry.

What package development tips do you have? Let us know in the comments below!

Additional Links

Cover image background provided by www.proflowers.com at https://www.flickr.com/photos/127365614@N08/16011252136.

Treasure map image source: https://openclipart.org/detail/299283/x-marks-the-spot