Skip to content

Commit

Permalink
Implement a very simple template engine
Browse files Browse the repository at this point in the history
This template engine renders files in a single pass and allows for
nested control flow in the templates. It's similar to Jinja2, except
extremely barebones and there is no pretty error reporting for mistakes
in the templates. The only error that is reported early is when blocks
weren't terminated properly. This check is done using a depth counter,
so don't expect too much.
  • Loading branch information
friendlyanon committed Feb 20, 2022
1 parent 81864b4 commit a4a86d8
Show file tree
Hide file tree
Showing 46 changed files with 352 additions and 293 deletions.
42 changes: 17 additions & 25 deletions cmake-init/cmake_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import sys
import zipfile

from template import compile_template

__version__ = "0.25.0"

is_windows = os.name == "nt"
Expand Down Expand Up @@ -162,7 +164,6 @@ def ask(*args, **kwargs):
"os": "win64" if is_windows else "unix",
"c": cli_args.c,
"cpp": not cli_args.c,
"suppress": False,
"c_header": False,
"include_source": False,
"has_source": True,
Expand All @@ -179,13 +180,14 @@ def ask(*args, **kwargs):
)
d[key] = value
d["examples"] = value
if d["type_id"] == "s" and d["cpp"]:
d["suppress"] = True
if d["type_id"] == "e":
d["include_source"] = True
if d["type_id"] == "h":
d["has_source"] = False
d["c_header"] = d["c"] and d["type_id"] == "h"
d["exe"] = d["type_id"] == "e"
d["lib"] = d["type_id"] == "s"
d["header"] = d["type_id"] == "h"
return d


Expand All @@ -194,28 +196,16 @@ def mkdir(path):


def write_file(path, d, overwrite, zip_path):
if not overwrite and os.path.exists(path):
return

def replacer(match):
query = match.group(1)
expected = True
if query.endswith("_not"):
query = query[:-4]
expected = False
if query == "type":
mapping = {"exe": "e", "header": "h", "shared": "s"}
actual = mapping[match.group(2)] == d["type_id"]
if actual == expected:
return match.group(3)
elif query == "if" and d[match.group(2)] == expected:
return match.group(3)
return ""

regex = re.compile("{((?:type|if)(?:_not)?) ([^}]+)}(.+?){end}", re.DOTALL)
contents = regex.sub(replacer, zip_path.read_text(encoding="UTF-8"))
with open(path, "w", encoding="UTF-8", newline="\n") as f:
f.write(contents % d)
if overwrite or not os.path.exists(path):
renderer = compile_template(zip_path.read_text(encoding="UTF-8"), d)
# noinspection PyBroadException
try:
contents = renderer()
except Exception:
print(f"Error while rendering {path}", file=sys.stderr)
raise
with open(path, "w", encoding="UTF-8", newline="\n") as f:
f.write(contents)


def should_write_examples(d, at):
Expand Down Expand Up @@ -354,6 +344,8 @@ def vcpkg(d, zip):
print(f"""'{d["name"]}' already exists""", file=sys.stderr)
exit(1)
mkdir(path)
d["lib"] = d["type_id"] == "s"
d["header"] = d["type_id"] == "h"
write_dir(path, d, False, zipfile.Path(zip, "templates/vcpkg/"))
vcpkg_root = r"%VCPKG_ROOT:\=/%" if is_windows else "$VCPKG_ROOT"
pwd = r"%cd:\=/%" if is_windows else "$PWD"
Expand Down
67 changes: 67 additions & 0 deletions cmake-init/template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# cmake-init - The missing CMake project initializer
# Copyright (C) 2021 friendlyanon
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

# Website: https://github.com/friendlyanon/cmake-init

import re

__all__ = ["compile_template"]

block_regex = re.compile(
r"(.*?)({% .+? %}|{= .+? =})|(.+?)\Z",
re.MULTILINE | re.DOTALL
)


def compile_template(template_source, globals):
depth = 0
python_source = ["def f():\n _result = []"]

def add_line(line):
python_source.append(" " * (depth + 1) + line)

def add_repr(o):
add_line("_result.append(str(" + repr(o) + "))")

