PkgTemplates.jl/src/template.jl

307 lines
11 KiB
Julia
Raw Normal View History

2017-12-01 17:33:57 +00:00
import Base.show
2017-08-11 22:18:09 +00:00
"""
Template(; kwargs...) -> Template
2017-08-21 19:53:00 +00:00
Records common information used to generate a package. If you don't wish to manually
create a template, you can use [`interactive_template`](@ref) instead.
2017-08-11 22:18:09 +00:00
# Keyword Arguments
2017-08-23 17:50:52 +00:00
* `user::AbstractString=""`: GitHub username. If left unset, it will try to take the
value of a supplied git config's "github.user" key, then the global git config's
value. If neither is set, an `ArgumentError` is thrown.
**This is case-sensitive for some plugins, so take care to enter it correctly.**
* `host::AbstractString="github.com"`: URL to the code hosting service where your package
2017-08-18 07:08:11 +00:00
will reside. Note that while hosts other than GitHub won't cause errors, they are not
officially supported and they will cause certain plugins will produce incorrect output.
For example, [`AppVeyor`](@ref)'s badge image will point to a GitHub-specific URL,
regardless of the value of `host`.
2017-08-23 17:50:52 +00:00
* `license::AbstractString="MIT"`: Name of the package license. If an empty string is
given, no license is created. [`available_licenses`](@ref) can be used to list all
available licenses, and [`show_license`](@ref) can be used to print out a particular
license's text.
* `authors::Union{AbstractString, Vector{<:AbstractString}}=""`: Names that appear on the
license. Supply a string for one author or an array for multiple. Similarly to `user`,
2017-08-23 17:50:52 +00:00
it will try to take the value of a supplied git config's "user.name" key, then the global
git config's value, if it is left unset.
* `years::Union{Integer, AbstractString}=Dates.year(Dates.today())`: Copyright years on the
license. Can be supplied by a number, or a string such as "2016 - 2017".
* `dir::AbstractString=Pkg.dir()`: Directory in which the package will go. Relative paths
are converted to absolute ones at template creation time.
2017-10-06 12:27:01 +00:00
* `precompile::Bool=true`: Whether or not to enable precompilation in generated packages.
2017-08-11 22:18:09 +00:00
* `julia_version::VersionNumber=VERSION`: Minimum allowed Julia version.
* `requirements::Vector{<:AbstractString}=String[]`: Package requirements. If there are
duplicate requirements with different versions, i.e. ["PkgTemplates", "PkgTemplates
0.1"], an `ArgumentError` is thrown. Each entry in this array will be copied into the
`REQUIRE` file of packages generated with this template.
* `gitconfig::Dict=Dict()`: Git configuration options.
* `plugins::Vector{<:Plugin}=Plugin[]`: A list of `Plugin`s that the package will include.
2017-08-11 22:18:09 +00:00
"""
2017-08-14 18:12:37 +00:00
@auto_hash_equals struct Template
user::AbstractString
host::AbstractString
2017-08-23 17:50:52 +00:00
license::AbstractString
authors::AbstractString
2017-08-11 22:18:09 +00:00
years::AbstractString
dir::AbstractString
2017-10-06 12:27:01 +00:00
precompile::Bool
2017-08-11 22:18:09 +00:00
julia_version::VersionNumber
2017-08-18 07:08:03 +00:00
requirements::Vector{AbstractString}
2017-08-23 17:50:52 +00:00
gitconfig::Dict
2017-08-11 22:18:09 +00:00
plugins::Dict{DataType, Plugin}
function Template(;
user::AbstractString="",
host::AbstractString="https://github.com",
2017-08-22 16:29:53 +00:00
license::Union{AbstractString, Void}="MIT",
2017-08-23 17:50:52 +00:00
authors::Union{AbstractString, Vector{<:AbstractString}}="",
years::Union{Integer, AbstractString}=Dates.year(Dates.today()),
dir::AbstractString=Pkg.dir(),
2017-10-06 12:27:01 +00:00
precompile::Bool=true,
2017-08-11 22:18:09 +00:00
julia_version::VersionNumber=VERSION,
2017-08-23 17:50:52 +00:00
requirements::Vector{<:AbstractString}=String[],
gitconfig::Dict=Dict(),
plugins::Vector{<:Plugin}=Plugin[],
)
# If no username was set, look for one in a supplied git config,
# and then in the global git config.
if isempty(user)
2017-08-23 17:50:52 +00:00
user = get(gitconfig, "github.user", LibGit2.getconfig("github.user", ""))
end
if isempty(user)
throw(ArgumentError("No GitHub username found, set one with user=username"))
end
host = URI(startswith(host, "https://") ? host : "https://$host").host
2017-08-23 17:50:52 +00:00
if !isempty(license) && !isfile(joinpath(LICENSE_DIR, license))
throw(ArgumentError("License '$license' is not available"))
2017-08-11 22:18:09 +00:00
end
# If no author was set, look for one in the supplied git config,
# and then in the global git config.
if isempty(authors)
2017-08-23 17:50:52 +00:00
authors = get(gitconfig, "user.name", LibGit2.getconfig("user.name", ""))
elseif isa(authors, Vector)
2017-08-11 22:18:09 +00:00
authors = join(authors, ", ")
end
years = string(years)
2017-08-14 19:15:53 +00:00
2017-08-25 05:09:49 +00:00
dir = abspath(expanduser(dir))
2017-08-18 07:08:03 +00:00
requirements_dedup = collect(Set(requirements))
diff = length(requirements) - length(requirements_dedup)
names = [tokens[1] for tokens in split.(requirements_dedup)]
if length(names) > length(Set(names))
throw(ArgumentError(
"requirements contains duplicate packages with conflicting versions"
))
elseif diff > 0
warn("Removed $(diff) duplicate$(diff == 1 ? "" : "s") from requirements")
end
2017-08-14 19:15:53 +00:00
plugin_dict = Dict{DataType, Plugin}(typeof(p) => p for p in plugins)
if (length(plugins) != length(plugin_dict))
warn("Plugin list contained duplicates, only the last of each type was kept")
end
2017-08-11 22:18:09 +00:00
new(
2017-10-06 12:27:01 +00:00
user, host, license, authors, years, dir, precompile,
julia_version, requirements_dedup, gitconfig, plugin_dict,
2017-08-11 22:18:09 +00:00
)
end
end
2017-08-21 19:53:00 +00:00
2017-12-01 17:33:57 +00:00
function show(io::IO, t::Template)
maybe_none(s::AbstractString) = isempty(string(s)) ? "None" : string(s)
spc = " "
println(io, "Template:")
println(io, "$spc→ User: $(maybe_none(t.user))")
println(io, "$spc→ Host: $(maybe_none(t.host))")
println(io, "$spc→ License: $(maybe_none(t.license))")
# We don't care about authors or license years if there is no license.
if !isempty(t.license)
# TODO: Authors could be split into multiple lines if there are more than one.
# Maybe the authors field of Template should be an array (or Dict, see #4).
println(io, "$spc→ Authors: $(maybe_none(t.authors))")
println(io, "$spc→ License years: $(maybe_none(t.years))")
end
println(io, "$spc→ Package directory: $(replace(maybe_none(t.dir), homedir(), "~"))")
println(io, "$spc→ Precompilation enabled: $(t.precompile ? "yes" : "no")")
println(io, "$spc→ Minimum Julia version: v$(t.julia_version)")
print(io, "$spc→ Package dependencies: ")
if isempty(t.requirements)
println(io, "None")
else
println(io)
for req in sort(t.requirements)
println(io, "$(spc^2)$req")
end
end
print(io, "$spc→ Git configuration options: ")
if isempty(t.gitconfig)
println(io, "None")
else
println(io)
for k in sort(collect(keys(t.gitconfig)); by=string)
println(io, "$(spc^2)$k = $(t.gitconfig[k])")
end
end
print(io, "$spc→ Plugins: ")
if isempty(t.plugins)
print(io, "None")
else
for plugin in sort(collect(values(t.plugins)); by=string)
println(io)
buf = IOBuffer()
show(buf, plugin)
print(io, "$(spc^2)")
print(io, join(split(String(take!(buf)), "\n"), "\n$(spc^2)"))
end
end
end
2017-08-21 19:53:00 +00:00
"""
2017-08-25 06:25:26 +00:00
interactive_template(; fast::Bool=false) -> Template
2017-08-21 19:53:00 +00:00
Interactively create a [`Template`](@ref). If `fast` is set, defaults will be assumed for
all values except username and plugins.
2017-08-21 19:53:00 +00:00
"""
function interactive_template(; fast::Bool=false)
info("Default values are shown in [brackets]")
2017-08-21 19:53:00 +00:00
# Getting the leaf types in a separate thread eliminates an awkward wait after
# "Select plugins" is printed.
plugin_types = @spawn leaves(Plugin)
kwargs = Dict{Symbol, Any}()
default_user = LibGit2.getconfig("github.user", "")
2017-08-21 19:53:00 +00:00
print("Enter your username [$(isempty(default_user) ? "REQUIRED" : default_user)]: ")
user = readline()
kwargs[:user] = if !isempty(user)
user
elseif !isempty(default_user)
default_user
else
throw(ArgumentError("Username is required"))
end
kwargs[:host] = if fast
"https://github.com"
2017-08-21 19:53:00 +00:00
else
default_host = "github.com"
print("Enter the code hosting service [$default_host]: ")
host = readline()
isempty(host) ? default_host : host
2017-08-21 19:53:00 +00:00
end
kwargs[:license] = if fast
"MIT"
else
println("Select a license:")
io = IOBuffer()
2017-08-23 17:50:52 +00:00
available_licenses(io)
licenses = ["" => "", collect(LICENSES)...]
menu = RadioMenu(["None", split(String(take!(io)), "\n")...])
2017-08-23 17:50:52 +00:00
# If the user breaks out of the menu with Ctrl-c, the result is -1, the absolute
# value of which correponds to no license.
licenses[abs(request(menu))].first
end
# We don't need to ask for authors or copyright years if there is no license,
# because the license is the only place that they matter.
kwargs[:authors] = if fast || isempty(kwargs[:license])
LibGit2.getconfig("user.name", "")
else
default_authors = LibGit2.getconfig("user.name", "")
default_str = isempty(default_authors) ? "None" : default_authors
print("Enter the package author(s) [$default_str]: ")
authors = readline()
isempty(authors) ? default_authors : authors
end
kwargs[:years] = if fast || isempty(kwargs[:license])
2017-08-23 17:50:52 +00:00
Dates.year(Dates.today())
else
2017-08-23 17:50:52 +00:00
default_years = Dates.year(Dates.today())
print("Enter the copyright year(s) [$default_years]: ")
years = readline()
isempty(years) ? default_years : years
end
kwargs[:dir] = if fast
Pkg.dir()
else
default_dir = Pkg.dir()
print("Enter the path to the package directory [$default_dir]: ")
dir = readline()
isempty(dir) ? default_dir : dir
end
2017-10-06 12:27:01 +00:00
kwargs[:precompile] = if fast
true
else
print("Enable precompilation? [yes]: ")
!in(uppercase(readline()), ["N", "NO", "F", "FALSE"])
end
kwargs[:julia_version] = if fast
VERSION
else
default_julia_version = VERSION
print("Enter the minimum Julia version [$(version_floor(default_julia_version))]: ")
julia_version = readline()
isempty(julia_version) ? default_julia_version : VersionNumber(julia_version)
end
2017-08-21 19:53:00 +00:00
kwargs[:requirements] = if fast
String[]
else
print("Enter any Julia package requirements, (separated by spaces) []: ")
String.(split(readline()))
end
2017-08-23 17:50:52 +00:00
kwargs[:gitconfig] = if fast
Dict()
else
2017-08-23 17:50:52 +00:00
gitconfig = Dict()
print("Enter any Git key-value pairs (one per line, separated by spaces) [None]: ")
while true
2017-10-06 14:02:25 +00:00
line = readline()
isempty(line) && break
tokens = split(line, " ", limit=2)
2017-08-23 17:50:52 +00:00
if haskey(gitconfig, tokens[1])
warn("Duplicate key '$(tokens[1])': Replacing old value '$(tokens[2])'")
end
2017-08-23 17:50:52 +00:00
gitconfig[tokens[1]] = tokens[2]
2017-08-21 19:53:00 +00:00
end
2017-08-23 17:50:52 +00:00
gitconfig
2017-08-21 19:53:00 +00:00
end
println("Select plugins:")
# Only include plugin types which have an `interactive` method.
plugin_types = filter(t -> method_exists(interactive, (Type{t},)), fetch(plugin_types))
2017-08-21 19:53:00 +00:00
type_names = map(t -> split(string(t), ".")[end], plugin_types)
menu = MultiSelectMenu(String.(type_names); pagesize=length(type_names))
selected = collect(request(menu))
kwargs[:plugins] = Vector{Plugin}(
map(t -> interactive(t), getindex(plugin_types, selected))
)
2017-08-21 19:53:00 +00:00
return Template(; kwargs...)
end
"""
leaves(t:Type) -> Vector{DataType}
Get all concrete subtypes of `t`.
"""
2017-08-23 17:50:52 +00:00
leaves(t::Type)::Vector{DataType} = isleaftype(t) ? [t] : vcat(leaves.(subtypes(t))...)