10 KiB
CurrentModule = PkgTemplates
PkgTemplates Developer Guide
Pages = ["developer.md"]
PkgTemplates can be easily extended by adding new Plugin
s.
There are two types of plugins: Plugin
and BasicPlugin
.
Plugin
BasicPlugin
Package Generation Pipeline
The package generation process looks basically like this:
- create directory for the package
- for each plugin, ordered by priority:
- run plugin prehook
- for each plugin, ordered by priority:
- run plugin hook
- for each plugin, ordered by priority:
- run plugin posthook
That's it! As you can tell, plugins play a central role in setting up a package.
The three main entrypoints for plugins to do work are the prehook
, the hook
, and the posthook
.
As the names might imply, they basically mean "before the main stage", "the main stage", and "after the main stage", respectively.
Each stage is basically identical, since the functions take the exact same arguments.
However, the multiple stages allow us to depend on artifacts of the previous stages.
For example, the Git
plugin uses posthook
to commit all generated files, but it wouldn't make sense to do that before the files are generated.
But what about dependencies within the same stage?
In this case, we have priority
to define which plugins go when.
The Git
plugin also uses this function to lower its priority, so that even if other plugins generate files in their posthooks, they still get committed.
prehook
hook
posthook
priority
Plugin
Walkthrough
Concrete types that subtype Plugin
directly are free to do almost anything.
To understand how they're implemented, let's look at simplified versions of two plugins: Documenter
to explore templating, and Git
to further clarify the multi-stage pipeline.
Example: Documenter
@with_kw_noshow struct Documenter <: Plugin
make_jl::String = default_file("make.jl")
index_md::String = default_file("index.md")
end
gitignore(::Documenter) = ["/docs/build/", "/docs/site/"]
badges(::Documenter) = [
Badge(
"Stable",
"https://img.shields.io/badge/docs-stable-blue.svg",
"https://{{USER}}.github.io/{{PKG}}.jl/stable",
),
Badge(
"Dev",
"https://img.shields.io/badge/docs-dev-blue.svg",
"https://{{USER}}.github.io/{{PKG}}.jl/dev",
),
]
view(p::Documenter, t::Template, pkg::AbstractString) = Dict(
"AUTHORS" => join(t.authors, ", "),
"PKG" => pkg,
"REPO" => "$(t.host)/$(t.user)/$pkg.jl",
"USER" => t.user,
)
function hook(p::Documenter, t::Template, pkg_dir::AbstractString)
pkg = basename(pkg_dir)
docs_dir = joinpath(pkg_dir, "docs")
make = render_file(p.make_jl, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(docs_dir, "make.jl"), make)
index = render_file(p.index_md, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(docs_dir, "src", "index.md"), index)
# What this function does is not relevant here.
create_documentation_project()
end
First of all, @with_kw_noshow
comes from Parameters.jl, and it just defines a nice keyword constructor for us.
The default values for our type are using default_file
to point to files in this repository.
default_file
The first method we implement for Documenter
is gitignore
, so that packages created with this plugin ignore documentation build artifacts.
gitignore
Second, we implement badges
to add a couple of badges to new packages' README files.
badges
Badge
These two functions, gitignore
and badges
, are currently the only "special" functions for cross-plugin interactions.
In other cases, you can still access the Template
's plugins to depend on the presence/properties of other plugins, although that's less powerful.
Third, we implement view
, which is used to fill placeholders in badges and rendered files.
view
Finally, we implement hook
, which is the real workhorse for the plugin.
Inside of this function, we generate a couple of files with the help of a few more text templating functions.
render_file
render_text
gen_file
combined_view
tags
For more information on text templating, see the BasicPlugin
Walkthrough and the section on Custom Template Files.
Example: Git
struct Git <: Plugin end
priority(::Git, ::typeof(posthook)) = 5
function prehook(::Git, t::Template, pkg_dir::AbstractString)
LibGit2.with(LibGit2.init(pkg_dir)) do repo
LibGit2.commit(repo, "Initial commit")
pkg = basename(pkg_dir)
url = "https://$(t.host)/$(t.user)/$pkg.jl"
close(GitRemote(repo, "origin", url))
end
end
function hook(::Git, t::Template, pkg_dir::AbstractString)
ignore = mapreduce(gitignore, append!, t.plugins)
unique!(sort!(ignore))
gen_file(joinpath(pkg_dir, ".gitignore"), join(ignore, "\n"))
end
function posthook(::Git, ::Template, pkg_dir::AbstractString)
LibGit2.with(GitRepo(pkg_dir)) do repo
LibGit2.add!(repo, ".")
LibGit2.commit(repo, "Files generated by PkgTemplates")
end
end
As previously mentioned, we use priority
to make sure that we commit all generated files.
Then, all three hooks are implemented:
prehook
creates the Git repository for the packagehook
generates the.gitignore
file, using the specialgitignore
functionposthook
adds and commits all generated files
Hopefully, this demonstrates the level of control you have over the package generation process when developing plugins.
BasicPlugin
Walkthrough
Most of the time, you don't really need all of the control that we showed off above.
Plugins that subtype BasicPlugin
perform a much more limited task.
In general, they just generate one templated file.
To illustrate, let's look at the Citation
plugin, which creates a CITATION.bib
file.
@with_kw_noshow struct Citation <: BasicPlugin
file::String = default_file("CITATION.bib")
end
source(p::Citation) = p.file
destination(::Citation) = "CITATION.bib"
tags(::Citation) = "<<", ">>"
view(::Citation, t::Template, pkg::AbstractString) = Dict(
"AUTHORS" => join(t.authors, ", "),
"MONTH" => month(today()),
"PKG" => pkg,
"URL" => "https://$(t.host)/$(t.user)/$pkg.jl",
"YEAR" => year(today()),
)
Similar to the Documenter
example above, we're defining a keyword constructor, and assigning a default template file from this repository.
This plugin adds nothing to .gitignore
, and it doesn't add any badges, so implementations for gitignore
and badges
are omitted.
First, we implement source
and destination
to define where the template file comes from, and where it goes.
These functions are specific to BasicPlugin
s, and have no effect on regular Plugin
s by default.
source
destination
Next, we implement tags
.
We briefly saw this function earlier, but in this case it's necessary to change its behaviour from the default.
To see why, it might help to see the template file in its entirety:
@misc{<<&PKG>>.jl,
author = {<<&AUTHORS>>},
title = {<<&PKG>>.jl},
url = {<<&URL>>},
version = {v0.1.0},
year = {<<&YEAR>>},
month = {<<&MONTH>>}
}
Because the file contains its own {}
delimiters, we need to use different ones for templating to work properly.
Finally, we implement view
to fill in the placeholders that we saw in the template file.
Doing Extra Work With BasicPlugin
s
Notice that we didn't have to implement hook
for our plugin.
It's implemented for all BasicPlugin
s, like so:
function render_plugin(p::BasicPlugin, t::Template, pkg::AbstractString)
return render_file(source(p), combined_view(p, t, pkg), tags(p))
end
function hook(p::BasicPlugin, t::Template, pkg_dir::AbstractString)
source(p) === nothing && return
pkg = basename(pkg_dir)
path = joinpath(pkg_dir, destination(p))
text = render_plugin(p, t, pkg)
gen_file(path, text)
end
But what if we want to do a little more than just generate one file?
A good example of this is the Tests
plugin.
It creates runtests.jl
, but it also modifies the Project.toml
to include the Test
dependency.
Of course, we could use a normal Plugin
, but it turns out there's a way to avoid that while still getting the extra capbilities that we want.
The plugin implements its own hook
, but uses invoke
to avoid duplicating the file creation code:
@with_kw_noshow struct Tests <: BasicPlugin
file::String = default_file("runtests.jl")
end
source(p::Tests) = p.file
destination(::Tests) = joinpath("test", "runtests.jl")
view(::Tests, ::Template, pkg::AbstractString) = Dict("PKG" => pkg)
function hook(p::Tests, t::Template, pkg_dir::AbstractString)
# Do the normal BasicPlugin behaviour to create the test script.
invoke(hook, Tuple{BasicPlugin, Template, AbstractString}, p, t, pkg_dir)
# Do some other work.
add_test_dependency()
end
There is also a default prehook
implementation for BasicPlugin
s, which checks that the plugin's source
file exists, and throws an ArgumentError
otherwise.
If you want to extend the prehook but keep the file existence check, use the invoke
method as described above.
For more examples, see the plugins in the Continuous Integration (CI) and Code Coverage sections.
Miscellaneous Tips
Writing Template Files
For an overview of writing template files for Mustache.jl, see Custom Template Files in the user guide.
Traits
There are a few traits for plugin types that are occassionally used to answer questions like "does this Template
have any code coverage plugins?".
If you're implementing a plugin that fits into one of the following categories, it would be wise to implement the corresponding trait function to return true
for your type.
is_ci
is_coverage
Formatting Version Numbers
When writing configuration files for CI services, working with version numbers is often needed. There are a few convenience functions that can be used to make this a little bit easier.
compat_version
format_version
collect_versions