for match in block_regex.finditer(template_source):
before, block, tail = match.groups()
if not block:
add_repr(tail)
continue
if block == "end":
depth -= 1
continue
if before:
add_repr(before)
inner = block[3:-3]
if block[1:2] == "=":
add_repr(globals[inner])
continue
if inner == "end":
depth -= 1
continue
if inner == "else" or inner.startswith("elif "):
depth -= 1
add_line(inner + ":")
depth += 1

if depth != 0:
raise SyntaxError("Block not properly terminated")

add_line("return \"\".join(_result)")
locals = {}
exec("\n".join(python_source), globals, locals)
return locals["f"]
8 changes: 4 additions & 4 deletions cmake-init/templates/c/common/example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
cmake_minimum_required(VERSION 3.14)

project(%(name)sExamples C)
project({= name =}Examples C)

include(../cmake/project-is-top-level.cmake)
include(../cmake/folders.cmake)

if(PROJECT_IS_TOP_LEVEL)
find_package(%(name)s REQUIRED)
find_package({= name =} REQUIRED)
endif()

add_custom_target(run-examples)

function(add_example NAME)
add_executable("${NAME}" "${NAME}.c")
target_link_libraries("${NAME}" PRIVATE %(name)s::%(name)s)
target_compile_features("${NAME}" PRIVATE c_std_%(std)s)
target_link_libraries("${NAME}" PRIVATE {= name =}::{= name =})
target_compile_features("${NAME}" PRIVATE c_std_{= std =})
add_custom_target("run_${NAME}" COMMAND "${NAME}" VERBATIM)
add_dependencies("run_${NAME}" "${NAME}")
add_dependencies(run-examples "run_${NAME}")
Expand Down
30 changes: 15 additions & 15 deletions cmake-init/templates/c/executable/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ cmake_minimum_required(VERSION 3.14)
include(cmake/prelude.cmake)

project(
%(name)s
VERSION %(version)s
DESCRIPTION "%(description)s"
HOMEPAGE_URL "%(homepage)s"
{= name =}
VERSION {= version =}
DESCRIPTION "{= description =}"
HOMEPAGE_URL "{= homepage =}"
LANGUAGES C
)

Expand All @@ -16,32 +16,32 @@ include(cmake/variables.cmake)
# ---- Declare library ----

add_library(
%(name)s_lib OBJECT
{= name =}_lib OBJECT
source/lib.c
)

target_include_directories(
%(name)s_lib ${warning_guard}
{= name =}_lib ${warning_guard}
PUBLIC
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/source>"
)

target_compile_features(%(name)s_lib PUBLIC c_std_%(std)s)
target_compile_features({= name =}_lib PUBLIC c_std_{= std =})

# ---- Declare executable ----

add_executable(%(name)s_exe source/main.c)
add_executable(%(name)s::exe ALIAS %(name)s_exe)
add_executable({= name =}_exe source/main.c)
add_executable({= name =}::exe ALIAS {= name =}_exe)

set_target_properties(
%(name)s_exe PROPERTIES
OUTPUT_NAME %(name)s
{= name =}_exe PROPERTIES
OUTPUT_NAME {= name =}
EXPORT_NAME exe
)

target_compile_features(%(name)s_exe PRIVATE c_std_%(std)s)
target_compile_features({= name =}_exe PRIVATE c_std_{= std =})

target_link_libraries(%(name)s_exe PRIVATE %(name)s_lib)
target_link_libraries({= name =}_exe PRIVATE {= name =}_lib)

# ---- Install rules ----

Expand All @@ -51,12 +51,12 @@ endif()

# ---- Developer mode ----

if(NOT %(name)s_DEVELOPER_MODE)
if(NOT {= name =}_DEVELOPER_MODE)
return()
elseif(NOT PROJECT_IS_TOP_LEVEL)
message(
AUTHOR_WARNING
"Developer mode is intended for developers of %(name)s"
"Developer mode is intended for developers of {= name =}"
)
endif()

Expand Down
2 changes: 1 addition & 1 deletion cmake-init/templates/c/executable/source/lib.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
library create_library()
{
library lib;
lib.name = "%(name)s";
lib.name = "{= name =}";
return lib;
}
10 changes: 5 additions & 5 deletions cmake-init/templates/c/executable/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
# depends on being added from it, i.e. the testing is done only from the build
# tree and is not feasible from an install location

