diff --git a/docs/src/user.md b/docs/src/user.md index 21ab78a2..ab72955c 100644 --- a/docs/src/user.md +++ b/docs/src/user.md @@ -87,6 +87,7 @@ ColPracBadge Develop Citation RegisterAction +PackageCompilerLib ``` ## A More Complicated Example @@ -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" diff --git a/src/PkgTemplates.jl b/src/PkgTemplates.jl index 4f581074..0d7a3f5c 100644 --- a/src/PkgTemplates.jl +++ b/src/PkgTemplates.jl @@ -34,6 +34,7 @@ export License, Logo, NoDeploy, + PackageCompilerLib, ProjectFile, Readme, RegisterAction, diff --git a/src/plugin.jl b/src/plugin.jl index c6c6eed4..e7e1eff6 100644 --- a/src/plugin.jl +++ b/src/plugin.jl @@ -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")) diff --git a/src/plugins/package_compiler_lib.jl b/src/plugins/package_compiler_lib.jl new file mode 100644 index 00000000..21f6e61a --- /dev/null +++ b/src/plugins/package_compiler_lib.jl @@ -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 diff --git a/templates/Makefile b/templates/Makefile new file mode 100644 index 00000000..c67e13c9 --- /dev/null +++ b/templates/Makefile @@ -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) diff --git a/templates/build/INSTALL.txt b/templates/build/INSTALL.txt new file mode 100644 index 00000000..07b2141e --- /dev/null +++ b/templates/build/INSTALL.txt @@ -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. diff --git a/templates/build/Project.toml b/templates/build/Project.toml new file mode 100644 index 00000000..3d67fbb4 --- /dev/null +++ b/templates/build/Project.toml @@ -0,0 +1,3 @@ +[deps] +PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d" +TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" diff --git a/templates/build/additional_precompile.jl b/templates/build/additional_precompile.jl new file mode 100644 index 00000000..6aa03a8f --- /dev/null +++ b/templates/build/additional_precompile.jl @@ -0,0 +1,3 @@ +## Add manual precompile statements here + +# precompile(Tuple{typeof({{{PKG}}}.increment64), Clong}) diff --git a/templates/build/build.jl b/templates/build/build.jl new file mode 100644 index 00000000..a3ea6ba6 --- /dev/null +++ b/templates/build/build.jl @@ -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, + ) diff --git a/templates/build/generate_precompile.jl b/templates/build/generate_precompile.jl new file mode 100644 index 00000000..9a3de247 --- /dev/null +++ b/templates/build/generate_precompile.jl @@ -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() diff --git a/templates/build/install.sh b/templates/build/install.sh new file mode 100755 index 00000000..04965db2 --- /dev/null +++ b/templates/build/install.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# install/uninstall script +# To use as an install script, 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:~/.local) +# +# To use as an uninstall script, add a symlink from uninstall.sh to install.sh + +############# +# Variables +############# + +OS=$(uname -s) +SCRIPT_NAME=$(basename $0) +SCRIPT_NAME=${SCRIPT_NAME%.*} +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +DEFAULT_PREFIX=${HOME}/.local + +if [ ${SCRIPT_NAME} == "uninstall" ]; then + INSTALL_ECHO=false +else + INSTALL_ECHO=echo +fi + +: "${NAME={{{LIB}}}}" +: "${SOURCE_DIR=${SCRIPT_DIR}/${NAME}}" +: "${PREFIX=${DEFAULT_PREFIX}}" + +SHARE_DIR=${PREFIX}/share/${NAME} + +FILES=${SHARE_DIR}/${NAME}-files.lst +DIRS=${SHARE_DIR}/${NAME}-dirs.lst + +################# +# Functions +################# + +die() { echo -e "\033[31m$*\033[0m"; exit 1; } + +remove_files() { + type=$1 + files=$2 + if [ -f ${files} ]; then + echo "Removing old $type (${files})" + cat ${files} | xargs rm -f + rm -f ${files} || die "Unable to remove ${files}" + fi +} + +remove_dirs() { + type=$1 + dirs=$2 + if [ -f ${dirs} ]; then + echo "Removing old $type directories (${dirs})" + cat ${dirs} | xargs rmdir -p > /dev/null 2>&1 + rm -f ${dirs} || die "Unable to remove ${dirs}" + fi +} + + +prefix_in_lib_path() { + ldconfig -N -v 2> /dev/null | grep : | sed -e 's/://' | grep -x "${PREFIX}/lib" > /dev/null 2>&1 +} + +update_lib_search_path() { + if [ $OS == "Linux" ]; then + if prefix_in_lib_path; then + echo "Running ldconfig" + ldconfig || sudo ldconfig + else + ${INSTALL_ECHO} "Be sure to add $PREFIX/lib to LD_LIBRARY_PATH" + fi + else + if [ $OS == "Darwin" ]; then + ${INSTALL_ECHO} "Be sure to add $PREFIX/lib to DYLD_FALLBACK_LIBRARY_PATH if it is not a standard library path" + fi + fi +} + +sudo_mkdir() { + dir=$1 + + echo "Attempting to sudo mkdir $dir" + group=$(id -gn $USER) + sudo mkdir -p $dir && sudo chown $USER:$group $dir +} + +check_dest_dirs () { + prefix=$1 + source_dir=$2 + + # Create $prefix if necessary + mkdir -p ${prefix} > /dev/null 2>&1 + if [ ! -d ${prefix} ]; then + die "Unable to find or create ${prefix} directory" + fi + + for dir in $(cd ${source_dir} && find . -maxdepth 1 -mindepth 1 -type d); do + path=${prefix}/${dir#./} + echo -n "Checking ability to create and write to $path... " + + RMDIR=0 + + if [ ! -d ${path} ]; then + mkdir -p ${path} > /dev/null 2>&1 && RMDIR=1 || sudo_mkdir ${path} + else + RMDIR=0 + fi + + TEST_FILE=${path}/__${NAME}__ + touch ${TEST_FILE} > /dev/null 2>&1 || die "You don't have permission to write to ${path}" + rm -f ${TEST_FILE} > /dev/null 2>&1 || die "Unable to remove ${TEST_FILE}" + + if [ $RMDIR ]; then + rmdir ${path} > /dev/null 2>&1 + fi + + echo "success" + done +} + +########### +# Main +########### + +# Remove old files +remove_files files ${FILES} +remove_dirs "" ${DIRS} + +# Remove share directory (where file/dir lists are stored) +rmdir -p ${SHARE_DIR} > /dev/null 2>&1 + +# If we're just uninstalling, stop here +if [ ${SCRIPT_NAME} == "uninstall" ]; then + update_lib_search_path + exit 0 +fi + +# Create $PREFIX if necessary +check_dest_dirs ${PREFIX} ${SOURCE_DIR} + +mkdir -p ${SHARE_DIR} > /dev/null 2>&1 || die "Unable to create ${SHARE_DIR}" + +echo "Copying files to ${PREFIX}" +(cd ${SOURCE_DIR} && find * -type d) | xargs -n1 -I{} echo ${PREFIX}/{} > ${DIRS} +(cd ${SOURCE_DIR} && find * -type f) | xargs -n1 -I{} echo ${PREFIX}/{} > ${FILES} +(cd ${SOURCE_DIR} && find * -type l) | xargs -n1 -I{} echo ${PREFIX}/{} >> ${FILES} +cp -a ${SOURCE_DIR}/* ${PREFIX}/ || die "Unable to copy files" + +update_lib_search_path diff --git a/templates/build/lib.h b/templates/build/lib.h new file mode 100644 index 00000000..e51a1cc6 --- /dev/null +++ b/templates/build/lib.h @@ -0,0 +1,4 @@ +// Create a c-style header file for the functions exported by your library + +// int increment32(int); +// long increment64(long); diff --git a/test/reference.jl b/test/reference.jl index d7fa08e5..157aae84 100644 --- a/test/reference.jl +++ b/test/reference.jl @@ -89,7 +89,8 @@ end @testset "All plugins" begin test_all("AllPlugins"; authors=USER, plugins=[ AppVeyor(), CirrusCI(), Citation(), Codecov(), CompatHelper(), Coveralls(), - Develop(), Documenter(), DroneCI(), GitHubActions(), GitLabCI(), TravisCI(), RegisterAction(), + Develop(), Documenter(), DroneCI(), GitHubActions(), GitLabCI(), PackageCompilerLib(), + TravisCI(), RegisterAction(), ]) end @@ -126,6 +127,7 @@ end GitHubActions(; x86=true, linux=false, coverage=false), GitLabCI(; coverage=false, extra_versions=[v"0.6"]), License(; name="ISC"), + PackageCompilerLib(; lib_name="foosball"), ProjectFile(; version=v"1"), Readme(; inline_badges=true, badge_off=[Codecov]), RegisterAction(; prompt="gimme version"),