Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a PackageCompilerLib plugin #299

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/src/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ ColPracBadge
Develop
Citation
RegisterAction
PackageCompilerLib
```

## A More Complicated Example
Expand Down Expand Up @@ -125,6 +126,21 @@ Template(;
)
```

Here's one that generates code to build a C library using PackageCompiler.jl.

```julia
Template(;
user="my-username",
dir="~/MyLib",
authors="Wiley Coyote",
julia=v"1.6",
plugins=[
PackageCompilerLib(lib_name="mylib"),
],
)
```


## Custom Template Files

!!! note "Templates vs Templating"
Expand Down
1 change: 1 addition & 0 deletions src/PkgTemplates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export
License,
Logo,
NoDeploy,
PackageCompilerLib,
ProjectFile,
Readme,
RegisterAction,
Expand Down
1 change: 1 addition & 0 deletions src/plugin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,4 @@ include(joinpath("plugins", "citation.jl"))
include(joinpath("plugins", "documenter.jl"))
include(joinpath("plugins", "badges.jl"))
include(joinpath("plugins", "register.jl"))
include(joinpath("plugins", "package_compiler_lib.jl"))
119 changes: 119 additions & 0 deletions src/plugins/package_compiler_lib.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using PkgTemplates: @plugin, @with_kw_noshow, Plugin

# Used to generate the library name
camel_to_snake_case(str::AbstractString) = replace(str, r"([a-z])([A-Z]+)" => s"\1_\2") |> lowercase
kmsquire marked this conversation as resolved.
Show resolved Hide resolved

"""
PackageCompilerLib(;
lib_name=nothing,
build_jl="$(contractuser(default_file("build", "build.jl")))",
generate_precompile_jl="$(contractuser(default_file("build", "generate_precompile.jl")))",
additional_precompile_jl="$(contractuser(default_file("build", "additional_precompile.jl")))",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the use case for this additional_precompile file?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generate_precompile.jl runs code which is traced to determine which functions to precompile.
additional_precompile.jl contains explicit precompile statements for compiling versions of functions that were not captured by generate_precompile.jl

Both files exist under the templates/build directory.

Do you need/want me to add any of this description to the PR?

install_sh="$(contractuser(default_file("build", "install.sh")))",
install_txt="$(contractuser(default_file("build", "INSTALL.txt")))",
lib_h="$(contractuser(default_file("build", "lib.h")))",
project_toml="$(contractuser(default_file("build", "Project.toml")))",
makefile="$(contractuser(default_file("Makefile")))",
additional_gitignore=[],
)

Adds files which facilitate the creation of a C-library from the generated project.

See [PackageCompiler.jl](https://github.com/JuliaLang/PackageCompiler.jl) for more information.

## Keyword Arguments
- `lib_name::Union{String, Nothing}`: Name of the library to generate. If `nothing`,
defaults to the snake-case version of the package name.
- `build_jl::String`: The file used to generate the C library. Calls out to `PackageCompiler.jl`.
- `generate_precompile_jl::String`: File with a code which will be used to generate precompile statements.
- `additional_precompile_jl::String`: File with additional precompile statements.
- `install_sh::String`: Installation script for the generated library.
- `install_txt::String`: Installation instructions for the generated library.
- `lib_h::String`: C header file for the generated library.
- `project_toml::String`: Julia `Project.toml` for the build directory.
- `makefile::String`: Makefile with targets to help build the C library.
- `additional_gitignore::Vector{String}`: Additional strings to add to .gitignore.

"""
@plugin struct PackageCompilerLib <: Plugin
lib_name::Union{String, Nothing} = nothing
build_jl::String = default_file("build", "build.jl")
generate_precompile_jl::String = default_file("build", "generate_precompile.jl")
additional_precompile_jl::String = default_file("build", "additional_precompile.jl")
install_sh::String = default_file("build", "install.sh")
install_txt::String = default_file("build", "INSTALL.txt")
lib_h::String = default_file("build", "lib.h")
project_toml::String = default_file("build", "Project.toml")
makefile::String = default_file("Makefile")
additional_gitignore::Vector{String} = []
end

function validate(p::PackageCompilerLib, ::Template)
isfile(p.build_jl) || throw(ArgumentError("PackageCompilerLib: $(p.build_jl) does not exist"))
isfile(p.additional_precompile_jl) || throw(ArgumentError("PackageCompilerLib: $(p.additional_precompile_jl) does not exist"))
isfile(p.generate_precompile_jl) || throw(ArgumentError("PackageCompilerLib: $(p.generate_precompile_jl) does not exist"))
isfile(p.install_sh) || throw(ArgumentError("PackageCompilerLib: $(p.install_sh) does not exist"))
isfile(p.install_txt) || throw(ArgumentError("PackageCompilerLib: $(p.install_txt) does not exist"))
isfile(p.lib_h) || throw(ArgumentError("PackageCompilerLib: $(p.lib_h) does not exist"))
isfile(p.project_toml) || throw(ArgumentError("PackageCompilerLib: $(p.project_toml) does not exist"))
isfile(p.makefile) || throw(ArgumentError("PackageCompilerLib: $(p.makefile) does not exist"))
end