project(%(name)sTests LANGUAGES C)
project({= name =}Tests LANGUAGES C)

add_executable(%(name)s_test source/%(name)s_test.c)
target_link_libraries(%(name)s_test PRIVATE %(name)s_lib)
target_compile_features(%(name)s_test PRIVATE c_std_%(std)s)
add_executable({= name =}_test source/{= name =}_test.c)
target_link_libraries({= name =}_test PRIVATE {= name =}_lib)
target_compile_features({= name =}_test PRIVATE c_std_{= std =})

add_test(NAME %(name)s_test COMMAND %(name)s_test)
add_test(NAME {= name =}_test COMMAND {= name =}_test)

add_folders(Test)
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ int main(int argc, const char* argv[])

library lib = create_library();

return strcmp(lib.name, "%(name)s") == 0 ? 0 : 1;
return strcmp(lib.name, "{= name =}") == 0 ? 0 : 1;
}
30 changes: 15 additions & 15 deletions cmake-init/templates/c/header/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ cmake_minimum_required(VERSION 3.14)
include(cmake/prelude.cmake)

project(
%(name)s
VERSION %(version)s
DESCRIPTION "%(description)s"
HOMEPAGE_URL "%(homepage)s"
{= name =}
VERSION {= version =}
DESCRIPTION "{= description =}"
HOMEPAGE_URL "{= homepage =}"
LANGUAGES NONE
)

Expand All @@ -15,45 +15,45 @@ include(cmake/variables.cmake)

# ---- Declare library ----

add_library(%(name)s_%(name)s INTERFACE)
add_library(%(name)s::%(name)s ALIAS %(name)s_%(name)s)
add_library({= name =}_{= name =} INTERFACE)
add_library({= name =}::{= name =} ALIAS {= name =}_{= name =})

set_property(
TARGET %(name)s_%(name)s PROPERTY
EXPORT_NAME %(name)s
TARGET {= name =}_{= name =} PROPERTY
EXPORT_NAME {= name =}
)

target_include_directories(
%(name)s_%(name)s ${warning_guard}
{= name =}_{= name =} ${warning_guard}
INTERFACE
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
)

target_compile_features(%(name)s_%(name)s INTERFACE c_std_%(std)s)
target_compile_features({= name =}_{= name =} INTERFACE c_std_{= std =})

# ---- Install rules ----

if(NOT CMAKE_SKIP_INSTALL_RULES)
include(cmake/install-rules.cmake)
endif(){if c_examples}
endif(){% if c_examples %}

# ---- Examples ----

if(PROJECT_IS_TOP_LEVEL)
option(BUILD_EXAMPLES "Build examples tree." "${%(name)s_DEVELOPER_MODE}")
option(BUILD_EXAMPLES "Build examples tree." "${{= name =}_DEVELOPER_MODE}")
if(BUILD_EXAMPLES)
add_subdirectory(example)
endif()
endif(){end}
endif(){% end %}

# ---- Developer mode ----

if(NOT %(name)s_DEVELOPER_MODE)
if(NOT {= name =}_DEVELOPER_MODE)
return()
elseif(NOT PROJECT_IS_TOP_LEVEL)
message(
AUTHOR_WARNING
"Developer mode is intended for developers of %(name)s"
"Developer mode is intended for developers of {= name =}"
)
endif()

Expand Down
4 changes: 2 additions & 2 deletions cmake-init/templates/c/header/include/__name__/__name__.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ extern "C" {
*/
const char* header_only_name(void);

#ifdef %(uc_name)s_IMPLEMENTATION
#ifdef {= uc_name =}_IMPLEMENTATION
const char* header_only_name()
{
return "%(name)s";
return "{= name =}";
}
#endif

Expand Down
4 changes: 2 additions & 2 deletions cmake-init/templates/c/header/test/source/__name___test.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "%(name)s/%(name)s.h"
#include "{= name =}/{= name =}.h"

#include <string.h>

Expand All @@ -7,5 +7,5 @@ int main(int argc, const char* argv[])
(void)argc;
(void)argv;

return strcmp("%(name)s", header_only_name()) == 0 ? 0 : 1;
return strcmp("{= name =}", header_only_name()) == 0 ? 0 : 1;
}
Loading

0 comments on commit a4a86d8

Please sign in to comment.