5. Creating Packages
Generating files for a package
The PkgTemplates package offers an easy, repeatable, and customizable way to generate the files for a new package. It can also generate files needed for Documentation, CI, etc. We recommend that you use PkgTemplates for creating new packages instead of using the minimal pkg> generate functionality described below.
To generate the bare minimum files for a new package, use pkg> generate.
(@v1.10) pkg> generate HelloWorldThis creates a new project HelloWorld in a subdirectory by the same name, with the following files (visualized with the external tree command):
shell> tree HelloWorld/
HelloWorld/
├── Project.toml
└── src
└── HelloWorld.jl
2 directories, 2 filesThe Project.toml file contains the name of the package, its unique UUID, its version, the authors and potential dependencies:
name = "HelloWorld"
uuid = "b4cd1eb8-1e24-11e8-3319-93036a3eb9f3"
version = "0.1.0"
authors = ["Some One <someone@email.com>"]
[deps]The content of src/HelloWorld.jl is:
module HelloWorld
greet() = print("Hello World!")
end # moduleWe can now activate the project by using the path to the directory where it is installed, and load the package:
pkg> activate ./HelloWorld
julia> import HelloWorld
julia> HelloWorld.greet()
Hello World!For the rest of the tutorial we enter inside the directory of the project, for convenience:
julia> cd("HelloWorld")Adding dependencies to the project
Let’s say we want to use the standard library package Random and the registered package JSON in our project. We simply add these packages (note how the prompt now shows the name of the newly generated project, since we activated it):
(HelloWorld) pkg> add Random JSON
Resolving package versions...
Updating `~/HelloWorld/Project.toml`
[682c06a0] + JSON v0.21.3
[9a3f8284] + Random
Updating `~/HelloWorld/Manifest.toml`
[682c06a0] + JSON v0.21.3
[69de0a69] + Parsers v2.4.0
[ade2ca70] + Dates
...Both Random and JSON got added to the project’s Project.toml file, and the resulting dependencies got added to the Manifest.toml file. The resolver has installed each package with the highest possible version, while still respecting the compatibility that each package enforces on its dependencies.
We can now use both Random and JSON in our project. Changing src/HelloWorld.jl to
module HelloWorld
import Random
import JSON
greet() = print("Hello World!")
greet_alien() = print("Hello ", Random.randstring(8))
end # moduleand reloading the package, the new greet_alien function that uses Random can be called:
julia> HelloWorld.greet_alien()
Hello aT157rHVDefining a public API
If you want your package to be useful to other packages and you want folks to be able to easily update to newer version of your package when they come out, it is important to document what behavior will stay consistent across updates.
Unless you note otherwise, the public API of your package is defined as all the behavior you describe about public symbols. A public symbol is a symbol that is exported from your package with the export keyword or marked as public with the public keyword. When you change the behavior of something that was previously public so that the new version no longer conforms to the specifications provided in the old version, you should adjust your package version number according to Julia's variant on SemVer. If you would like to include a symbol in your public API without exporting it into the global namespace of folks who call using YourPackage, you should mark that symbol as public with public that_symbol. Symbols marked as public with the public keyword are just as public as those marked as public with the export keyword, but when folks call using YourPackage, they will still have to qualify access to those symbols with YourPackage.that_symbol.
Let's say we would like our greet function to be part of the public API, but not the greet_alien function. We could then write the following and release it as version 1.0.0.
module HelloWorld
export greet
import Random
import JSON
"Writes a friendly message."
greet() = print("Hello World!")
"Greet an alien by a randomly generated name."
greet_alien() = print("Hello ", Random.randstring(8))
end # moduleThen, if we change greet to
"Writes a friendly message that is exactly three words long."
greet() = print("Hello Lovely World!")We would release the new version as 1.1.0. This is not breaking because the new implementation conforms to the old documentation, but it does add a new feature, that the message must be three words long.
Later, we may wish to change greet_alien to
"Greet an alien by the name of \"Zork\"."
greet_alien() = print("Hello Zork")And also export it by changing
export greetto
export greet, greet_alienWe should release this new version as 1.2.0 because it adds a new feature greet_alien to the public API. Even though greet_alien was documented before and the new version does not conform to the old documentation, this is not breaking because the old documentation was not attached to a symbol that was exported at the time so that documentation does not apply across released versions.
However, if we now wish to change greet to
"Writes a friendly message that is exactly four words long."
greet() = print("Hello very lovely world")we would need to release the new version as 2.0.0. In version 1.1.0, we specified that the greeting would be three words long, and because greet was exported, that description also applies to all future versions until the next breaking release. Because this new version does not conform to the old specification, it must be tagged as a breaking change.
Please note that version numbers are free and unlimited. It is okay to use lots of them (e.g. version 6.62.8).
Adding a build step to the package
The build step is executed the first time a package is installed or when explicitly invoked with build. A package is built by executing the file deps/build.jl.
julia> mkpath("deps");
julia> write("deps/build.jl",
"""
println("I am being built...")
""");
(HelloWorld) pkg> build
Building HelloWorld → `deps/build.log`
Resolving package versions...
julia> print(readchomp("deps/build.log"))
I am being built...If the build step fails, the output of the build step is printed to the console
julia> write("deps/build.jl",
"""
error("Ooops")
""");
(HelloWorld) pkg> build
Building HelloWorld → `~/HelloWorld/deps/build.log`
ERROR: Error building `HelloWorld`:
ERROR: LoadError: Ooops
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:35
[2] top-level scope
@ ~/HelloWorld/deps/build.jl:1
[3] include(fname::String)
@ Base.MainInclude ./client.jl:476
[4] top-level scope
@ none:5
in expression starting at /home/kc/HelloWorld/deps/build.jl:1A build step should generally not create or modify any files in the package directory. If you need to store some files from the build step, use the Scratch.jl package.
Adding tests to the package
When a package is tested the file test/runtests.jl is executed:
julia> mkpath("test");
julia> write("test/runtests.jl",
"""
println("Testing...")
""");
(HelloWorld) pkg> test
Testing HelloWorld
Resolving package versions...
Testing...
Testing HelloWorld tests passedTests are run in a new Julia process, where the package itself, and any test-specific dependencies, are available, see below.
Tests should generally not create or modify any files in the package directory. If you need to store some files from the build step, use the Scratch.jl package.
Test-specific dependencies
Test-specific dependencies are dependencies that are not dependencies of the package itself but are available when the package is tested.
Recommended approach: Using workspaces with test/Project.toml
The recommended way to add test-specific dependencies is to use workspaces. This is done by:
- Adding a
[workspace]section to your package'sProject.toml:
[workspace]
projects = ["test"]- Creating a
test/Project.tomlfile with your test dependencies:
(HelloWorld) pkg> activate ./test
[ Info: activating environment at `~/HelloWorld/test/Project.toml`.
(HelloWorld/test) pkg> add Test
Resolving package versions...
Updating `~/HelloWorld/test/Project.toml`
[8dfed614] + TestWhen using workspaces, the package manager resolves dependencies for all projects in the workspace together, and creates a single Manifest.toml next to the base Project.toml. This provides better dependency resolution and makes it easier to manage test-specific dependencies.
You can now use Test in the test script:
julia> write("test/runtests.jl",
"""
using Test
@test 1 == 1
""");
(HelloWorld/test) pkg> activate .
(HelloWorld) pkg> test
Testing HelloWorld
Resolving package versions...
Testing HelloWorld tests passedWorkspaces can also be used for other purposes, such as documentation or benchmarks, by adding additional projects to the workspace:
[workspace]
projects = ["test", "docs", "benchmarks"]See the section on Workspaces in the Project.toml documentation for more details.
Alternative approach: Using [sources] with path-based dependencies
An alternative to workspaces is to use the [sources] section in test/Project.toml to reference the parent package. The [sources] section allows you to specify custom locations (paths or URLs) for dependencies, overriding registry information. This approach creates a separate manifest in the test/ directory (unlike workspaces which create a single shared manifest).
To use this approach:
- Create a
test/Project.tomlfile and add your test dependencies:
(HelloWorld) pkg> activate ./test
[ Info: activating environment at `~/HelloWorld/test/Project.toml`.
(HelloWorld/test) pkg> add Test
Resolving package versions...
Updating `~/HelloWorld/test/Project.toml`
[8dfed614] + Test- Add the parent package as a dependency using
[sources]with a relative path:
# In test/Project.toml
[deps]
HelloWorld = "00000000-0000-0000-0000-000000000000" # Your package UUID
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[sources]
HelloWorld = {path = ".."}The [sources] section tells Pkg to use the local path for HelloWorld instead of looking it up in a registry. This creates a separate test/Manifest.toml that tracks the resolved dependencies for your test environment independently from the main package manifest. You can now run tests directly:
$ julia --project=test
julia> using HelloWorld, Test
julia> include("test/runtests.jl")The key difference from workspaces is that this approach uses a separate manifest file (test/Manifest.toml) for the test environment, while workspaces create a single shared manifest (Manifest.toml) that resolves all projects together. This means:
- With
[sources]+ path: Dependencies are resolved independently for each environment - With workspaces: Dependencies are resolved together, ensuring compatibility across all projects in the workspace
For more details on [sources], see the [sources] section in the Project.toml documentation.
Legacy approach: target based test specific dependencies
This approach is legacy and maintained for compatibility. New packages should use workspaces instead.
Using this method, test-specific dependencies are added under an [extras] section and to a test target:
[extras]
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Markdown", "Test"]Note that the only supported targets are test and build, the latter of which (not recommended) can be used for any deps/build.jl scripts.
Legacy approach: test/Project.toml without workspace
This approach is legacy and maintained for compatibility. New packages should use workspaces instead.
In Julia 1.2 and later, test dependencies can be declared in test/Project.toml without using a workspace. When running tests, Pkg will automatically merge the package and test projects to create the test environment.
This approach works similarly to the workspace approach, but without the workspace declaration in the main Project.toml.
Compatibility on dependencies
Every dependency should in general have a compatibility constraint on it. This is an important topic so there is a separate chapter about it: Compatibility.
Weak dependencies
This is a somewhat advanced usage of Pkg which can be skipped for people new to Julia and Julia packages.
A weak dependency is a dependency that will not automatically install when the package is installed but you can still control what versions of that package are allowed to be installed by setting compatibility on it. These are listed in the project file under the [weakdeps] section:
[weakdeps]
SomePackage = "b3785f31-9d33-4cdf-bc73-f646780f1739"
[compat]
SomePackage = "1.2"The current usage of this is almost solely limited to "extensions" which is described in the next section.
Conditional loading of code in packages (Extensions)
This is a somewhat advanced usage of Pkg which can be skipped for people new to Julia and Julia packages.
Sometimes one wants to make two or more packages work well together, but may be reluctant (perhaps due to increased load times) to make one an unconditional dependency of the other. A package extension is a module in a file (similar to a package) that is automatically loaded when some other set of packages are loaded into the Julia session. This is very similar to functionality that the external package Requires.jl provides, but which is now available directly through Julia, and provides added benefits such as being able to precompile the extension.
Code structure
A useful application of extensions could be for a plotting package that should be able to plot objects from a wide variety of different Julia packages. Adding all those different Julia packages as dependencies of the plotting package could be expensive since they would end up getting loaded even if they were never used. Instead, the code required to plot objects for specific packages can be put into separate files (extensions) and these are loaded only when the packages that define the type(s) we want to plot are loaded.
Below is an example of how the code can be structured for a use case in which a Plotting package wants to be able to display objects defined in the external package Contour. The file and folder structure shown below is found in the Plotting package.
Project.toml:
name = "Plotting"
version = "0.1.0"
uuid = "..."
[weakdeps]
Contour = "d38c429a-6771-53c6-b99e-75d170b6e991"
[extensions]
# name of extension to the left
# extension dependencies required to load the extension to the right
# use a list for multiple extension dependencies
ContourExt = "Contour"
[compat]
Contour = "0.6.2"src/Plotting.jl:
module Plotting
function plot(x::Vector)
# Some functionality for plotting a vector here
end
end # moduleext/ContourExt.jl (can also be in ext/ContourExt/ContourExt.jl):
module ContourExt # Should be same name as the file (just like a normal package)
using Plotting, Contour
function Plotting.plot(c::Contour.ContourCollection)
# Some functionality for plotting a contour here
end
end # moduleExtensions can have arbitrary names (here ContourExt), following the format of this example is likely a good idea for extensions with a single dependency. In Pkg output, extension names are always shown together with their parent package name.
Often you will want to load extension dependencies when testing your package. The recommended approach is to use workspaces and add the extension dependencies to your test/Project.toml (see Test-specific dependencies). For older Julia versions that don't support workspaces, you can put the extension dependencies into the test target, which requires you to also put the package in the [extras] section. The project verifier on older Julia versions will complain if this is not done.
If you use a manifest generated by a Julia version that does not know about extensions with a Julia version that does know about them, the extensions will not load. This is because the manifest lacks some information that tells Julia when it should load these packages. So make sure you use a manifest generated at least the Julia version you are using.
Behavior of extensions
A user that depends only on Plotting will not pay the cost of the "extension" inside the ContourExt module. It is only when the Contour package actually gets loaded that the ContourExt extension is loaded too and provides the new functionality.
In our example, the new functionality is an additional method, which we add to an existing function from the parent package Plotting. Implementing such methods is among the most standard use cases of package extensions. Within the parent package, the function to extend can even be defined with zero methods, as follows:
function plot endIf one considers ContourExt as a completely separate package, it could be argued that defining Plotting.plot(c::Contour.ContourCollection) is type piracy since ContourExt owns neither the function Plotting.plot nor the type Contour.ContourCollection. However, for extensions, it is ok to assume that the extension owns the functions in its parent package.
In other situations, one may need to define new symbols in the extension (types, structs, functions, etc.) instead of reusing those from the parent package. Such symbols are created in a separate module corresponding to the extension, namely ContourExt, and thus not in Plotting itself. If extension symbols are needed in the parent package, one must call Base.get_extension to retrieve them. Here is an example showing how a custom type defined in ContourExt can be accessed in Plotting:
ext = Base.get_extension(@__MODULE__, :ContourExt)
if !isnothing(ext)
ContourPlotType = ext.ContourPlotType
endOn the other hand, accessing extension symbols from a third-party package (i.e. not the parent) is not a recommended practice at the moment.
Backwards compatibility
This section discusses various methods for using extensions on Julia versions that support them, while simultaneously providing similar functionality on older Julia versions.
Requires.jl
This section is relevant if you are currently using Requires.jl but want to transition to using extensions (while still having Requires be used on Julia versions that do not support extensions). This is done by making the following changes (using the example above):
Add the following to the package file. This makes it so that Requires.jl loads and inserts the callback only when extensions are not supported
# This symbol is only defined on Julia versions that support extensions if !isdefined(Base, :get_extension) using Requires end @static if !isdefined(Base, :get_extension) function __init__() @require Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" include("../ext/ContourExt.jl") end endor if you have other things in your
__init__()function:if !isdefined(Base, :get_extension) using Requires end function __init__() # Other init functionality here @static if !isdefined(Base, :get_extension) @require Contour = "d38c429a-6771-53c6-b99e-75d170b6e991" include("../ext/ContourExt.jl") end endMake the following change in the conditionally-loaded code in
ContourExt.jl:isdefined(Base, :get_extension) ? (using Contour) : (using ..Contour)Add
Requiresto[weakdeps]in yourProject.tomlfile, so that it is listed in both[deps]and[weakdeps]. Julia 1.9+ knows to not install it as a regular dependency, whereas earlier versions will consider it a dependency.
The package should now work with Requires.jl on Julia versions before extensions were introduced and with extensions on more recent Julia versions.
Transition from normal dependency to extension
This section is relevant if you have a normal dependency that you want to transition be an extension (while still having the dependency be a normal dependency on Julia versions that do not support extensions). This is done by making the following changes (using the example above):
- Make sure that the package is both in the
[deps]and[weakdeps]section. Newer Julia versions will ignore dependencies in[deps]that are also in[weakdeps]. - Add the following to your main package file (typically at the bottom):
if !isdefined(Base, :get_extension) include("../ext/ContourExt.jl") end
Using an extension while supporting older Julia versions
In the case where one wants to use an extension (without worrying about the feature of the extension being available on older Julia versions) while still supporting older Julia versions without workspace support, the packages under [weakdeps] should be duplicated into [extras]. This is an unfortunate duplication, but without doing this the project verifier under older Julia versions will throw an error if it finds packages under [compat] that is not listed in [extras].
For Julia 1.13+, using workspaces is recommended and this duplication is not necessary.
Package naming guidelines
Package names should be sensible to most Julia users, even to those who are not domain experts. The following guidelines apply to the General registry but may be useful for other package registries as well.
Since the General registry belongs to the entire community, people may have opinions about your package name when you publish it, especially if it's ambiguous or can be confused with something other than what it is. Usually, you will then get suggestions for a new name that may fit your package better.
Avoid jargon. In particular, avoid acronyms unless there is minimal possibility of confusion.
- It's ok for package names to contain
DNAif you're talking about the DNA, which has a universally agreed upon definition. - It's more difficult to justify package names containing the acronym
CIfor instance, which may mean continuous integration, confidence interval, etc. - If there is risk of confusion it may be best to disambiguate an acronym with additional words such as a lab group or field.
- If your acronym is unambiguous, easily searchable, and/or unlikely to be confused across domains a good justification is often enough for approval.
- It's ok for package names to contain
Avoid using
Juliain your package name or prefixing it withJu.- It is usually clear from context and to your users that the package is a Julia package.
- Package names already have a
.jlextension, which communicates to users thatPackage.jlis a Julia package. - Having Julia in the name can imply that the package is connected to, or endorsed by, contributors to the Julia language itself.
Packages that provide most of their functionality in association with a new type should have pluralized names.
DataFramesprovides theDataFrametype.BloomFiltersprovides theBloomFiltertype.- In contrast,
JuliaParserprovides no new type, but instead new functionality in theJuliaParser.parse()function.
Err on the side of clarity, even if clarity seems long-winded to you.
RandomMatricesis a less ambiguous name thanRndMatorRMT, even though the latter are shorter.- Generally package names should be at least 5 characters long not including the
.jlextension
A less systematic name may suit a package that implements one of several possible approaches to its domain.
- Julia does not have a single comprehensive plotting package. Instead,
Gadfly,PyPlot,Winstonand other packages each implement a unique approach based on a particular design philosophy. - In contrast,
SortingAlgorithmsprovides a consistent interface to use many well-established sorting algorithms.
- Julia does not have a single comprehensive plotting package. Instead,
Packages that wrap external libraries or programs can be named after those libraries or programs.
CPLEX.jlwraps theCPLEXlibrary, which can be identified easily in a web search.MATLAB.jlprovides an interface to call the MATLAB engine from within Julia.
Avoid naming a package closely to an existing package
Websocketis too close toWebSocketsand can be confusing to users. Rather use a new name such asSimpleWebsockets.
Avoid using a distinctive name that is already in use in a well known, unrelated project.
- Don't use the names
Tkinter.jl,TkinterGUI.jl, etc. for a package that is unrelated to the populartkinterpython package, even if it provides bindings to Tcl/Tk. A package name ofTkinter.jlwould only be appropriate if the package used Python's library to accomplish its work or was spearheaded by the same community of developers. - It's okay to name a package
HTTP.jleven though it is unrelated to the popular rust cratehttpbecause in most usages the name "http" refers to the hypertext transfer protocol, not to thehttprust crate. - It's okay to name a package
OpenSSL.jlif it provides an interface to the OpenSSL library, even without explicit affiliation with the creators of the OpenSSL (provided there's no copyright or trademark infringement etc.)
- Don't use the names
Packages should follow the Stylistic Conventions.
- The package name should begin with a capital letter and word separation is shown with upper camel case
- Only ASCII characters are allowed in a package name
- Packages that provide the functionality of a project from another language should use the Julia convention
- Packages that provide pre-built libraries and executables can keep their original name, but should get
_jllas a suffix. For examplepandoc_jllwraps pandoc. However, note that the generation and release of most JLL packages is handled by the Yggdrasil system.
For the complete list of rules for automatic merging into the General registry, see these guidelines.
Registering packages
Once a package is ready it can be registered with the General Registry (see also the FAQ). Currently, packages are submitted via Registrator. In addition to Registrator, TagBot helps manage the process of tagging releases.
Creating new package versions
After registering your package, you'll want to release new versions as you add features and fix bugs. The typical workflow is:
Update the version number in your
Project.tomlfile according to semantic versioning rules. For example:- Increment the patch version (1.2.3 → 1.2.4) for bug fixes
- Increment the minor version (1.2.3 → 1.3.0) for new features that don't break existing functionality
- Increment the major version (1.2.3 → 2.0.0) for breaking changes
Commit your changes to your package repository, including the updated version number.
Tag the release using Registrator. Comment
@JuliaRegistrator registeron a commit or pull request in your GitHub repositoryAutomated tagging: Once you've set up
TagBot, it will automatically create a git tag in your repository when a new version is registered. This keeps your repository tags synchronized with registered versions.
The registration process typically takes a few minutes. Registrator will:
- Check that your package meets registry requirements (has tests, proper version bounds, etc.)
- Submit a pull request to the General registry
- Automated checks will run, and if everything passes, the PR will be automatically merged
For private registries or more advanced workflows, see the documentation for LocalRegistry.jl and RegistryCI.jl.
Best Practices
Packages should avoid mutating their own state (writing to files within their package directory). Packages should, in general, not assume that they are located in a writable location (e.g. if installed as part of a system-wide depot) or even a stable one (e.g. if they are bundled into a system image by PackageCompiler.jl). To support the various use cases in the Julia package ecosystem, the Pkg developers have created a number of auxiliary packages and techniques to help package authors create self-contained, immutable, and relocatable packages:
Artifactscan be used to bundle chunks of data alongside your package, or even allow them to be downloaded on-demand. Prefer artifacts over attempting to open a file via a path such asjoinpath(@__DIR__, "data", "my_dataset.csv")as this is non-relocatable. Once your package has been precompiled, the result of@__DIR__will have been baked into your precompiled package data, and if you attempt to distribute this package, it will attempt to load files at the wrong location. Artifacts can be bundled and accessed easily using theartifact"name"string macro.Scratch.jlprovides the notion of "scratch spaces", mutable containers of data for packages. Scratch spaces are designed for data caches that are completely managed by a package and should be removed when the package itself is uninstalled. For important user-generated data, packages should continue to write out to a user-specified path that is not managed by Julia or Pkg.Preferences.jlallows packages to read and write preferences to the top-levelProject.toml. These preferences can be read at runtime or compile-time, to enable or disable different aspects of package behavior. Packages previously would write out files to their own package directories to record options set by the user or environment, but this is highly discouraged now thatPreferencesis available.
See Also
- Managing Packages - Learn how to add, update, and manage package dependencies
- Working with Environments - Understand environments and reproducible development
- Compatibility - Specify version constraints for dependencies
- API Reference - Functional API for non-interactive package management