view(p::PackageCompilerLib, t::Template, pkg::AbstractString) = Dict(
"PKG" => pkg,
"LIB" => lib_name(p, pkg),
)

function lib_name(p::PackageCompilerLib, pkg::AbstractString)
p.lib_name !== nothing ? p.lib_name : camel_to_snake_case(pkg)
end

function gitignore(p::PackageCompilerLib)
ignore_files = ["build/Manifest.toml", "target"]
append!(ignore_files, p.additional_gitignore)
return ignore_files
end

function prehook(p::PackageCompilerLib, t::Template, pkg_dir::AbstractString)
# The library name and version are used as the default Makefile output target
# (e.g. the library is built under mylib-0.1.0/).
# If we use a the default library name, p.lib_name === nothing, then the
# gitignore() function won't have access to the default library name.
# To work around this, we get the library name and store the output target directory
# glob here in the prehook, so it can be added by gitignore() later.
pkg = basename(pkg_dir)
library_name = lib_name(p, pkg)
push!(p.additional_gitignore, "/$(library_name)-*")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this workaround the main purpose of having the additional_gitignore file? It might be simpler to just force a default library name (e.g. MyLibrary).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the main purpose. Your suggestion is certainly easier--I was just trying to have a complete solution out of the box.

I can remove the additional_gitignore member if you prefer.

end

function hook(p::PackageCompilerLib, t::Template, pkg_dir::AbstractString)
build_dir = joinpath(pkg_dir, "build")
pkg = basename(pkg_dir)
library_name = lib_name(p, pkg)

build_jl = render_file(p.build_jl, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(build_dir, "build.jl"), build_jl)

additional_precompile_jl = render_file(p.additional_precompile_jl, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(build_dir, "additional_precompile.jl"), additional_precompile_jl)

generate_precompile_jl = render_file(p.generate_precompile_jl, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(build_dir, "generate_precompile.jl"), generate_precompile_jl)

install_sh = render_file(p.install_sh, combined_view(p, t, pkg), tags(p))
install_sh_target = joinpath(build_dir, "install.sh")
gen_file(install_sh_target, install_sh)
chmod(install_sh_target, 0o755)

install_txt = render_file(p.install_txt, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(build_dir, "INSTALL.txt"), install_txt)

lib_h = render_file(p.lib_h, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(build_dir, "$(library_name).h"), lib_h)

project_toml = render_file(p.project_toml, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(build_dir, "Project.toml"), project_toml)

makefile = render_file(p.makefile, combined_view(p, t, pkg), tags(p))
gen_file(joinpath(pkg_dir, "Makefile"), makefile)
end
64 changes: 64 additions & 0 deletions templates/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
.PHONY: build clean dist instantiate install uninstall

ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
BUILD := $(ROOT_DIR)/build

JULIA ?= julia
JULIA_DIR := $(shell $(JULIA) --startup-file=no -e 'print(dirname(Sys.BINDIR))')
DLEXT := $(shell $(JULIA) --startup-file=no -e 'using Libdl; print(Libdl.dlext)')
VERSION := $(shell sed -n 's/^version *= *"\(.*\)"/\1/p' $(ROOT_DIR)/Project.toml)
OS := $(shell uname)
DEPS := $(shell find . build src -maxdepth 1 -and \( -name \*.toml -or -name \*.jl -or -name Makefile \) -and -not -type l)

NAME := {{{LIB}}}
NAME_VERSION := $(NAME)-$(VERSION)
_TAR_GZ := $(NAME_VERSION)-$(OS).tar.gz

DEST_DIR ?= $(NAME_VERSION)
DEST_BASENAME := $(shell basename $(DEST_DIR))
TAR_GZ := $(abspath $(DEST_DIR)/../$(_TAR_GZ))
OUT_DIR := $(DEST_DIR)/$(NAME)
BIN_DIR := $(OUT_DIR)/bin
INCLUDE_DIR = $(OUT_DIR)/include
LIB_DIR := $(OUT_DIR)/lib
PREFIX ?= $${HOME}/.local

