PkgTemplates Developer Guide
PkgTemplates can be easily extended by adding new Plugin
s.
There are two types of plugins: Plugin
and BasicPlugin
.
PkgTemplates.Plugin
— TypePlugins are PkgTemplates' source of customization and extensibility. Add plugins to your Template
s to enable extra pieces of repository setup.
When implementing a new plugin, subtype this type to have full control over its behaviour.
PkgTemplates.BasicPlugin
— TypeA simple plugin that, in general, creates a single file.
Template + Package Creation Pipeline
The Template
constructor basically does this:
- extract values from keyword arguments
+Developer Guide · PkgTemplates.jl PkgTemplates Developer Guide
PkgTemplates can be easily extended by adding new Plugin
s.
There are two types of plugins: Plugin
and BasicPlugin
.
PkgTemplates.Plugin
— TypePlugins are PkgTemplates' source of customization and extensibility. Add plugins to your Template
s to enable extra pieces of repository setup.
When implementing a new plugin, subtype this type to have full control over its behaviour.
sourcePkgTemplates.BasicPlugin
— TypeA simple plugin that, in general, creates a single file.
sourceTemplate + Package Creation Pipeline
The Template
constructor basically does this:
- extract values from keyword arguments
- create a Template from the values
- for each plugin:
- - validate plugin against the template
The plugin validation step uses the validate
function. It lets us catch mistakes before we try to generate packages.
PkgTemplates.validate
— Functionvalidate(::Plugin, ::Template)
Perform any required validation for a Plugin
.
It is preferred to do validation here instead of in prehook
, because this function is called at Template
construction time, whereas the prehook is only run at package generation time.
sourceThe package generation process looks like this:
- create empty directory for the package
+ - validate plugin against the template
The plugin validation step uses the validate
function. It lets us catch mistakes before we try to generate packages.
PkgTemplates.validate
— Functionvalidate(::Plugin, ::Template)
Perform any required validation for a Plugin
.
It is preferred to do validation here instead of in prehook
, because this function is called at Template
construction time, whereas the prehook is only run at package generation time.
sourceThe package generation process looks like this:
- create empty 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
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 posthook's priority, so that even if other plugins generate files in their posthooks, they still get committed (provided that those plugins didn't set an even lower priority).
PkgTemplates.prehook
— Functionprehook(::Plugin, ::Template, pkg_dir::AbstractString)
Stage 1 of the package generation process (the "before" stage, in general). At this point, pkg_dir
is an empty directory that will eventually contain the package, and neither the hook
s nor the posthook
s have run.
Note pkg_dir
only stays empty until the first plugin chooses to create a file. See also: priority
.
sourcePkgTemplates.hook
— Functionhook(::Plugin, ::Template, pkg_dir::AbstractString)
Stage 2 of the package generation pipeline (the "main" stage, in general). At this point, the prehook
s have run, but not the posthook
s.
pkg_dir
is the directory in which the package is being generated (so basename(pkg_dir)
is the package name).
Note You usually shouldn't implement this function for BasicPlugin
s. If you do, it should probably invoke
the generic method (otherwise, there's not much reason to subtype BasicPlugin
).
sourcePkgTemplates.posthook
— Functionposthook(::Plugin, ::Template, pkg_dir::AbstractString)
Stage 3 of the package generation pipeline (the "after" stage, in general). At this point, both the prehook
s and hook
s have run.
sourcePkgTemplates.priority
— Functionpriority(::Plugin, ::Union{typeof(prehook), typeof(hook), typeof(posthook)}) -> Int
Determines the order in which plugins are processed (higher goes first). The default priority (DEFAULT_PRIORITY
), is 1000
.
You can implement this function per-stage (by using ::typeof(hook)
, for example), or for all stages by simply using ::Function
.
sourcePlugin
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
+ - run plugin posthook
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 posthook's priority, so that even if other plugins generate files in their posthooks, they still get committed (provided that those plugins didn't set an even lower priority).
PkgTemplates.prehook
— Functionprehook(::Plugin, ::Template, pkg_dir::AbstractString)
Stage 1 of the package generation process (the "before" stage, in general). At this point, pkg_dir
is an empty directory that will eventually contain the package, and neither the hook
s nor the posthook
s have run.
Note pkg_dir
only stays empty until the first plugin chooses to create a file. See also: priority
.
sourcePkgTemplates.hook
— Functionhook(::Plugin, ::Template, pkg_dir::AbstractString)
Stage 2 of the package generation pipeline (the "main" stage, in general). At this point, the prehook
s have run, but not the posthook
s.
pkg_dir
is the directory in which the package is being generated (so basename(pkg_dir)
is the package name).
Note You usually shouldn't implement this function for BasicPlugin
s. If you do, it should probably invoke
the generic method (otherwise, there's not much reason to subtype BasicPlugin
).
sourcePkgTemplates.posthook
— Functionposthook(::Plugin, ::Template, pkg_dir::AbstractString)
Stage 3 of the package generation pipeline (the "after" stage, in general). At this point, both the prehook
s and hook
s have run.
sourcePkgTemplates.priority
— Functionpriority(::Plugin, ::Union{typeof(prehook), typeof(hook), typeof(posthook)}) -> Int
Determines the order in which plugins are processed (higher goes first). The default priority (DEFAULT_PRIORITY
), is 1000
.
You can implement this function per-stage (by using ::typeof(hook)
, for example), or for all stages by simply using ::Function
.
sourcePlugin
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("docs", "make.jl")
index_md::String = default_file("docs", "src", "index.md")
end
@@ -47,7 +47,7 @@ function hook(p::Documenter, t::Template, pkg_dir::AbstractString)
# What this function does is not relevant here.
create_documentation_project()
-end
The @with_kw_noshow
macro defines keyword constructors for us. Inside of our struct definition, we're using default_file
to refer to files in this repository.
PkgTemplates.default_file
— Functiondefault_file(paths::AbstractString...) -> String
Return a path relative to the default template file directory (~/build/invenia/PkgTemplates.jl/templates
).
sourceThe first method we implement for Documenter
is gitignore
, so that packages created with this plugin ignore documentation build artifacts.
PkgTemplates.gitignore
— Functiongitignore(::Plugin) -> Vector{String}
Return patterns that should be added to .gitignore
. These are used by the Git
plugin.
By default, an empty list is returned.
sourceSecond, we implement badges
to add a couple of badges to new packages' README files.
PkgTemplates.badges
— Functionbadges(::Plugin) -> Union{Badge, Vector{Badge}}
Return a list of Badge
s, or just one, to be added to README.md
. These are used by the Readme
plugin to add badges to the README.
By default, an empty list is returned.
sourcePkgTemplates.Badge
— TypeBadge(hover::AbstractString, image::AbstractString, link::AbstractString)
Container for Markdown badge data. Each argument can contain placeholders, which will be filled in with values from combined_view
.
Arguments
hover::AbstractString
: Text to appear when the mouse is hovered over the badge.image::AbstractString
: URL to the image to display.link::AbstractString
: URL to go to upon clicking the badge.
sourceThese 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.
PkgTemplates.view
— Functionview(::Plugin, ::Template, pkg::AbstractString) -> Dict{String, Any}
Return the view to be passed to the text templating engine for this plugin. pkg
is the name of the package being generated.
For BasicPlugin
s, this is used for both the plugin badges (see badges
) and the template file (see source
). For other Plugin
s, it is used only for badges, but you can always call it yourself as part of your hook
implementation.
By default, an empty Dict
is returned.
sourceFinally, 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.
PkgTemplates.render_file
— Functionrender_file(file::AbstractString view::Dict{<:AbstractString}, tags) -> String
Render a template file with the data in view
. tags
should be a tuple of two strings, which are the opening and closing delimiters, or nothing
to use the default delimiters.
sourcePkgTemplates.render_text
— Functionrender_text(text::AbstractString, view::Dict{<:AbstractString}, tags=nothing) -> String
Render some text with the data in view
. tags
should be a tuple of two strings, which are the opening and closing delimiters, or nothing
to use the default delimiters.
sourcePkgTemplates.gen_file
— Functiongen_file(file::AbstractString, text::AbstractString)
Create a new file containing some given text. Trailing whitespace is removed, and the file will end with a newline.
sourcePkgTemplates.combined_view
— Functioncombined_view(::Plugin, ::Template, pkg::AbstractString) -> Dict{String, Any}
This function combines view
and user_view
for use in text templating. If you're doing manual file creation or text templating (i.e. writing Plugin
s that are not BasicPlugin
s), then you should use this function rather than either of the former two.
sourcePkgTemplates.tags
— Functiontags(::Plugin) -> Tuple{String, String}
Return the delimiters used for text templating. See the Citation
plugin for a rare case where changing the tags is necessary.
By default, the tags are "{{"
and "}}"
.
sourceFor more information on text templating, see the BasicPlugin
Walkthrough and the section on Custom Template Files.
Example: Git
struct Git <: Plugin end
+end
The @with_kw_noshow
macro defines keyword constructors for us. Inside of our struct definition, we're using default_file
to refer to files in this repository.
PkgTemplates.default_file
— Functiondefault_file(paths::AbstractString...) -> String
Return a path relative to the default template file directory (~/build/invenia/PkgTemplates.jl/templates
).
sourceThe first method we implement for Documenter
is gitignore
, so that packages created with this plugin ignore documentation build artifacts.
PkgTemplates.gitignore
— Functiongitignore(::Plugin) -> Vector{String}
Return patterns that should be added to .gitignore
. These are used by the Git
plugin.
By default, an empty list is returned.
sourceSecond, we implement badges
to add a couple of badges to new packages' README files.
PkgTemplates.badges
— Functionbadges(::Plugin) -> Union{Badge, Vector{Badge}}
Return a list of Badge
s, or just one, to be added to README.md
. These are used by the Readme
plugin to add badges to the README.
By default, an empty list is returned.
sourcePkgTemplates.Badge
— TypeBadge(hover::AbstractString, image::AbstractString, link::AbstractString)
Container for Markdown badge data. Each argument can contain placeholders, which will be filled in with values from combined_view
.
Arguments
hover::AbstractString
: Text to appear when the mouse is hovered over the badge.image::AbstractString
: URL to the image to display.link::AbstractString
: URL to go to upon clicking the badge.
sourceThese 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.
PkgTemplates.view
— Functionview(::Plugin, ::Template, pkg::AbstractString) -> Dict{String, Any}
Return the view to be passed to the text templating engine for this plugin. pkg
is the name of the package being generated.
For BasicPlugin
s, this is used for both the plugin badges (see badges
) and the template file (see source
). For other Plugin
s, it is used only for badges, but you can always call it yourself as part of your hook
implementation.
By default, an empty Dict
is returned.
sourceFinally, 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.
PkgTemplates.render_file
— Functionrender_file(file::AbstractString view::Dict{<:AbstractString}, tags) -> String
Render a template file with the data in view
. tags
should be a tuple of two strings, which are the opening and closing delimiters, or nothing
to use the default delimiters.
sourcePkgTemplates.render_text
— Functionrender_text(text::AbstractString, view::Dict{<:AbstractString}, tags=nothing) -> String
Render some text with the data in view
. tags
should be a tuple of two strings, which are the opening and closing delimiters, or nothing
to use the default delimiters.
sourcePkgTemplates.gen_file
— Functiongen_file(file::AbstractString, text::AbstractString)
Create a new file containing some given text. Trailing whitespace is removed, and the file will end with a newline.
sourcePkgTemplates.combined_view
— Functioncombined_view(::Plugin, ::Template, pkg::AbstractString) -> Dict{String, Any}
This function combines view
and user_view
for use in text templating. If you're doing manual file creation or text templating (i.e. writing Plugin
s that are not BasicPlugin
s), then you should use this function rather than either of the former two.
sourcePkgTemplates.tags
— Functiontags(::Plugin) -> Tuple{String, String}
Return the delimiters used for text templating. See the Citation
plugin for a rare case where changing the tags is necessary.
By default, the tags are "{{"
and "}}"
.
sourceFor 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
@@ -94,7 +94,7 @@ view(::Citation, t::Template, pkg::AbstractString) = Dict(
"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.
PkgTemplates.source
— Functionsource(::BasicPlugin) -> Union{String, Nothing}
Return the path to a plugin's template file, or nothing
to indicate no file.
By default, nothing
is returned.
sourcePkgTemplates.destination
— Functiondestination(::BasicPlugin) -> String
Return the destination, relative to the package root, of a plugin's configuration file.
This function must be implemented.
sourceNext, 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,
+)
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.
PkgTemplates.source
— Functionsource(::BasicPlugin) -> Union{String, Nothing}
Return the path to a plugin's template file, or nothing
to indicate no file.
By default, nothing
is returned.
sourcePkgTemplates.destination
— Functiondestination(::BasicPlugin) -> String
Return the destination, relative to the package root, of a plugin's configuration file.
This function must be implemented.
sourceNext, 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>>},
@@ -124,4 +124,4 @@ function hook(p::Tests, t::Template, pkg_dir::AbstractString)
invoke(hook, Tuple{BasicPlugin, Template, AbstractString}, p, t, pkg_dir)
# Do some other work.
add_test_dependency()
-end
There is also a default validate
implementation for BasicPlugin
s, which checks that the plugin's source
file exists, and throws an ArgumentError
otherwise. If you want to extend the validation 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.
Predicates
There are a few predicate functions for plugins that are occasionally 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 predicate function to return true
for instances of your type.
PkgTemplates.needs_username
— Functionneeds_username(::Plugin) -> Bool
Determine whether or not a plugin needs a Git hosting service username to function correctly. If you are implementing a plugin that uses the user
field of a Template
, you should implement this function and return true
.
sourcePkgTemplates.is_ci
— Functionis_ci(::Plugin) -> Bool
Determine whether or not a plugin is a CI plugin. If you are adding a CI plugin, you should implement this function and return true
.
sourcePkgTemplates.is_coverage
— Functionis_coverage(::Plugin) -> Bool
Determine whether or not a plugin is a coverage plugin. If you are adding a coverage plugin, you should implement this function and return true
.
sourceFormatting 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.
PkgTemplates.compat_version
— Functioncompat_version(v::VersionNumber) -> String
Format a VersionNumber
to exclude trailing zero components.
sourcePkgTemplates.format_version
— Functionformat_version(v::Union{VersionNumber, AbstractString}) -> String
Strip everything but the major and minor release from a VersionNumber
. Strings are left in their original form.
sourcePkgTemplates.collect_versions
— Functioncollect_versions(t::Template, versions::Vector) -> Vector{String}
Combine t
's Julia version with versions
, and format them as major.minor
. This is useful for creating lists of versions to be included in CI configurations.
sourceSettings
This document was generated with Documenter.jl on Monday 23 March 2020. Using Julia version 1.3.1.
+end
There is also a default validate
implementation for BasicPlugin
s, which checks that the plugin's source
file exists, and throws an ArgumentError
otherwise. If you want to extend the validation 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.
Predicates
There are a few predicate functions for plugins that are occasionally 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 predicate function to return true
for instances of your type.
PkgTemplates.needs_username
— Functionneeds_username(::Plugin) -> Bool
Determine whether or not a plugin needs a Git hosting service username to function correctly. If you are implementing a plugin that uses the user
field of a Template
, you should implement this function and return true
.
PkgTemplates.is_ci
— Functionis_ci(::Plugin) -> Bool
Determine whether or not a plugin is a CI plugin. If you are adding a CI plugin, you should implement this function and return true
.
PkgTemplates.is_coverage
— Functionis_coverage(::Plugin) -> Bool
Determine whether or not a plugin is a coverage plugin. If you are adding a coverage plugin, you should implement this function and return true
.
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.
PkgTemplates.compat_version
— Functioncompat_version(v::VersionNumber) -> String
Format a VersionNumber
to exclude trailing zero components.
PkgTemplates.format_version
— Functionformat_version(v::Union{VersionNumber, AbstractString}) -> String
Strip everything but the major and minor release from a VersionNumber
. Strings are left in their original form.
PkgTemplates.collect_versions
— Functioncollect_versions(t::Template, versions::Vector) -> Vector{String}
Combine t
's Julia version with versions
, and format them as major.minor
. This is useful for creating lists of versions to be included in CI configurations.