Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@

# docs/tasks/0060: braces around every single-statement body (clang-format InsertBraces)
33b30998156056c4de59dcb0ce598390602ab253

# docs/tasks/0076: namespace jinja2 wrapped in inline namespace v2 (scripts/abi_namespace.py)
ae4649c98baaf1999e99096f7d39ef7a4dca2b8d
5 changes: 5 additions & 0 deletions .github/workflows/format-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ jobs:
- name: Check formatting of src/ and include/
run: git ls-files 'src/**.h' 'src/**.cpp' 'include/**.h' | xargs clang-format-18 --dry-run --Werror

# Every namespace jinja2 in src/ and include/ opens inline namespace v2, the ABI tag
# (docs/tasks/0076); the nm test checks the symbols, this also covers header-only code
- name: Check the inline ABI namespace
run: python3 scripts/abi_namespace.py --check

# test/ still has hand-aligned tables, so only lines touched by the pull request
# are checked there. src/ and include/ are left to the whole-file step above: given
# a vendored file, git clang-format reports it as emptied (.clang-format-ignore).
Expand Down
52 changes: 49 additions & 3 deletions .github/workflows/linux-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ jobs:
# Two rows also install the library and build a consumer against the installed package
# at another standard (`package`, docs/tasks/0068): a static and a shared install, each
# read through find_package and pkg-config. It is a check after the tests, not an axis.
# One shared row per standard builds RelWithDebInfo (`abi`) so that abidiff can compare
# the library's exported ABI with the previous one (docs/tasks/0076): the headers differ
# by standard, so the ABI is checked once per standard. For the pairwise design those rows
# count as Release (optimised); the remaining Release rows still meet every standard,
# compiler, linkage, binding and noderef value on their own.
linux-build:
name: C++${{ matrix.std }} ${{ matrix.compiler }} ${{ matrix.config }} shared=${{ matrix.shared }} json=${{ matrix.bindings }}${{ matrix.noderef == 'OFF' && ' noderef=OFF' || '' }}
runs-on: ubuntu-24.04
Expand All @@ -56,14 +61,14 @@ jobs:
include:
- { std: 17, compiler: gcc-12, config: Release, shared: 'OFF', bindings: boost, noderef: 'OFF' }
- { std: 17, compiler: gcc-14, config: Debug, shared: 'OFF', bindings: rapid }
- { std: 17, compiler: clang-18, config: Release, shared: 'ON', bindings: nlohmann }
- { std: 17, compiler: clang-18, config: RelWithDebInfo, shared: 'ON', bindings: nlohmann, abi: true }
- { std: 17, compiler: clang-20, config: Release, shared: 'OFF', bindings: boost, package: 23 }
- { std: 20, compiler: gcc-12, config: Release, shared: 'ON', bindings: rapid }
- { std: 20, compiler: gcc-12, config: RelWithDebInfo, shared: 'ON', bindings: rapid, abi: true }
- { std: 20, compiler: gcc-14, config: Release, shared: 'ON', bindings: nlohmann }
- { std: 20, compiler: clang-18, config: Debug, shared: 'OFF', bindings: boost }
- { std: 20, compiler: clang-20, config: Debug, shared: 'OFF', bindings: nlohmann, noderef: 'OFF' }
- { std: 23, compiler: gcc-12, config: Debug, shared: 'OFF', bindings: nlohmann }
- { std: 23, compiler: gcc-14, config: Release, shared: 'ON', bindings: boost, noderef: 'OFF', package: 17 }
- { std: 23, compiler: gcc-14, config: RelWithDebInfo, shared: 'ON', bindings: boost, noderef: 'OFF', package: 17, abi: true }
- { std: 23, compiler: clang-18, config: Release, shared: 'ON', bindings: rapid, noderef: 'OFF' }
- { std: 23, compiler: clang-20, config: Debug, shared: 'ON', bindings: rapid }
- { std: 26, compiler: gcc-14, config: Release, shared: 'OFF', bindings: boost }
Expand Down Expand Up @@ -99,6 +104,47 @@ jobs:
- name: Test
run: ctest --test-dir .build --output-on-failure

