Skip to content

Commit

Permalink
Add a PackageCompilerLib plugin
Browse files Browse the repository at this point in the history
* This generates files which facilitate creating C libraries from Julia
  code, using PackageCompiler.jl.
  • Loading branch information
kmsquire committed Jul 29, 2021
1 parent bcea5d2 commit cf65bf7
Show file tree
Hide file tree
Showing 13 changed files with 431 additions and 1 deletion.
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

"""
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")))",
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)-*")
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
67 changes: 67 additions & 0 deletions templates/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
.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()'
# The line below should be removed once https://github.com/JuliaLang/PackageCompiler.jl/pull/490 is merged
# (and a new version of PackageCompiler.jl is released).
$(JULIA) --startup-file=no --project=$(BUILD) -e 'import Pkg; Pkg.add(url="https://github.com/kmsquire/PackageCompiler.jl.git", rev="kms/create_library")'
$(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

0 comments on commit cf65bf7

Please sign in to comment.