PkgTemplates.jl/src/generate.jl
2017-12-01 12:12:37 +00:00

433 lines
13 KiB
Julia

"""
generate(
pkg_name::AbstractString,
t::Template;
force::Bool=false,
ssh::Bool=false,
backup_dir::AbstractString="",
) -> Void
Generate a package named `pkg_name` from `template`.
# Keyword Arguments
* `force::Bool=false`: Whether or not to overwrite old packages with the same name.
* `ssh::Bool=false`: Whether or not to use SSH for the remote.
* `backup_dir::AbstractString=""`: Directory in which to store the generated package if
`t.dir` is not a valid directory. If left unset, a temporary directory will be created
(this keyword is mostly for internal usage).
# Notes
The package is generated entirely in a temporary directory and only moved into
`joinpath(t.dir, pkg_name)` at the very end. In the case of an error, the temporary
directory will contain leftovers, but the destination directory will remain untouched
(this is especially helpful when `force=true`).
"""
function generate(
pkg_name::AbstractString,
t::Template;
force::Bool=false,
ssh::Bool=false,
backup_dir::AbstractString="",
)
mktempdir() do temp_dir
generate(pkg_name, t, temp_dir; force=force, ssh=ssh, backup_dir=backup_dir)
end
end
function generate(
pkg_name::AbstractString,
t::Template,
dir::AbstractString;
force::Bool=false,
ssh::Bool=false,
backup_dir::AbstractString="",
)
pkg_name = Pkg.splitjl(pkg_name)
pkg_dir = joinpath(t.dir, pkg_name)
temp_pkg_dir = joinpath(dir, pkg_name)
if !force && ispath(pkg_dir)
throw(ArgumentError(
"Path '$pkg_dir' already exists, use force=true to overwrite it."
))
end
# Initialize the repo and configure it.
repo = LibGit2.init(temp_pkg_dir)
info("Initialized git repo at $temp_pkg_dir")
LibGit2.with(LibGit2.GitConfig, repo) do cfg
for (key, val) in t.gitconfig
LibGit2.set!(cfg, key, val)
end
end
!isempty(t.gitconfig) && info("Applied git configuration")
LibGit2.commit(repo, "Initial commit")
info("Made empty initial commit")
rmt = if ssh
"git@$(t.host):$(t.user)/$pkg_name.jl.git"
else
"https://$(t.host)/$(t.user)/$pkg_name.jl"
end
# We need to set the remote in a strange way, see #8.
close(LibGit2.GitRemote(repo, "origin", rmt))
info("Set remote origin to $rmt")
# Create the gh-pages branch if necessary.
if haskey(t.plugins, GitHubPages)
LibGit2.branch!(repo, "gh-pages")
LibGit2.commit(repo, "Empty initial commit")
info("Created empty gh-pages branch")
LibGit2.branch!(repo, "master")
end
# Generate the files.
files = vcat(
gen_entrypoint(dir, pkg_name, t),
gen_tests(dir, pkg_name, t),
gen_require(dir, pkg_name, t),
gen_readme(dir, pkg_name, t),
gen_gitignore(dir, pkg_name, t),
gen_license(dir, pkg_name, t),
vcat([gen_plugin(plugin, t, dir, pkg_name) for plugin in values(t.plugins)]...),
)
LibGit2.add!(repo, files...)
info("Staged $(length(files)) files/directories: $(join(files, ", "))")
LibGit2.commit(repo, "Files generated by PkgTemplates")
info("Committed files generated by PkgTemplates")
multiple_branches = length(collect(LibGit2.GitBranchIter(repo))) > 1
try
mkpath(dirname(pkg_dir))
mv(temp_pkg_dir, pkg_dir; remove_destination=force)
info("Moved temporary package directory into $(t.dir)/")
catch # Likely cause is that t.dir can't be created (is a file, etc.).
# We're just going to trust that backup_dir is a valid directory.
backup_dir = if isempty(backup_dir)
mktempdir()
else
abspath(backup_dir)
end
mkpath(backup_dir)
mv(temp_pkg_dir, joinpath(backup_dir, pkg_name))
warn("$pkg_name couldn't be moved into $pkg_dir, left package in $backup_dir")
end
info("Finished")
if multiple_branches
warn("Remember to push all created branches to your remote: git push --all")
end
end
function generate(
t::Template,
pkg_name::AbstractString;
force::Bool=false,
ssh::Bool=false,
backup_dir::AbstractString="",
)
generate(pkg_name, t; force=force, ssh=ssh, backup_dir=backup_dir)
end
"""
generate_interactive(
pkg_name::AbstractString;
force::Bool=false,
ssh::Bool=false,
backup_dir::AbstractString="",
fast::Bool=false,
) -> Void
Interactively create a template, and then generate a package with it. Arguments and
keywords are used in the same way as in [`generate`](@ref) and
[`interactive_template`](@ref).
"""
function generate_interactive(
pkg_name::AbstractString;
force::Bool=false,
ssh::Bool=false,
backup_dir::AbstractString="",
fast::Bool=false,
)
generate(
pkg_name,
interactive_template(; fast=fast);
force=force,
ssh=ssh,
backup_dir=backup_dir,
)
end
"""
gen_entrypoint(
dir::AbstractString,
pkg_name::AbstractString,
template::Template,
) -> Vector{String}
Create the module entrypoint in the temp package directory.
# Arguments
* `dir::AbstractString`: The directory in which the files will be generated. Note that
this will be joined to `pkg_name`.
* `pkg_name::AbstractString`: Name of the package.
* `template::Template`: The template whose entrypoint we are generating.
Returns an array of generated file/directory names.
"""
function gen_entrypoint(dir::AbstractString, pkg_name::AbstractString, template::Template)
text = template.precompile ? "__precompile__()\n" : ""
text *= """
module $pkg_name
# Package code goes here.
end
"""
gen_file(joinpath(dir, pkg_name, "src", "$pkg_name.jl"), text)
return ["src/"]
end
"""
gen_tests(
dir::AbstractString,
pkg_name::AbstractString,
template::Template,
) -> Vector{String}
Create the test directory and entrypoint in the temp package directory.
# Arguments
* `dir::AbstractString`: The directory in which the files will be generated. Note that
this will be joined to `pkg_name`.
* `pkg_name::AbstractString`: Name of the package.
* `template::Template`: The template whose tests we are generating.
Returns an array of generated file/directory names.
"""
function gen_tests(dir::AbstractString, pkg_name::AbstractString, template::Template)
text = """
using $pkg_name
using Base.Test
@testset "$pkg_name.jl" begin
# Write your own tests here.
@test 1 == 2
end
"""
gen_file(joinpath(dir, pkg_name, "test", "runtests.jl"), text)
return ["test/"]
end
"""
gen_require(
dir::AbstractString,
pkg_name::AbstractString,
template::Template,
) -> Vector{String}
Create the `REQUIRE` file in the temp package directory.
# Arguments
* `dir::AbstractString`: The directory in which the files will be generated. Note that
this will be joined to `pkg_name`.
* `pkg_name::AbstractString`: Name of the package.
* `template::Template`: The template whose REQUIRE we are generating.
Returns an array of generated file/directory names.
"""
function gen_require(dir::AbstractString, pkg_name::AbstractString, template::Template)
text = "julia $(version_floor(template.julia_version))\n"
text *= join(template.requirements, "\n")
gen_file(joinpath(dir, pkg_name, "REQUIRE"), text)
return ["REQUIRE"]
end
"""
gen_readme(
dir::AbstractString,
pkg_name::AbstractString,
template::Template,
) -> Vector{String}
Create a README in the temp package directory with badges for each enabled plugin.
# Arguments
* `dir::AbstractString`: The directory in which the files will be generated. Note that
this will be joined to `pkg_name`.
* `pkg_name::AbstractString`: Name of the package.
* `template::Template`: The template whose README we are generating.
Returns an array of generated file/directory names.
"""
function gen_readme(dir::AbstractString, pkg_name::AbstractString, template::Template)
text = "# $pkg_name\n"
done = []
# Generate the ordered badges first, then add any remaining ones to the right.
for plugin_type in BADGE_ORDER
if haskey(template.plugins, plugin_type)
text *= "\n"
text *= join(
badges(template.plugins[plugin_type], template.user, pkg_name),
"\n",
)
push!(done, plugin_type)
end
end
for plugin_type in setdiff(keys(template.plugins), done)
text *= "\n"
text *= join(
badges(template.plugins[plugin_type], template.user, pkg_name),
"\n",
)
end
gen_file(joinpath(dir, pkg_name, "README.md"), text)
return ["README.md"]
end
"""
gen_gitignore(
dir::AbstractString,
pkg_name::AbstractString,
template::Template,
) -> Vector{String}
Create a `.gitignore` in the temp package directory.
# Arguments
* `dir::AbstractString`: The directory in which the files will be generated. Note that
this will be joined to `pkg_name`.
* `pkg_name::AbstractString`: Name of the package.
* `template::Template`: The template whose .gitignore we are generating.
Returns an array of generated file/directory names.
"""
function gen_gitignore(dir::AbstractString, pkg_name::AbstractString, template::Template)
seen = [".DS_Store"]
patterns = vcat([plugin.gitignore for plugin in values(template.plugins)]...)
for pattern in patterns
if !in(pattern, seen)
push!(seen, pattern)
end
end
text = join(seen, "\n")
gen_file(joinpath(dir, pkg_name, ".gitignore"), text)
return [".gitignore"]
end
"""
gen_license(
dir::AbstractString,
pkg_name::AbstractString,
template::Template,
) -> Vector{String}
Create a license in the temp package directory.
# Arguments
* `dir::AbstractString`: The directory in which the files will be generated. Note that
this will be joined to `pkg_name`.
* `pkg_name::AbstractString`: Name of the package.
* `template::Template`: The template whose LICENSE we are generating.
Returns an array of generated file/directory names.
"""
function gen_license(dir::AbstractString, pkg_name::AbstractString, template::Template)
if isempty(template.license)
return String[]
end
text = "Copyright (c) $(template.years) $(template.authors)\n"
text *= read_license(template.license)
gen_file(joinpath(dir, pkg_name, "LICENSE"), text)
return ["LICENSE"]
end
"""
gen_file(file_path::AbstractString, text::AbstractString) -> Int
Create a new file containing some given text. Always ends the file with a newline.
# Arguments
* `file::AbstractString`: Path to the file to be created.
* `text::AbstractString`: Text to write to the file.
Returns the number of bytes written to the file.
"""
function gen_file(file::AbstractString, text::AbstractString)
mkpath(dirname(file))
if !endswith(text , "\n")
text *= "\n"
end
open(file, "w") do fp
return write(fp, text)
end
end
"""
version_floor(v::VersionNumber=VERSION) -> String
Format the given Julia version.
# Keyword arguments
* `v::VersionNumber=VERSION`: Version to floor.
Returns "major.minor" for the most recent release version relative to v. For prereleases
with v.minor == v.patch == 0, returns "major.minor-".
"""
function version_floor(v::VersionNumber=VERSION)
if isempty(v.prerelease) || v.patch > 0
return "$(v.major).$(v.minor)"
else
return "$(v.major).$(v.minor)-"
end
end
"""
substitute(template::AbstractString, view::Dict{String, Any}) -> String
Replace placeholders in `template` with values in `view` via
[`Mustache`](https://github.com/jverzani/Mustache.jl). `template` is not modified.
For information on how to structure `template`, see "Defining Template Files" section in
[Custom Plugins](@ref).
**Note**: Conditionals in `template` without a corresponding key in `view` won't error,
but will simply be evaluated as false.
"""
substitute(template::AbstractString, view::Dict{String, Any}) = render(template, view)
"""
substitute(
template::AbstractString,
pkg_template::Template;
view::Dict{String, Any}=Dict{String, Any}(),
) -> String
Replace placeholders in `template`, using some default replacements based on the
`pkg_template` and additional ones in `view`. `template` is not modified.
"""
function substitute(
template::AbstractString,
pkg_template::Template;
view::Dict{String, Any}=Dict{String, Any}(),
)
# Don't use version_floor here because we don't want the trailing '-' on prereleases.
v = pkg_template.julia_version
d = Dict{String, Any}(
"USER" => pkg_template.user,
"VERSION" => "$(v.major).$(v.minor)",
"DOCUMENTER" => any(isa(p, Documenter) for p in values(pkg_template.plugins)),
"CODECOV" => haskey(pkg_template.plugins, CodeCov),
"COVERALLS" => haskey(pkg_template.plugins, Coveralls),
)
# d["AFTER"] is true whenever something needs to occur in a CI "after_script".
d["AFTER"] = d["DOCUMENTER"] || d["CODECOV"] || d["COVERALLS"]
return substitute(template, merge(d, view))
end