# abidiff against the ABI of the previous master build (report only) or, once the RC
# commits abi/libjinja2cpp.so.2.cxx<std>.abi, against that baseline: an incompatible
# change then fails unless SOVERSION is bumped and the baseline regenerated (0076)
- name: ABI dump
if: matrix.abi
run: |
sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends abigail-tools
mkdir -p .abi
abidw --headers-dir include --drop-private-types --no-show-locs \
--suppressions abi/suppressions.ini \
--out-file .abi/libjinja2cpp.so.2.cxx${{ matrix.std }}.abi .build/libjinja2cpp.so

- name: Previous ABI dump
if: matrix.abi && github.event_name == 'pull_request'
uses: actions/cache/restore@v4
with:
path: .abi-base
key: abi-cxx${{ matrix.std }}-${{ github.event.pull_request.base.sha }}
restore-keys: abi-cxx${{ matrix.std }}-

- name: ABI report
if: matrix.abi
run: scripts/abi_report.sh ${{ matrix.std }}

- name: Save ABI dump
if: matrix.abi && github.event_name == 'push'
run: mkdir -p .abi-base && cp .abi/*.abi .abi-base/

- if: matrix.abi && github.event_name == 'push'
uses: actions/cache/save@v4
with:
path: .abi-base
key: abi-cxx${{ matrix.std }}-${{ github.sha }}

# also when the report fails: its message points at this dump
- if: ${{ !cancelled() && matrix.abi }}
uses: actions/upload-artifact@v4
with:
name: abi-cxx${{ matrix.std }}
path: .abi/

- name: Installed package
if: matrix.package
run: |
Expand Down
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,12 @@ if (JINJA2CPP_BUILD_TESTS)
add_test(NAME jinja2cpp_tests COMMAND jinja2cpp_tests --gtest_filter=-Parity*)
# Differential corpus against Python Jinja2, see test/parity/README.md
add_test(NAME jinja2cpp_parity COMMAND jinja2cpp_tests --gtest_filter=Parity*)
# Every jinja2 symbol the library defines is in the inline ABI namespace v2 (docs/tasks/0076)
if (CMAKE_NM AND NOT MSVC)
add_test(NAME jinja2cpp_abi_namespace
COMMAND ${CMAKE_COMMAND} -DNM=${CMAKE_NM} -DLIBRARY=$<TARGET_FILE:jinja2cpp>
-P ${CMAKE_CURRENT_SOURCE_DIR}/test/abi_symbols.cmake)
endif ()

if ("${JINJA2CPP_WITH_JSON_BINDINGS}" STREQUAL "boost")
set(_bindings_define "-DJINJA2CPP_WITH_JSON_BINDINGS_BOOST")
Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ The install carries a CMake package and a pkg-config file. With CMake, point
target:

```cmake
find_package(jinja2cpp 1 REQUIRED)
find_package(jinja2cpp 2 REQUIRED)
target_link_libraries(my_app PRIVATE jinja2cpp::jinja2cpp)
```

Expand All @@ -201,6 +201,32 @@ it in the default internal dependencies mode). The un-namespaced `jinja2cpp` tar
`lib/cmake/jinja2cpp` (was `lib/jinja2cpp`), the static library in `lib` (was
`lib/static`) and `jinja2cpp.pc` in `lib/pkgconfig` (was `share/pkgconfig`). Without CMake, use `pkg-config --cflags --libs jinja2cpp`
(`--static` for a static build).

### Compatibility and versioning

- **Names.** The 1.x names that 2.0 could keep still compile, marked `[[deprecated]]`;
they are removed in 3.0.
- **ABI across majors.** Everything is declared in `jinja2::v2`, an inline namespace: you
keep writing `jinja2::Value`, but the major version is part of every mangled name, so a
program compiled against 1.x headers fails to link with a 2.x library (static or shared)
instead of misbehaving at run time. Never spell `v2` yourself. To declare Jinja2C++ types
without their definitions, include `<jinja2cpp/fwd.h>`: a hand-written
`namespace jinja2 { class Value; }` declares a different, undefined class and makes later
uses ambiguous. `<jinja2cpp/jinja2cpp.h>` includes the whole API except the JSON bindings.
If your code says `using namespace jinja2;` and has its own namespace-scope `v2`, qualify
one of them. Specialise the extension points inside `namespace jinja2 { ... }` or with their
qualified name (`template<> struct jinja2::Reflector<MyType>`). A 1.x-style
`detail::Reflector` specialisation must use the qualified form
(`template<> struct jinja2::detail::Reflector<MyType>`): MSVC does not look through the
inline namespace when `namespace detail` is reopened inside `namespace jinja2`, and declares
a second `jinja2::detail` instead.
- **ABI within 2.x.** The shared library's SONAME carries the ABI version
(`libjinja2cpp.so.2`, `libjinja2cpp.2.dylib`). It is bumped by any release that changes the
layout of a type you allocate (a `Settings` field, a `Value` alternative) or the signature
of an exported function, so a shared consumer is never loaded against an incompatible
library. A static consumer has no such guard and rebuilds after a minor upgrade; patch
releases keep the ABI.

### Usage with conan.io dependency manager
Jinja2C++ can be used as conan.io package. In this case, you should do the following steps:

Expand Down
22 changes: 22 additions & 0 deletions abi/suppressions.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# libabigail suppressions for the abidiff job (.github/workflows/linux-build.yml, docs/tasks/0076).
# Only vendored internals that no public signature exposes are suppressed. jinja2::v2::detail
# is not: inline functions in the public headers call exported detail functions, so those
# are ABI too.

# jbcoe/value_types, vendored as jinja2::v2::detail::xyz; header-only templates, their
# layout reaches users only through Value, which is reported on its own
[suppress_function]
name_regexp = ^jinja2::v2::detail::xyz::

[suppress_variable]
name_regexp = ^jinja2::v2::detail::xyz::

# robin_hood.h and lexertk.h are private to src/ and hidden by -fvisibility=hidden
[suppress_type]
name_regexp = ^(robin_hood|lexertk)::

[suppress_function]
name_regexp = ^(robin_hood|lexertk)::

[suppress_variable]
name_regexp = ^(robin_hood|lexertk)::
2 changes: 1 addition & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ source and committed), `system` (found on the machine), `ci` (GitHub Actions).
| Boost | 1.92.0 | BSL-1.0 | mandatory | link | fetched | the nine libraries below | Yes, piece by piece (docs/tasks/0230) |
| robin-hood-hashing | 3.11.5, modified | MIT | mandatory | internal | vendored | flat hash map for values and scopes, faster than `std::unordered_map` here | Keep the idea; upstream is archived (docs/tasks/0231) |
| lexertk | unrecorded, modified | MIT | mandatory | internal | vendored | expression tokenizer | Keep; effectively forked code |
| value_types | 57f2e7a, modified | MIT | mandatory | public-api | vendored | `polymorphic`/`indirect` in `jinja2::detail::xyz`, behind `jinja2::types` (Value, GenericList) | Keep; `std::` at C++26 |
| value_types | 57f2e7a, modified | MIT | mandatory | public-api | vendored | `polymorphic`/`indirect` in `jinja2::v2::detail::xyz`, behind `jinja2::types` (Value, GenericList) | Keep; `std::` at C++26 |
| Unicode tables | UCD 15.0.0 via Python 3.12 | Unicode-3.0 | mandatory | internal | generated | Python-identical `isprintable`, case mapping, digits without ICU | Data only; acquisition decision open in 0187/0186 |
| HTML entities | Python 3.12 `html.entities` | PSF-2.0 | mandatory | internal | generated | MarkupSafe-compatible unescape | Data only; keep |
| Boost.JSON | 1.92.0 | BSL-1.0 | alternative (default) | public-optional | fetched | parses `{% meta %}` JSON for `GetMetadata` | Yes: the heaviest backend for a niche feature |
Expand Down
20 changes: 18 additions & 2 deletions docs/tasks/0076-abi-namespace-and-headers.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
status: in-progress
status: done
priority: high
area: release
depends: [0072, 0073, 0074, 0075, 0043]
touches: [include/, src/, CMakeLists.txt, cmake/public/, test/package/, test/fwd_test.cpp, test/value_ptr_test.cpp, README.md, sbom/, scripts/abi_namespace.py, .git-blame-ignore-revs, .github/workflows/format-check.yml]
touches: [include/, src/, CMakeLists.txt, cmake/public/, test/package/, test/fwd_test.cpp, test/value_ptr_test.cpp, test/abi_symbols.cmake, test/template_api_test.cpp, README.md, sbom/, abi/, scripts/abi_namespace.py, scripts/abi_report.sh, .git-blame-ignore-revs, .github/workflows/format-check.yml, .github/workflows/linux-build.yml]
---
# 2.0: inline ABI namespace, header layout, version

Expand Down Expand Up @@ -36,3 +36,19 @@ future 2.x build is binary-compatible. Settle the API-shaping decisions listed i
script-generated `inline namespace v2` commit, the nm/link-probe/SONAME gates, the abidiff
job and the README policy. Follow-ups: 0259 (robin_hood/lexertk namespaces), 0260
(`<iosfwd>`), 0261 (Windows package link check).

**Done.** PR-1 #580. PR-2: `scripts/abi_namespace.py` wrote `inline namespace v2` into
every `namespace jinja2` of include/ and src/ (one commit, in .git-blame-ignore-revs) and
checks it in the format-check job; CTest `jinja2cpp_abi_namespace` (test/abi_symbols.cmake)
fails on a jinja2 symbol outside `v2` or a global `xyz`; test/package links
`abi_probe_v2` and expects `abi_probe_v1` (1.x mangling) not to link, in the static and
shared package rows; check_package.cmake checks the SONAME; the version asserts require
major 2; README "Compatibility and versioning". The abidiff job runs on one shared
RelWithDebInfo row per standard and reports against the last master build; committing the
baselines at the RC is 0285. Deliberate divergence from the plan: `jinja2::v2::detail` is
not suppressed in abidiff (exported detail functions are called from inline headers), and
the C++20 abi row is gcc-12, not gcc-14, so that the Release rows keep every pair; the C++17
abi row stays clang-18 (the plan preferred GCC, but the only C++17 shared row is clang-18).
The gate counts any change left after `--no-added-syms` as a break, since abidiff reports
a changed layout (a new `Settings` field) with bit 4, not bit 8. The four Unicode/HTML
generators in scripts/ emit the `v2` form, so their `--check` stays green.
31 changes: 31 additions & 0 deletions docs/tasks/0285-abi-baselines-at-rc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
status: open
priority: high
area: release
depends: [0076, 0221]
touches: [abi/, abi/suppressions.ini]
---
# Commit the ABI baselines at the 2.0 RC and turn the abidiff gate on

**Problem.** Since 0076 the three `abi` rows of linux-build.yml (one shared RelWithDebInfo
row per standard) dump the library's ABI with abidw and compare it with the last master
build. Until baselines are committed the job only reports: the public API is still
changing in the API wave, and failing on every change would block it. The SOVERSION
policy (README, "Compatibility and versioning") is enforced only once
`abi/libjinja2cpp.so.2.cxx{17,20,23}.abi` exist; then `scripts/abi_report.sh` fails an
incompatible change unless SOVERSION was bumped.

**Proposal.** On the RC commit (0221 G1 freeze), download the `abi-cxx17`, `abi-cxx20` and
`abi-cxx23` artifacts of that commit's linux-build run and commit them as `abi/`. Before
that, read one report and review `abi/suppressions.ini`: it suppresses only the vendored
value_types and robin_hood/lexertk names; `jinja2::v2::detail` stays checked because
inline header code calls exported detail functions. Add suppressions only for noise that
is shown not to reach users. Two things to settle first: a RelWithDebInfo dump is about 31 MB
(1.2 MB gzipped), so commit the baselines compressed or trimmed and teach
`scripts/abi_report.sh` to read them; and the dump includes weak std and Boost symbols the
library exports (`boost::system` among them), so a Boost pin bump may read as a break
unless those are suppressed or hidden. `scripts/**` is in linux-build's `paths-ignore`, so
a PR that touches only `abi_report.sh` runs no abi row: check such a change by hand.

**Done when.** The baselines are on master, a deliberate test change to an exported
layout makes the abi rows red, and docs/release-2.0.md lists the step in its RC procedure.
3 changes: 3 additions & 0 deletions include/jinja2cpp/big_int.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

namespace jinja2
{
inline namespace v2
{
namespace detail
{
//! Integral types a \ref BigInt is made from: all except `bool` and the character types, as for \ref Value
Expand Down Expand Up @@ -204,6 +206,7 @@ class BigInt

std::string m_decimal = "0";
};
} // namespace v2
} // namespace jinja2

#endif // JINJA2CPP_BIG_INT_H
3 changes: 3 additions & 0 deletions include/jinja2cpp/binding/boost_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

namespace jinja2
{
inline namespace v2
{
namespace detail
{

Expand Down Expand Up @@ -247,6 +249,7 @@ struct Reflector<boost::json::value>
}
};

} // namespace v2
} // namespace jinja2

#endif // JINJA2CPP_BINDING_BOOST_JSON_H
3 changes: 3 additions & 0 deletions include/jinja2cpp/binding/nlohmann_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

namespace jinja2
{
inline namespace v2
{
namespace detail
{

Expand Down Expand Up @@ -237,6 +239,7 @@ struct Reflector<nlohmann::ordered_json> : detail::NLohmannJsonReflector<nlohman
};
#endif

} // namespace v2
} // namespace jinja2

#endif // JINJA2CPP_BINDING_NLOHMANN_JSON_H
3 changes: 3 additions & 0 deletions include/jinja2cpp/binding/rapid_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace jinja2
{
inline namespace v2
{
namespace detail
{

Expand Down Expand Up @@ -227,6 +229,7 @@ struct Reflector<rapidjson::GenericDocument<Enc>>
return GenericMap([accessor = detail::RapidJsonObjectAccessor<rapidjson::GenericDocument<Enc>>(val)]() { return &accessor; });
}
};
} // namespace v2
} // namespace jinja2

#endif // JINJA2CPP_BINDING_RAPID_JSON_H
3 changes: 3 additions & 0 deletions include/jinja2cpp/error_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN

namespace jinja2
{
inline namespace v2
{
/*!
* \brief Type of the error
*
Expand Down Expand Up @@ -197,6 +199,7 @@ using ErrorInfoTpl JINJA2CPP_DEPRECATED("jinja2cpp-2: BasicErrorInfo") = BasicEr

JINJA2CPP_EXPORT std::ostream& operator<<(std::ostream& os, const ErrorInfo& res);
JINJA2CPP_EXPORT std::wostream& operator<<(std::wostream& os, const ErrorInfoW& res);
} // namespace v2
} // namespace jinja2

JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END
Expand Down
3 changes: 3 additions & 0 deletions include/jinja2cpp/filesystem_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN

namespace jinja2
{
inline namespace v2
{

template<typename CharT>
using FileStreamPtr = std::unique_ptr<std::basic_istream<CharT>, void (*)(std::basic_istream<CharT>*)>;
Expand Down Expand Up @@ -207,6 +209,7 @@ class JINJA2CPP_EXPORT RealFileSystem : public IFilesystemHandler
private:
std::string m_rootFolder;
};
} // namespace v2
} // namespace jinja2

JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END
Expand Down
3 changes: 3 additions & 0 deletions include/jinja2cpp/fwd.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

namespace jinja2
{
inline namespace v2
{
class Value;
class GenericList;
class GenericMap;
Expand Down Expand Up @@ -59,6 +61,7 @@ using ErrorInfo = BasicErrorInfo<char>;
using ErrorInfoW = BasicErrorInfo<wchar_t>;
using ValuesList = std::vector<Value>;
using FilesystemHandlerPtr = std::shared_ptr<IFilesystemHandler>;
} // namespace v2
} // namespace jinja2

#endif // JINJA2CPP_FWD_H
3 changes: 3 additions & 0 deletions include/jinja2cpp/generic_list.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN

namespace jinja2
{
inline namespace v2
{
/*!
* \brief Interface for accessing items in list by the indexes
*
Expand Down Expand Up @@ -287,6 +289,7 @@ inline ListEnumeratorPtr IListItemAccessor::MakeEnumerator(Args&&... args)
{
return ListEnumeratorPtr{ types::in_place_type_t<T>{}, std::forward<Args&&>(args)... };
}
} // namespace v2
} // namespace jinja2

JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END
Expand Down
Loading
Loading