LIB_NAME := lib$(NAME).$(DLEXT)
INCLUDES = $(INCLUDE_DIR)/julia_init.h $(INCLUDE_DIR)/$(NAME).h
LIB_PATH := $(LIB_DIR)/$(LIB_NAME)

.DEFAULT_GOAL := build

$(LIB_PATH) $(INCLUDES): $(BUILD)/build.jl $(DEPS)
$(JULIA) --startup-file=no --project=. -e 'using Pkg; Pkg.instantiate()'
$(JULIA) --startup-file=no --project=$(BUILD) -e 'using Pkg; Pkg.instantiate()'
$(JULIA) --startup-file=no --project=$(BUILD) $< $(OUT_DIR)
# Replace the previous line with the line below to enable verbose debugging during package build
# JULIA_DEBUG=PackageCompiler $(JULIA) --startup-file=no --project=$(BUILD) $< $(OUT_DIR)

build: $(LIB_PATH) $(INCLUDES) README.md $(BUILD)/INSTALL.txt $(BUILD)/install.sh
cp README.md $(BUILD)/INSTALL.txt $(BUILD)/install.sh $(DEST_DIR)
cd $(DEST_DIR) && ln -sf install.sh uninstall.sh

install: build
cd $(DEST_DIR) && PREFIX=$(PREFIX) ./install.sh

uninstall: build
cd $(DEST_DIR) && ./uninstall.sh

$(TAR_GZ): $(LIB_PATH) $(INCLUDES) Project.toml Manifest.toml $(BUILD)/*.jl $(BUILD)/*.toml
cd $(DEST_DIR)/.. && tar -zcf $(TAR_GZ) \
$(DEST_BASENAME)/README.md \
$(DEST_BASENAME)/INSTALL.txt \
$(DEST_BASENAME)/install.sh \
$(DEST_BASENAME)/uninstall.sh \
$(DEST_BASENAME)/$(NAME)

dist: $(TAR_GZ)

instantiate:
$(JULIA) --startup-file=no --project=. -e "import Pkg; Pkg.instantiate()"
$(JULIA) --startup-file=no --project=build -e "import Pkg; Pkg.instantiate()"

clean:
$(RM) -Rf $(OUT_DIR)
18 changes: 18 additions & 0 deletions templates/build/INSTALL.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

By default, install.sh installs files under $HOME/.local.

To install, optionally set the following environment variables and run
install.sh.

NAME project name (default: {{{LIB}}})
SOURCE_DIR directory whose contents to copy (default: {{{LIB}}})
PREFIX destination prefix (default: $HOME/.local)

Examples:

$ ./install.sh # install in ~/.local
$ SOURCE_DIR={{{LIB}}} install.sh # install in ~/.local ({{{LIB}}} is the default)
$ PREFIX=/usr/local ./install.sh # install directly in /usr/local (no symlinks)

To uninstall, make sure PREFIX and NAME have the same values used during
install and run uninstall.sh.
3 changes: 3 additions & 0 deletions templates/build/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[deps]
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
3 changes: 3 additions & 0 deletions templates/build/additional_precompile.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Add manual precompile statements here

# precompile(Tuple{typeof({{{PKG}}}.increment64), Clong})
32 changes: 32 additions & 0 deletions templates/build/build.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import PackageCompiler, TOML

if length(ARGS) < 1 || length(ARGS) > 2
println("Usage: julia $PROGRAM_FILE target_dir [major|minor]")
println()
println("where:")
println(" target_dir is the directory to use to create the library bundle")
println(" [major|minor] is the (optional) compatibility version (default: major).")
println(" Use 'minor' if you use new/non-backwards-compatible functionality.")
println()
println("[major|minor] is only useful on OSX.")
exit(1)
end

const build_dir = @__DIR__
const target_dir = ARGS[1]
const project_toml = realpath(joinpath(build_dir, "..", "Project.toml"))
const version = VersionNumber(TOML.parsefile(project_toml)["version"])

const compatibility = length(ARGS) == 2 ? ARGS[2] : "major"

PackageCompiler.create_library(".", target_dir;
lib_name="{{{LIB}}}",
precompile_execution_file=[joinpath(build_dir, "generate_precompile.jl")],
precompile_statements_file=[joinpath(build_dir, "additional_precompile.jl")],
incremental=false,
filter_stdlibs=true,
header_files = [joinpath(build_dir, "{{{LIB}}}.h")],
force=true,
version=version,
compat_level=compatibility,
)
12 changes: 12 additions & 0 deletions templates/build/generate_precompile.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Add code to generate precompile statements here

using {{{PKG}}}

# function count_to_ten()
# count = zero(Int32)
# while count < 10
# count = increment32(count)
# end
# end

# count_to_ten()
Loading