From c4a0aaf5cedbf06913d6c68229f0e490f8ed7de2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 00:44:17 +0000 Subject: [PATCH 1/6] Add scripts/abi_namespace.py: wrap namespace jinja2 in inline namespace v2 (0076) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Ua8ABM1TRsAvso2sWGHFSw --- scripts/abi_namespace.py | 214 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100755 scripts/abi_namespace.py diff --git a/scripts/abi_namespace.py b/scripts/abi_namespace.py new file mode 100755 index 000000000..9b6e3652b --- /dev/null +++ b/scripts/abi_namespace.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Keep every `namespace jinja2` in include/ and src/ inside the inline ABI namespace (docs/tasks/0076). + + scripts/abi_namespace.py # rewrite the files in place + scripts/abi_namespace.py --check # list what would change, exit 1 if anything would + +Public and internal code live in `jinja2::v2`, an inline namespace: users still write +`jinja2::Value`, but the mangled names carry the major version, so a program built against +1.x headers fails to link with a 2.x library instead of misbehaving. The tag is spelled out +in every file rather than hidden behind a macro (house clang-format and clang-tidy handle the +literal form); this script writes it so that nobody has to: + + namespace jinja2 namespace jinja2 + { { + ... -> inline namespace v2 + } { + ... + } // namespace v2 + } + +A nested `namespace jinja2::detail` becomes `namespace jinja2 { inline namespace v2 { +namespace detail` (C++17 cannot nest an inline namespace in that form). An opening already +followed by `inline namespace vN` with another N gets the new tag, so the 3.0 retag is one +run with TAG changed. The vendored polymorphic_value headers are rewritten in their own +upstream brace style; robin_hood.h and lexertk.h do not use namespace jinja2. +""" + +import argparse +import os +import re +import subprocess +import sys + +TAG = "v2" + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ROOTS = ("include", "src") +OPENING = re.compile(r"^namespace jinja2(?P(?:::\w+)*)(?P\s*\{)?\s*$") +INLINE = re.compile(r"^inline namespace (?Pv\d+)(?P\s*\{)?\s*$") +CLOSE_COMMENT = re.compile(r"^\}(?P\s*)//\s*namespace\s+(?P[\w:]+)\s*$") + + +def code_mask(text): + """Copy of text with comments, string and character literals blanked, newlines kept.""" + out = list(text) + i, n = 0, len(text) + + def blank(a, b): + for k in range(a, b): + if out[k] != "\n": + out[k] = " " + + while i < n: + c = text[i] + if text.startswith("//", i): + j = text.find("\n", i) + j = n if j < 0 else j + blank(i, j) + i = j + elif text.startswith("/*", i): + j = text.find("*/", i + 2) + j = n if j < 0 else j + 2 + blank(i, j) + i = j + elif c == '"' or c == "'": + # R"delim( ... )delim", with any encoding prefix + prefix = re.search(r"(?:u8|[uUL])?R$", text[max(0, i - 3) : i]) + before = i - len(prefix.group(0)) - 1 if prefix else -1 + if c == '"' and prefix and (before < 0 or not (text[before].isalnum() or text[before] == "_")): + m = re.match(r'"([^(\s]*)\(', text[i:]) + if m: + end = text.find(")" + m.group(1) + '"', i + len(m.group(0))) + end = n if end < 0 else end + len(m.group(1)) + 2 + blank(i + 1, end - 1) + i = end + continue + if c == "'" and i > 0 and text[i - 1].isalnum() and i + 1 < n and text[i + 1].isalnum(): + i += 1 # digit separator: 1'000 + continue + j = i + 1 + while j < n and text[j] != c and text[j] != "\n": + j += 2 if text[j] == "\\" else 1 + blank(i + 1, min(j, n)) + i = j + 1 + else: + i += 1 + return "".join(out) + + +def matching_close(mask_lines, line, col): + """Line index of the `}` closing the `{` at (line, col) of the masked lines.""" + depth = 0 + for li in range(line, len(mask_lines)): + start = col if li == line else 0 + for ch in mask_lines[li][start:]: + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + return li + raise ValueError("unbalanced braces") + + +def brace_after(mask_lines, line): + """(line, col) of the first `{` at or after the end of `line` (the opening's brace).""" + for li in range(line, len(mask_lines)): + col = mask_lines[li].find("{") + if col >= 0: + return li, col + if li > line and mask_lines[li].strip(): + break + raise ValueError("namespace opening without a brace") + + +def next_code_line(mask_lines, line): + for li in range(line + 1, len(mask_lines)): + if mask_lines[li].strip(): + return li + return None + + +def rewrite(text): + """Return text with every namespace jinja2 opening wrapped in `inline namespace TAG`.""" + lines = text.split("\n") + # Each pass fixes the first opening that needs it; the next pass re-reads the result. + for _ in range(10000): + mask_lines = code_mask("\n".join(lines)).split("\n") + edit = None + for li, raw in enumerate(lines): + m = OPENING.match(mask_lines[li].rstrip()) + if not m: + continue + same_line = bool(m.group("brace")) + open_line, open_col = brace_after(mask_lines, li) + close_line = matching_close(mask_lines, open_line, open_col) + close_text = lines[close_line] + cm = CLOSE_COMMENT.match(close_text.strip()) + space = cm.group("space") if cm else " " + if m.group("nested"): + inner = m.group("nested")[2:] + head = ["namespace jinja2", "{", f"inline namespace {TAG}", "{", f"namespace {inner}"] + if same_line: + head = ["namespace jinja2 {", f"inline namespace {TAG} {{", f"namespace {inner} {{"] + closing = "}" + space + f"// namespace {inner}" if cm else close_text + edit = (li, li + 1, head, close_line, [closing, "}" + space + f"// namespace {TAG}", "}" + space + "// namespace jinja2"]) + break + nxt = next_code_line(mask_lines, open_line if not same_line else li) + im = INLINE.match(mask_lines[nxt].strip()) if nxt is not None else None + if im: + if im.group("tag") == TAG: + continue + inline_brace_line, inline_col = brace_after(mask_lines, nxt) + inline_close = matching_close(mask_lines, inline_brace_line, inline_col) + lines[nxt] = lines[nxt].replace(im.group("tag"), TAG, 1) + icm = CLOSE_COMMENT.match(lines[inline_close].strip()) + if icm: + lines[inline_close] = "}" + icm.group("space") + f"// namespace {TAG}" + edit = () + break + insert = [f"inline namespace {TAG} {{"] if same_line else [f"inline namespace {TAG}", "{"] + edit = (open_line + 1, open_line + 1, insert, close_line, ["}" + space + f"// namespace {TAG}", close_text]) + break + if edit is None: + return "\n".join(lines) + if edit == (): + continue + a, b, new_open, close_line, new_close = edit + lines[close_line:close_line + 1] = new_close + lines[a:b] = new_open + raise RuntimeError("too many namespace openings") + + +def files(): + out = subprocess.run(["git", "ls-files", "--", *ROOTS], cwd=REPO, capture_output=True, text=True, check=True).stdout + for path in out.split(): + if path.endswith((".h", ".cpp")) and not path.endswith(("robin_hood.h", "lexertk.h")): + yield path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--check", action="store_true", help="change nothing; exit 1 if a file would change") + args = parser.parse_args() + + changed = [] + for path in files(): + full = os.path.join(REPO, path) + with open(full, encoding="utf-8", newline="") as f: + text = f.read() + try: + new = rewrite(text) + except ValueError as e: + print(f"{path}: {e}", file=sys.stderr) + return 2 + if new != text: + changed.append(path) + if not args.check: + with open(full, "w", encoding="utf-8", newline="") as f: + f.write(new) + + if args.check: + for path in changed: + print(f"{path}: namespace jinja2 not wrapped in inline namespace {TAG}") + if changed: + print("Run scripts/abi_namespace.py and commit the result (docs/tasks/0076).") + return 1 + return 0 + print(f"{len(changed)} files rewritten") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ae4649c98baaf1999e99096f7d39ef7a4dca2b8d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 00:44:18 +0000 Subject: [PATCH 2/6] Wrap include/ and src/ in inline namespace jinja2::v2 (0076) Pure output of scripts/abi_namespace.py; no hand edits. Listed in .git-blame-ignore-revs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Ua8ABM1TRsAvso2sWGHFSw --- include/jinja2cpp/binding/boost_json.h | 3 +++ include/jinja2cpp/binding/nlohmann_json.h | 3 +++ include/jinja2cpp/binding/rapid_json.h | 3 +++ include/jinja2cpp/error_info.h | 3 +++ include/jinja2cpp/filesystem_handler.h | 3 +++ include/jinja2cpp/fwd.h | 3 +++ include/jinja2cpp/generic_list.h | 3 +++ include/jinja2cpp/generic_list_iterator.h | 10 ++++++++-- include/jinja2cpp/make_generic_list.h | 3 +++ include/jinja2cpp/markup.h | 3 +++ .../polymorphic_value/indirect_cxx14.h | 2 ++ .../polymorphic_value/polymorphic_cxx14.h | 8 ++++++++ include/jinja2cpp/reflected_value.h | 3 +++ include/jinja2cpp/string_helpers.h | 3 +++ include/jinja2cpp/template.h | 3 +++ include/jinja2cpp/template_env.h | 3 +++ include/jinja2cpp/user_callable.h | 3 +++ include/jinja2cpp/utils/i_comparable.h | 3 +++ include/jinja2cpp/utils/ordered_map.h | 3 +++ include/jinja2cpp/value.h | 3 +++ include/jinja2cpp/value_ptr.h | 20 +++++++++++++++---- src/binding/boost_json_parser.h | 3 +++ src/binding/nlohmann_json_parser.h | 3 +++ src/binding/rapid_json_parser.h | 3 +++ src/cycle_collector.cpp | 3 +++ src/cycle_collector.h | 3 +++ src/error_handling.h | 3 +++ src/error_info.cpp | 3 +++ src/expression_evaluator.cpp | 3 +++ src/expression_evaluator.h | 3 +++ src/expression_parser.cpp | 3 +++ src/expression_parser.h | 3 +++ src/filesystem_handler.cpp | 3 +++ src/filter_factories.cpp | 3 +++ src/filters.cpp | 10 ++++++++-- src/filters.h | 3 +++ src/function_base.h | 3 +++ src/generic_adapters.h | 3 +++ src/generic_list.cpp | 3 +++ src/global_functions.cpp | 3 +++ src/helpers.h | 3 +++ src/html_entities.h | 10 ++++++++-- src/internal_value.cpp | 3 +++ src/internal_value.h | 3 +++ src/lexer.cpp | 3 +++ src/lexer.h | 3 +++ src/load_settings.h | 10 ++++++++-- src/lookup_result.h | 3 +++ src/loop_attr.h | 3 +++ src/make_unexpected.h | 3 +++ src/markup.h | 3 +++ src/name_resolver.cpp | 3 +++ src/name_resolver.h | 3 +++ src/node_arena.cpp | 3 +++ src/node_arena.h | 3 +++ src/out_stream.cpp | 3 +++ src/out_stream.h | 3 +++ src/python_format.cpp | 3 +++ src/python_format.h | 3 +++ src/recursion_guard.cpp | 3 +++ src/recursion_guard.h | 3 +++ src/render_context.h | 3 +++ src/render_workspace.cpp | 3 +++ src/render_workspace.h | 3 +++ src/renderer.h | 3 +++ src/serialize_filters.cpp | 10 ++++++++-- src/slot_frame.h | 3 +++ src/statements.cpp | 3 +++ src/statements.h | 3 +++ src/string_builder.h | 3 +++ src/string_converter_filter.cpp | 20 +++++++++++++++---- src/template.cpp | 3 +++ src/template_env.cpp | 3 +++ src/template_env_impl.h | 10 ++++++++-- src/template_impl.h | 3 +++ src/template_parser.cpp | 3 +++ src/template_parser.h | 3 +++ src/template_slots.h | 3 +++ src/tester_factories.cpp | 3 +++ src/testers.cpp | 10 ++++++++-- src/testers.h | 3 +++ src/undefined.cpp | 3 +++ src/undefined.h | 3 +++ src/unicode_case.h | 10 ++++++++-- src/unicode_printable.h | 10 ++++++++-- src/unicode_tables.h | 3 +++ src/value.cpp | 3 +++ src/value_methods.cpp | 10 ++++++++-- src/value_methods.h | 3 +++ src/value_visitors.h | 3 +++ 90 files changed, 350 insertions(+), 28 deletions(-) diff --git a/include/jinja2cpp/binding/boost_json.h b/include/jinja2cpp/binding/boost_json.h index 3a8449279..a08a73e8c 100644 --- a/include/jinja2cpp/binding/boost_json.h +++ b/include/jinja2cpp/binding/boost_json.h @@ -19,6 +19,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -247,6 +249,7 @@ struct Reflector } }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_BINDING_BOOST_JSON_H diff --git a/include/jinja2cpp/binding/nlohmann_json.h b/include/jinja2cpp/binding/nlohmann_json.h index 6f0ebb80e..91883131c 100644 --- a/include/jinja2cpp/binding/nlohmann_json.h +++ b/include/jinja2cpp/binding/nlohmann_json.h @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -237,6 +239,7 @@ struct Reflector : detail::NLohmannJsonReflector> return GenericMap([accessor = detail::RapidJsonObjectAccessor>(val)]() { return &accessor; }); } }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_BINDING_RAPID_JSON_H diff --git a/include/jinja2cpp/error_info.h b/include/jinja2cpp/error_info.h index faa989d33..af5f4dc2e 100644 --- a/include/jinja2cpp/error_info.h +++ b/include/jinja2cpp/error_info.h @@ -14,6 +14,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN namespace jinja2 { +inline namespace v2 +{ /*! * \brief Type of the error * @@ -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 diff --git a/include/jinja2cpp/filesystem_handler.h b/include/jinja2cpp/filesystem_handler.h index f5b0556c5..f89ffc02b 100644 --- a/include/jinja2cpp/filesystem_handler.h +++ b/include/jinja2cpp/filesystem_handler.h @@ -19,6 +19,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN namespace jinja2 { +inline namespace v2 +{ template using FileStreamPtr = std::unique_ptr, void (*)(std::basic_istream*)>; @@ -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 diff --git a/include/jinja2cpp/fwd.h b/include/jinja2cpp/fwd.h index ae7a3cd75..46844153c 100644 --- a/include/jinja2cpp/fwd.h +++ b/include/jinja2cpp/fwd.h @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ class Value; class GenericList; class GenericMap; @@ -59,6 +61,7 @@ using ErrorInfo = BasicErrorInfo; using ErrorInfoW = BasicErrorInfo; using ValuesList = std::vector; using FilesystemHandlerPtr = std::shared_ptr; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_FWD_H diff --git a/include/jinja2cpp/generic_list.h b/include/jinja2cpp/generic_list.h index 86622e1ee..012624cbc 100644 --- a/include/jinja2cpp/generic_list.h +++ b/include/jinja2cpp/generic_list.h @@ -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 * @@ -287,6 +289,7 @@ inline ListEnumeratorPtr IListItemAccessor::MakeEnumerator(Args&&... args) { return ListEnumeratorPtr{ types::in_place_type_t{}, std::forward(args)... }; } +} // namespace v2 } // namespace jinja2 JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END diff --git a/include/jinja2cpp/generic_list_iterator.h b/include/jinja2cpp/generic_list_iterator.h index 45eab1f69..d5b017568 100644 --- a/include/jinja2cpp/generic_list_iterator.h +++ b/include/jinja2cpp/generic_list_iterator.h @@ -15,7 +15,11 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN -namespace jinja2::detail +namespace jinja2 +{ +inline namespace v2 +{ +namespace detail { class JINJA2CPP_EXPORT GenericListIterator { @@ -124,7 +128,9 @@ class JINJA2CPP_EXPORT GenericListIterator Value m_current; }; -} // namespace jinja2::detail +} // namespace detail +} // namespace v2 +} // namespace jinja2 JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END diff --git a/include/jinja2cpp/make_generic_list.h b/include/jinja2cpp/make_generic_list.h index f1f50dc2e..17e45a473 100644 --- a/include/jinja2cpp/make_generic_list.h +++ b/include/jinja2cpp/make_generic_list.h @@ -18,6 +18,8 @@ namespace jinja2 { +inline namespace v2 +{ /*! * \brief Generator of a generated list, see \ref MakeGenericList(ListGenerator) * @@ -513,6 +515,7 @@ inline GenericList MakeGenericList(ListGenerator fn) return detail::MakeGeneratedList(std::move(fn)); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_MAKE_GENERIC_LIST_H diff --git a/include/jinja2cpp/markup.h b/include/jinja2cpp/markup.h index 7e956ad1f..19dae86db 100644 --- a/include/jinja2cpp/markup.h +++ b/include/jinja2cpp/markup.h @@ -8,6 +8,8 @@ namespace jinja2 { +inline namespace v2 +{ /*! * \brief Text that is already safe HTML: Python's `markupsafe.Markup` * @@ -115,6 +117,7 @@ class BasicMarkup using Markup = BasicMarkup; //! Safe wide text, see \ref BasicMarkup using WMarkup = BasicMarkup; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_MARKUP_H diff --git a/include/jinja2cpp/polymorphic_value/indirect_cxx14.h b/include/jinja2cpp/polymorphic_value/indirect_cxx14.h index 96efe3106..ad16920f1 100644 --- a/include/jinja2cpp/polymorphic_value/indirect_cxx14.h +++ b/include/jinja2cpp/polymorphic_value/indirect_cxx14.h @@ -36,6 +36,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // with it at link time. namespace jinja2 { +inline namespace v2 { namespace detail { namespace xyz { #ifndef JINJA2CPP_XYZ_IN_PLACE_DEFINED @@ -616,6 +617,7 @@ indirect(std::allocator_arg_t, Alloc, Value) -> indirect< } // namespace xyz } // namespace detail +} // namespace v2 } // namespace jinja2 template diff --git a/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h b/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h index b41d702a0..6ea3042d2 100644 --- a/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h +++ b/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h @@ -47,12 +47,14 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #define JINJA2CPP_XYZ_IN_PLACE_TYPE_DEFINED namespace jinja2 { +inline namespace v2 { namespace detail { namespace xyz { template struct in_place_type_t {}; } // namespace xyz } // namespace detail +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_XYZ_IN_PLACE_TYPE_DEFINED @@ -60,6 +62,7 @@ struct in_place_type_t {}; #define JINJA2CPP_XYZ_UNREACHABLE_DEFINED namespace jinja2 { +inline namespace v2 { namespace detail { namespace xyz { [[noreturn]] inline void unreachable() { // LCOV_EXCL_LINE @@ -73,6 +76,7 @@ namespace xyz { } } // namespace xyz } // namespace detail +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_XYZ_UNREACHABLE_DEFINED @@ -85,6 +89,7 @@ namespace xyz { // We duplicate implementations to allow this header to work as a single // include. https://godbolt.org needs single-file includes. namespace jinja2 { +inline namespace v2 { namespace detail { namespace xyz { namespace detail { @@ -121,10 +126,12 @@ class empty_base_optimization : private T { } // namespace detail } // namespace xyz } // namespace detail +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_XYZ_EMPTY_BASE_DEFINED namespace jinja2 { +inline namespace v2 { namespace detail { namespace xyz { @@ -505,6 +512,7 @@ class polymorphic : private detail::empty_base_optimization { } // namespace xyz } // namespace detail +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_XYZ_POLYMORPHIC_H_ diff --git a/include/jinja2cpp/reflected_value.h b/include/jinja2cpp/reflected_value.h index 18a8c9e95..84bd21f6d 100644 --- a/include/jinja2cpp/reflected_value.h +++ b/include/jinja2cpp/reflected_value.h @@ -23,6 +23,8 @@ namespace jinja2 { +inline namespace v2 +{ /*! * \brief Reflect the arbitrary C++ type to the Jinja2C++ engine @@ -687,6 +689,7 @@ Value Reflect(T&& val) return Reflector::Create(std::forward(val)); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_REFLECTED_VALUE_H diff --git a/include/jinja2cpp/string_helpers.h b/include/jinja2cpp/string_helpers.h index b8d27868c..f626f830d 100644 --- a/include/jinja2cpp/string_helpers.h +++ b/include/jinja2cpp/string_helpers.h @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { template @@ -374,6 +376,7 @@ inline std::string AsString(const Value& val) { return std::visit(detail::String //! The stored string, converted if needed, or an empty string for any other value, as in 1.x; \ref ToWString prints every value JINJA2CPP_DEPRECATED("jinja2cpp-2: ToWString") inline std::wstring AsWString(const Value& val) { return std::visit(detail::WStringGetter(), val.GetData()); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_STRING_HELPERS_H diff --git a/include/jinja2cpp/template.h b/include/jinja2cpp/template.h index 60c2cef2f..55e746638 100644 --- a/include/jinja2cpp/template.h +++ b/include/jinja2cpp/template.h @@ -27,6 +27,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN namespace jinja2 { +inline namespace v2 +{ class ITemplateImpl; template class TemplateImpl; @@ -235,6 +237,7 @@ using Template = BasicTemplate; //! Template with wide (`wchar_t`) source and output using TemplateW = BasicTemplate; +} // namespace v2 } // namespace jinja2 JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END diff --git a/include/jinja2cpp/template_env.h b/include/jinja2cpp/template_env.h index 918923270..a787cbb56 100644 --- a/include/jinja2cpp/template_env.h +++ b/include/jinja2cpp/template_env.h @@ -22,6 +22,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -379,6 +381,7 @@ class JINJA2CPP_EXPORT TemplateEnv std::shared_ptr m_impl; }; +} // namespace v2 } // namespace jinja2 JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END diff --git a/include/jinja2cpp/user_callable.h b/include/jinja2cpp/user_callable.h index 78b152e4b..b98d87a38 100644 --- a/include/jinja2cpp/user_callable.h +++ b/include/jinja2cpp/user_callable.h @@ -17,6 +17,8 @@ namespace jinja2 { +inline namespace v2 +{ #ifndef JINJA2CPP_NO_DOXYGEN namespace detail { @@ -455,6 +457,7 @@ auto MakeCallable(Fn&& f) { return UserCallable{ [=, fn = std::forward(f)](const UserCallableParams&) { return fn(); }, {} }; } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_USER_CALLABLE_H diff --git a/include/jinja2cpp/utils/i_comparable.h b/include/jinja2cpp/utils/i_comparable.h index 2f1cbc6de..0860ca1a2 100644 --- a/include/jinja2cpp/utils/i_comparable.h +++ b/include/jinja2cpp/utils/i_comparable.h @@ -8,6 +8,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN namespace jinja2 { +inline namespace v2 +{ /*! * \brief Equality comparison for the extension interfaces (list and map accessors, enumerators, filesystem handlers) @@ -28,6 +30,7 @@ struct JINJA2CPP_EXPORT IComparable [[nodiscard]] virtual bool IsEqual(const IComparable& other) const { return this == &other; } }; +} // namespace v2 } // namespace jinja2 JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END diff --git a/include/jinja2cpp/utils/ordered_map.h b/include/jinja2cpp/utils/ordered_map.h index 6e53b9418..3d120c447 100644 --- a/include/jinja2cpp/utils/ordered_map.h +++ b/include/jinja2cpp/utils/ordered_map.h @@ -17,6 +17,8 @@ namespace jinja2 { +inline namespace v2 +{ // A hash map that iterates in insertion order, like a Python dict (docs/tasks/0031, 0043). It is the base of // the public ValuesMap and of the library's own dicts. @@ -408,6 +410,7 @@ class OrderedMap std::unordered_map m_index; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_UTILS_ORDERED_MAP_H diff --git a/include/jinja2cpp/value.h b/include/jinja2cpp/value.h index 2a05e7ece..63a2d0573 100644 --- a/include/jinja2cpp/value.h +++ b/include/jinja2cpp/value.h @@ -30,6 +30,8 @@ JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_BEGIN namespace jinja2 { +inline namespace v2 +{ //! Python `None`: what a default-constructed \ref Value holds struct NoneValue { @@ -1356,6 +1358,7 @@ decltype(auto) Visit(Fn&& fn, Value& val) return detail::VisitUnwrapped(std::forward(fn), val.GetData()); } +} // namespace v2 } // namespace jinja2 JINJA2CPP_SUPPRESS_DLL_INTERFACE_WARNINGS_END diff --git a/include/jinja2cpp/value_ptr.h b/include/jinja2cpp/value_ptr.h index debdb2d43..6638a919b 100644 --- a/include/jinja2cpp/value_ptr.h +++ b/include/jinja2cpp/value_ptr.h @@ -4,7 +4,11 @@ #include "jinja2cpp/polymorphic_value/indirect_cxx14.h" #include "jinja2cpp/polymorphic_value/polymorphic_cxx14.h" -namespace jinja2::types +namespace jinja2 +{ +inline namespace v2 +{ +namespace types { // Only the names the library's headers use, not the whole vendored namespace @@ -20,9 +24,15 @@ auto MakeValuePtr(Ts&&... ts) return polymorphic(in_place_type_t{}, std::forward(ts)...); } -} // namespace jinja2::types +} // namespace types +} // namespace v2 +} // namespace jinja2 -namespace jinja2::detail +namespace jinja2 +{ +inline namespace v2 +{ +namespace detail { // Owns one heap-allocated T and copies it deeply, as C++26 std::indirect does; T may be // incomplete where the wrapper is declared. Holds the recursive alternatives of Value. @@ -30,6 +40,8 @@ template using indirect = xyz::indirect; // The tag that selects indirect's in-place constructor using in_place_t = xyz::in_place_t; -} // namespace jinja2::detail +} // namespace detail +} // namespace v2 +} // namespace jinja2 #endif // JINJA2CPP_VALUE_PTR_H diff --git a/src/binding/boost_json_parser.h b/src/binding/boost_json_parser.h index d60b41a58..bb12a5606 100644 --- a/src/binding/boost_json_parser.h +++ b/src/binding/boost_json_parser.h @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ template nonstd::expected Parse(std::basic_string_view json, boost::anys::unique_any& metadataJson) @@ -29,6 +31,7 @@ nonstd::expected Parse(std::basic_string_view json, b return Reflect(value); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_BOOST_JSON_PARSER_H diff --git a/src/binding/nlohmann_json_parser.h b/src/binding/nlohmann_json_parser.h index 50e65c6f0..9e9d5f012 100644 --- a/src/binding/nlohmann_json_parser.h +++ b/src/binding/nlohmann_json_parser.h @@ -9,6 +9,8 @@ namespace jinja2 { +inline namespace v2 +{ template nonstd::expected Parse(std::basic_string_view json, boost::anys::unique_any& metadataJson) @@ -26,6 +28,7 @@ nonstd::expected Parse(std::basic_string_view json, b } } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_NLOHMANN_JSON_PARSER_H diff --git a/src/binding/rapid_json_parser.h b/src/binding/rapid_json_parser.h index b0ba13634..6ddcb94de 100644 --- a/src/binding/rapid_json_parser.h +++ b/src/binding/rapid_json_parser.h @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -68,6 +70,7 @@ nonstd::expected Parse(std::basic_string_view json, b return Reflect(jsonDoc); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_RAPID_JSON_PARSER_H diff --git a/src/cycle_collector.cpp b/src/cycle_collector.cpp index 8dfb8df3a..f447f1e0b 100644 --- a/src/cycle_collector.cpp +++ b/src/cycle_collector.cpp @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -262,4 +264,5 @@ void CycleCollector::Collect() noexcept m_notes->candidates.clear(); } +} // namespace v2 } // namespace jinja2 diff --git a/src/cycle_collector.h b/src/cycle_collector.h index 05c4b9e24..99cbb274e 100644 --- a/src/cycle_collector.h +++ b/src/cycle_collector.h @@ -10,6 +10,8 @@ namespace jinja2 { +inline namespace v2 +{ // Frees the reference cycles among the lists, dicts and namespaces a render builds // (docs/tasks/0110). They are shared by reference count, as Python's are, so a container @@ -78,6 +80,7 @@ class CycleCollector std::unique_ptr m_notes; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_CYCLE_COLLECTOR_H diff --git a/src/error_handling.h b/src/error_handling.h index 726a67e43..8eafae953 100644 --- a/src/error_handling.h +++ b/src/error_handling.h @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ struct ParseError { @@ -49,5 +51,6 @@ inline auto MakeParseError(ErrorCode code, Token tok, std::initializer_list namespace jinja2 { +inline namespace v2 +{ template void RenderErrorInfo(std::basic_string& result, const BasicErrorInfo& errInfo) @@ -327,4 +329,5 @@ std::wostream& operator<<(std::wostream& os, const ErrorInfoW& res) os << res.ToString(); return os; } +} // namespace v2 } // namespace jinja2 diff --git a/src/expression_evaluator.cpp b/src/expression_evaluator.cpp index e17293535..a279d828e 100644 --- a/src/expression_evaluator.cpp +++ b/src/expression_evaluator.cpp @@ -41,6 +41,8 @@ using namespace std::string_literals; namespace jinja2 { +inline namespace v2 +{ // Out of line, so that the paths without autoescape stay small void WriteEscaped(OutStream& stream, const InternalValue& val, IRendererCallback* callback) @@ -1589,4 +1591,5 @@ ArenaCallParams ArenaCallParams::Make(NodeArena& nodes, const CallParamsInfo& pa result.kwParams = nodes.MakeSpan(kwParams); return result; } +} // namespace v2 } // namespace jinja2 diff --git a/src/expression_evaluator.h b/src/expression_evaluator.h index 06cf78ecb..dfa99c9a3 100644 --- a/src/expression_evaluator.h +++ b/src/expression_evaluator.h @@ -33,6 +33,8 @@ namespace jinja2 { +inline namespace v2 +{ class ExpressionEvaluatorBase : public ArenaNode { @@ -1064,6 +1066,7 @@ ParsedArgumentsInfo ParseCallParamsInfo(const std::vector& args, c CallParams EvaluateCallParams(const CallParamsInfo& info, RenderContext& context); CallParams EvaluateCallParams(const ArenaCallParams& info, RenderContext& context); } // namespace helpers +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_EXPRESSION_EVALUATOR_H diff --git a/src/expression_parser.cpp b/src/expression_parser.cpp index b5aba9a65..2c98d3afd 100644 --- a/src/expression_parser.cpp +++ b/src/expression_parser.cpp @@ -28,6 +28,8 @@ namespace jinja2 { +inline namespace v2 +{ ParseError::ParseError(const ParseError&) = default; ParseError::ParseError(ParseError&& other) noexcept @@ -1281,4 +1283,5 @@ ExpressionParser::ParseResult> ExpressionParser::ParseIfEx return result; } +} // namespace v2 } // namespace jinja2 diff --git a/src/expression_parser.h b/src/expression_parser.h index b4687a373..1f1504437 100644 --- a/src/expression_parser.h +++ b/src/expression_parser.h @@ -20,6 +20,8 @@ namespace jinja2 { +inline namespace v2 +{ // Jinja2 rejects a filter or test it does not know when it compiles the template, except in // an `if` statement or an inline if, which leave the check to the call (Frame.soft_frame in // jinja2/compiler.py). The expression parsers of one template report each unknown name here. @@ -211,6 +213,7 @@ class ExpressionParser unsigned m_operators = 0; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_EXPRESSION_PARSER_H diff --git a/src/filesystem_handler.cpp b/src/filesystem_handler.cpp index c480524b4..17759ff3f 100644 --- a/src/filesystem_handler.cpp +++ b/src/filesystem_handler.cpp @@ -19,6 +19,8 @@ namespace jinja2 { +inline namespace v2 +{ using TargetFileStream = std::variant; @@ -277,4 +279,5 @@ bool RealFileSystem::IsEqual(const IComparable& other) const return m_rootFolder == ptr->m_rootFolder; } +} // namespace v2 } // namespace jinja2 diff --git a/src/filter_factories.cpp b/src/filter_factories.cpp index 3df04841e..dd2a5e05e 100644 --- a/src/filter_factories.cpp +++ b/src/filter_factories.cpp @@ -22,6 +22,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -164,4 +166,5 @@ FilterPtr CreateFilter(const std::string& filterName, const CallParamsInfo& para return std::make_shared(filterName, params, std::move(callable)); } +} // namespace v2 } // namespace jinja2 diff --git a/src/filters.cpp b/src/filters.cpp index 5867861eb..e6615f135 100644 --- a/src/filters.cpp +++ b/src/filters.cpp @@ -41,7 +41,11 @@ using namespace std::string_literals; -namespace jinja2::filters +namespace jinja2 +{ +inline namespace v2 +{ +namespace filters { // Which values Python can order against each other with `<` @@ -2604,4 +2608,6 @@ InternalValue UserDefinedFilter::Filter(const InternalValue& baseVal, RenderCont return callable->GetExpressionCallable()(callParams, context); } -} // namespace jinja2::filters +} // namespace filters +} // namespace v2 +} // namespace jinja2 diff --git a/src/filters.h b/src/filters.h index dd46467d5..b73e98964 100644 --- a/src/filters.h +++ b/src/filters.h @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ using FilterPtr = std::shared_ptr; using FilterParams = CallParamsInfo; @@ -340,6 +342,7 @@ class UserDefinedFilter : public FilterBase }; } // namespace filters +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_FILTERS_H diff --git a/src/function_base.h b/src/function_base.h index 2af06a502..f0bc1d502 100644 --- a/src/function_base.h +++ b/src/function_base.h @@ -18,6 +18,8 @@ namespace jinja2 { +inline namespace v2 +{ // The parameters of a filter or tester kind, built once (a function-local static): an instance // keeps a pointer to its table, so the names and defaults are stored once per kind using ArgumentsTable = std::vector; @@ -166,6 +168,7 @@ inline bool FunctionBase::ParseParams(const ArgumentsTable& argsInfo, const Call return result; } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_FUNCTION_BASE_H diff --git a/src/generic_adapters.h b/src/generic_adapters.h index a7b982ffd..95313c163 100644 --- a/src/generic_adapters.h +++ b/src/generic_adapters.h @@ -18,6 +18,8 @@ namespace jinja2 { +inline namespace v2 +{ template class IndexedEnumeratorImpl : public Base @@ -331,6 +333,7 @@ inline std::optional IndexedListItemAccessorImpl::CreateEn return MakeEnumerator(this); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_GENERIC_ADAPTERS_H diff --git a/src/generic_list.cpp b/src/generic_list.cpp index cfb5b06c2..2652c907e 100644 --- a/src/generic_list.cpp +++ b/src/generic_list.cpp @@ -3,6 +3,8 @@ namespace jinja2 { +inline namespace v2 +{ GenericList::iterator GenericList::begin() const { @@ -47,4 +49,5 @@ bool operator!=(const GenericList& lhs, const GenericList& rhs) return !(lhs == rhs); } +} // namespace v2 } // namespace jinja2 diff --git a/src/global_functions.cpp b/src/global_functions.cpp index 573a1a83c..f6851bb56 100644 --- a/src/global_functions.cpp +++ b/src/global_functions.cpp @@ -28,6 +28,8 @@ using namespace std::string_literals; namespace jinja2 { +inline namespace v2 +{ namespace { // jinja2.constants.LOREM_IPSUM_WORDS @@ -530,4 +532,5 @@ const InternalValueMap& GetBuiltinGlobals(bool withI18n) }(); return withI18n ? globalsWithI18n : globals; } +} // namespace v2 } // namespace jinja2 diff --git a/src/helpers.h b/src/helpers.h index b189b84c9..ec0eab7f1 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -9,6 +9,8 @@ namespace jinja2 { +inline namespace v2 +{ struct MultiStringLiteral { const char* charValue; @@ -281,6 +283,7 @@ Sequence CompileEscapes(Sequence s, bool* ok = nullptr) return s; } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_HELPERS_H diff --git a/src/html_entities.h b/src/html_entities.h index ffe3f2de9..b02240303 100644 --- a/src/html_entities.h +++ b/src/html_entities.h @@ -8,7 +8,11 @@ #include #include -namespace jinja2::html +namespace jinja2 +{ +inline namespace v2 +{ +namespace html { // A named character reference and the one or two code points it stands for (0 when only one) @@ -2261,6 +2265,8 @@ inline const HtmlEntity* FindHtmlEntity(std::string_view name) return it != std::end(HtmlEntities) && std::string_view(it->name) == name ? &*it : nullptr; } -} // namespace jinja2::html +} // namespace html +} // namespace v2 +} // namespace jinja2 #endif // JINJA2CPP_SRC_HTML_ENTITIES_H diff --git a/src/internal_value.cpp b/src/internal_value.cpp index 8f4405d97..03d299ffc 100644 --- a/src/internal_value.cpp +++ b/src/internal_value.cpp @@ -31,6 +31,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -2246,4 +2248,5 @@ bool Callable::IsSame(const Callable& other) const return left != nullptr && right != nullptr && left->val.IsEqual(right->val); } +} // namespace v2 } // namespace jinja2 diff --git a/src/internal_value.h b/src/internal_value.h index c27717ae7..d8e1a7162 100644 --- a/src/internal_value.h +++ b/src/internal_value.h @@ -45,6 +45,8 @@ namespace jinja2 { +inline namespace v2 +{ template class ReferenceWrapper @@ -1746,6 +1748,7 @@ static_assert(std::is_nothrow_move_assignable_v); Value IntValue2Value(const InternalValue& val); Value OptIntValue2Value(std::optional val); +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_INTERNAL_VALUE_H diff --git a/src/lexer.cpp b/src/lexer.cpp index 7a33af003..4af410a48 100644 --- a/src/lexer.cpp +++ b/src/lexer.cpp @@ -13,6 +13,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -77,4 +79,5 @@ InternalValue ParseNumberLiteral(std::string number) return InternalValue(std::strtod(digits, nullptr)); } +} // namespace v2 } // namespace jinja2 diff --git a/src/lexer.h b/src/lexer.h index f932d0ce0..2596b35ae 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ struct CharRange { size_t startOffset; @@ -919,6 +921,7 @@ class LexScanner } }; +} // namespace v2 } // namespace jinja2 namespace std diff --git a/src/load_settings.h b/src/load_settings.h index 9ce584d18..684bf0dd5 100644 --- a/src/load_settings.h +++ b/src/load_settings.h @@ -11,7 +11,11 @@ #include #include -namespace jinja2::detail +namespace jinja2 +{ +inline namespace v2 +{ +namespace detail { // Delimiters and line prefixes from Settings, in the character type of a template @@ -122,6 +126,8 @@ using LoadSettingsPtr = std::shared_ptr; // For templates made without an environment const LoadSettingsPtr& DefaultLoadSettings(); -} // namespace jinja2::detail +} // namespace detail +} // namespace v2 +} // namespace jinja2 #endif // JINJA2CPP_SRC_LOAD_SETTINGS_H diff --git a/src/lookup_result.h b/src/lookup_result.h index 8107208c5..3586d255d 100644 --- a/src/lookup_result.h +++ b/src/lookup_result.h @@ -7,6 +7,8 @@ namespace jinja2 { +inline namespace v2 +{ // Where a lookup found a value, or nothing: a nullable reference to a value stored elsewhere // (a scope, a constant node), read in place instead of copied (docs/tasks/0088, 0117). It is // valid while what holds the value is unchanged: for a variable, until a scope of the @@ -47,6 +49,7 @@ class [[nodiscard]] BasicLookupResult using LookupResult = BasicLookupResult; // A variable found for changing in place (docs/tasks/0020) using MutableLookupResult = BasicLookupResult; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_LOOKUP_RESULT_H diff --git a/src/loop_attr.h b/src/loop_attr.h index 30de75c8f..d77d45f2c 100644 --- a/src/loop_attr.h +++ b/src/loop_attr.h @@ -6,6 +6,8 @@ namespace jinja2 { +inline namespace v2 +{ // An attribute of a for loop's `loop` object. `loop.` in the template is resolved to // one of these at Load, so a render reads the attribute without comparing strings // (docs/design/perf-design-overview.md row 17; 0117 P1) @@ -61,6 +63,7 @@ inline LoopAttr FindLoopAttr(std::string_view name) } return LoopAttr::None; } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_LOOP_ATTR_H diff --git a/src/make_unexpected.h b/src/make_unexpected.h index 475394766..821f760a7 100644 --- a/src/make_unexpected.h +++ b/src/make_unexpected.h @@ -7,6 +7,8 @@ namespace jinja2 { +inline namespace v2 +{ // The one place that names the backing `expected` library's error factory, so that // switching it (docs/api-2.0.md, decision 4) touches this function and not every @@ -17,6 +19,7 @@ auto MakeUnexpected(E&& error) return nonstd::make_unexpected(std::forward(error)); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_MAKE_UNEXPECTED_H diff --git a/src/markup.h b/src/markup.h index e5f34f6e9..bfb702180 100644 --- a/src/markup.h +++ b/src/markup.h @@ -16,6 +16,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -218,6 +220,7 @@ inline void WriteOutput(OutStream& stream, const InternalValue& val, RenderConte WriteEscaped(stream, val, context.GetRendererCallback()); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_MARKUP_H diff --git a/src/name_resolver.cpp b/src/name_resolver.cpp index 9c8381cf1..a783d2cf7 100644 --- a/src/name_resolver.cpp +++ b/src/name_resolver.cpp @@ -19,6 +19,8 @@ namespace jinja2 { +inline namespace v2 +{ NameResolver::FrameId NameResolver::Push(FrameId parent, Kind kind) { const auto id = static_cast(m_frames.size()); @@ -488,4 +490,5 @@ std::pair NameResolver::Find(const NodeArena& nodes, NodeRef< } return dynamic; } +} // namespace v2 } // namespace jinja2 diff --git a/src/name_resolver.h b/src/name_resolver.h index 902b1fc65..4d7ea777b 100644 --- a/src/name_resolver.h +++ b/src/name_resolver.h @@ -17,6 +17,8 @@ namespace jinja2 { +inline namespace v2 +{ class BindUndefinedStatement; class ForStatement; class IRendererBase; @@ -246,6 +248,7 @@ class NameResolver bool m_inBranch = false; bool m_hasSlots = false; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_NAME_RESOLVER_H diff --git a/src/node_arena.cpp b/src/node_arena.cpp index f5cc922da..d039dd8dd 100644 --- a/src/node_arena.cpp +++ b/src/node_arena.cpp @@ -20,6 +20,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { void OffsetList::Grow() @@ -259,4 +261,5 @@ SealedArena NodeArena::SealWith(boost::span roots) } return sealed; } +} // namespace v2 } // namespace jinja2 diff --git a/src/node_arena.h b/src/node_arena.h index 2caba5c85..fc0aa055a 100644 --- a/src/node_arena.h +++ b/src/node_arena.h @@ -37,6 +37,8 @@ namespace jinja2 { +inline namespace v2 +{ // How much a handle is checked before it is followed (CMake JINJA2CPP_NODEREF_CHECKS): // 0 (OFF): never. 1 (ON, the Release default): Seal checks every handle of the tree, and // a render checks the ones that cross into another template. 2 (FULL, Debug, sanitizer and @@ -1392,6 +1394,7 @@ class NodeArena : public detail::ArenaAccess std::size_t m_symbolCount = 0; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_NODE_ARENA_H diff --git a/src/out_stream.cpp b/src/out_stream.cpp index d64adf61d..039d07b5f 100644 --- a/src/out_stream.cpp +++ b/src/out_stream.cpp @@ -13,6 +13,8 @@ namespace jinja2 { +inline namespace v2 +{ void OutStream::Flush() { if (!m_writer) @@ -262,4 +264,5 @@ void OutStream::WriteValueTo(const InternalValue& val) template void OutStream::WriteValueTo(const InternalValue& val); template void OutStream::WriteValueTo(const InternalValue& val); +} // namespace v2 } // namespace jinja2 diff --git a/src/out_stream.h b/src/out_stream.h index a080c4de8..432e41b2e 100644 --- a/src/out_stream.h +++ b/src/out_stream.h @@ -11,6 +11,8 @@ namespace jinja2 { +inline namespace v2 +{ // Where a render writes its output. A render to a string, the common case, gathers the // output in a small buffer and appends it to the string a buffer at a time: a fragment // costs a bounds check and a memcpy, with no virtual call (docs/tasks/0113). Any other @@ -171,6 +173,7 @@ TargetString RenderToString(Callback* callback, Fn&& render) return result; } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_OUT_STREAM_H diff --git a/src/python_format.cpp b/src/python_format.cpp index c6739a14c..ca83ad2bd 100644 --- a/src/python_format.cpp +++ b/src/python_format.cpp @@ -21,6 +21,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -864,4 +866,5 @@ std::string CompiledPercentFormat::Format(const InternalValue* args, size_t coun return Formatter(args, count, false).Run(m_format, m_pieces); } +} // namespace v2 } // namespace jinja2 diff --git a/src/python_format.h b/src/python_format.h index 6c1e35dc8..77487aefa 100644 --- a/src/python_format.h +++ b/src/python_format.h @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ // Python's printf-style formatting, `format % values`, for a UTF-8 format string. `values` is // what Python's right operand is: a tuple (a list marked as tuple) of positional arguments, a @@ -78,6 +80,7 @@ class CompiledPercentFormat std::vector m_pieces; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_PYTHON_FORMAT_H diff --git a/src/recursion_guard.cpp b/src/recursion_guard.cpp index df22bb284..6716709a4 100644 --- a/src/recursion_guard.cpp +++ b/src/recursion_guard.cpp @@ -29,6 +29,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { // Stack left free for the work between two checks: a filter, a value conversion, the @@ -104,4 +106,5 @@ bool StackNearlyExhaustedSlow(std::uintptr_t frame) } } // namespace detail +} // namespace v2 } // namespace jinja2 diff --git a/src/recursion_guard.h b/src/recursion_guard.h index 4848d66cf..6f8c617b9 100644 --- a/src/recursion_guard.h +++ b/src/recursion_guard.h @@ -11,6 +11,8 @@ namespace jinja2 { +inline namespace v2 +{ // Limits that keep hostile or runaway templates from overflowing the stack (docs/tasks/0003). // @@ -189,6 +191,7 @@ class RenderDepthGuard DepthGuard m_guard; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_RECURSION_GUARD_H diff --git a/src/render_context.h b/src/render_context.h index f4b56a657..9bb7216f9 100644 --- a/src/render_context.h +++ b/src/render_context.h @@ -51,6 +51,8 @@ namespace jinja2 { +inline namespace v2 +{ template class TemplateImpl; @@ -1987,6 +1989,7 @@ class HomeCall int m_debugExceptions = 0; #endif }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_RENDER_CONTEXT_H diff --git a/src/render_workspace.cpp b/src/render_workspace.cpp index dc4c98897..714f1eb86 100644 --- a/src/render_workspace.cpp +++ b/src/render_workspace.cpp @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ RenderWorkspace::RenderWorkspace() { m_loopFrames.reserve(MaxLoopFrames); @@ -70,4 +72,5 @@ void RenderWorkspace::EmptyMaps(RenderMaps& maps, bool keepTables) noexcept } } } +} // namespace v2 } // namespace jinja2 diff --git a/src/render_workspace.h b/src/render_workspace.h index d6cace44b..8b9408d84 100644 --- a/src/render_workspace.h +++ b/src/render_workspace.h @@ -16,6 +16,8 @@ namespace jinja2 { +inline namespace v2 +{ struct GlobalsSnapshot; namespace detail @@ -171,6 +173,7 @@ class RenderWorkspace std::vector> m_maps; std::size_t m_lentMaps = 0; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_RENDER_WORKSPACE_H diff --git a/src/renderer.h b/src/renderer.h index 4368339d9..7ba7cfef3 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -18,6 +18,8 @@ namespace jinja2 { +inline namespace v2 +{ class IRendererBase : public ArenaNode { public: @@ -181,6 +183,7 @@ inline NodeRef MakeExpressionRenderer(NodeArena& nodes, NodeRef(expr); } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_RENDERER_H diff --git a/src/serialize_filters.cpp b/src/serialize_filters.cpp index 71bc148a0..315ff9289 100644 --- a/src/serialize_filters.cpp +++ b/src/serialize_filters.cpp @@ -30,7 +30,11 @@ using namespace std::string_literals; -namespace jinja2::filters +namespace jinja2 +{ +inline namespace v2 +{ +namespace filters { struct PrettyPrinter : visitors::BaseVisitor { @@ -620,4 +624,6 @@ InternalValue XmlAttrFilter::Filter(const InternalValue& baseVal, RenderContext& return resultVal; } -} // namespace jinja2::filters +} // namespace filters +} // namespace v2 +} // namespace jinja2 diff --git a/src/slot_frame.h b/src/slot_frame.h index 037bea479..b134123c4 100644 --- a/src/slot_frame.h +++ b/src/slot_frame.h @@ -25,6 +25,8 @@ namespace jinja2 { +inline namespace v2 +{ // A name's slot in the frame of its unit; Dynamic for a name looked up by name struct SlotIndex { @@ -152,6 +154,7 @@ struct ScopeView bool operator==(const ScopeView& other) const { return view == other.view && scopeIndex == other.scopeIndex && linkBelow == other.linkBelow; } bool operator!=(const ScopeView& other) const { return !(*this == other); } }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_SLOT_FRAME_H diff --git a/src/statements.cpp b/src/statements.cpp index 6c40b51c9..6f6e4a6a7 100644 --- a/src/statements.cpp +++ b/src/statements.cpp @@ -47,6 +47,8 @@ using namespace std::string_literals; namespace jinja2 { +inline namespace v2 +{ void ForStatement::Render(OutStream& os, RenderContext& values) { @@ -2642,4 +2644,5 @@ void BindUndefinedStatement::Render(OutStream& /*os*/, RenderContext& values) } } } +} // namespace v2 } // namespace jinja2 diff --git a/src/statements.h b/src/statements.h index 82c7532db..32da9d31c 100644 --- a/src/statements.h +++ b/src/statements.h @@ -22,6 +22,8 @@ namespace jinja2 { +inline namespace v2 +{ class Statement : public IRendererBase { public: @@ -962,6 +964,7 @@ class BindUndefinedStatement final : public Statement ArenaSpan m_names; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_STATEMENTS_H diff --git a/src/string_builder.h b/src/string_builder.h index 2278b1437..752e2876c 100644 --- a/src/string_builder.h +++ b/src/string_builder.h @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ // The text of a string value being built: the first 500 characters on the stack, so that the // finished value allocates once, its object holding the characters (docs/tasks/0151). Code that @@ -64,6 +66,7 @@ class StringBuilder fmt::basic_memory_buffer m_buffer; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_STRING_BUILDER_H diff --git a/src/string_converter_filter.cpp b/src/string_converter_filter.cpp index 58f23017c..27b205cff 100644 --- a/src/string_converter_filter.cpp +++ b/src/string_converter_filter.cpp @@ -35,7 +35,11 @@ namespace ba = boost::algorithm; using namespace std::string_literals; -namespace jinja2::filters +namespace jinja2 +{ +inline namespace v2 +{ +namespace filters { template @@ -2035,9 +2039,15 @@ InternalValue StringConverter::Filter(const InternalValue& baseVal, RenderContex return resultVal; } -} // namespace jinja2::filters +} // namespace filters +} // namespace v2 +} // namespace jinja2 -namespace jinja2::detail +namespace jinja2 +{ +inline namespace v2 +{ +namespace detail { namespace { @@ -2091,4 +2101,6 @@ int CompareLowerUnicode(std::wstring_view left, std::wstring_view right) { return CompareLower(left, right); } -} // namespace jinja2::detail +} // namespace detail +} // namespace v2 +} // namespace jinja2 diff --git a/src/template.cpp b/src/template.cpp index 07103b2c8..75e7d7312 100644 --- a/src/template.cpp +++ b/src/template.cpp @@ -20,6 +20,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { template @@ -182,4 +184,5 @@ template class BasicTemplate; template class BasicTemplate; #endif +} // namespace v2 } // namespace jinja2 diff --git a/src/template_env.cpp b/src/template_env.cpp index 312c9f7d5..a02a5ba1f 100644 --- a/src/template_env.cpp +++ b/src/template_env.cpp @@ -26,6 +26,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -487,4 +489,5 @@ void TemplateEnv::ApplyGlobals(const std::function& fn) fn(*m_impl->globalValues); } +} // namespace v2 } // namespace jinja2 diff --git a/src/template_env_impl.h b/src/template_env_impl.h index e39adf31c..176759c19 100644 --- a/src/template_env_impl.h +++ b/src/template_env_impl.h @@ -19,7 +19,11 @@ #include #include -namespace jinja2::detail +namespace jinja2 +{ +inline namespace v2 +{ +namespace detail { // The state of a TemplateEnv. The TemplateEnv the user creates owns it; every template made in the environment holds @@ -100,6 +104,8 @@ struct TemplateEnvAccess static TemplateEnv MakeHandle(const TemplateEnv* env) { return TemplateEnv(env ? env->m_impl : nullptr); } }; -} // namespace jinja2::detail +} // namespace detail +} // namespace v2 +} // namespace jinja2 #endif // JINJA2CPP_SRC_TEMPLATE_ENV_IMPL_H diff --git a/src/template_impl.h b/src/template_impl.h index d3538f202..9fa80f3b5 100644 --- a/src/template_impl.h +++ b/src/template_impl.h @@ -63,6 +63,8 @@ namespace jinja2 { +inline namespace v2 +{ // The default globals of every render, looked up after the environment's (global_functions.cpp) extern const InternalValueMap& GetBuiltinGlobals(bool withI18n); @@ -827,6 +829,7 @@ class TemplateImpl : public ITemplateImpl MetadataInfo m_metadataInfo; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_TEMPLATE_IMPL_H diff --git a/src/template_parser.cpp b/src/template_parser.cpp index 88c0723bc..eb4a4879f 100644 --- a/src/template_parser.cpp +++ b/src/template_parser.cpp @@ -34,6 +34,8 @@ namespace jinja2 { +inline namespace v2 +{ NodeRef StatementsParser::TakeFrameBody(StatementInfo& info) { @@ -1943,4 +1945,5 @@ InternalValue EscapedLiteralValue(std::basic_string_view text, const std: template InternalValue EscapedLiteralValue(std::string_view text, const std::string& newlineSequence, bool& malformed); template InternalValue EscapedLiteralValue(std::wstring_view text, const std::string& newlineSequence, bool& malformed); +} // namespace v2 } // namespace jinja2 diff --git a/src/template_parser.h b/src/template_parser.h index 787960f41..fe0cc7625 100644 --- a/src/template_parser.h +++ b/src/template_parser.h @@ -50,6 +50,8 @@ namespace jinja2 { +inline namespace v2 +{ template struct ParserTraits; @@ -2391,6 +2393,7 @@ std::unordered_map ParserTraitsBase::s_tokens = { { Token::ExprEnd, UNIVERSAL_STR("}}") }, }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_TEMPLATE_PARSER_H diff --git a/src/template_slots.h b/src/template_slots.h index 21f1781f9..3d755f6b4 100644 --- a/src/template_slots.h +++ b/src/template_slots.h @@ -15,6 +15,8 @@ namespace jinja2 { +inline namespace v2 +{ // A template one render runs, by its place in the render's table of templates (0118 P4b). // The render keeps every template it loads until it ends, so a handle needs no reference @@ -165,6 +167,7 @@ class TemplateSlots std::uint8_t m_generation = 0; }; +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_TEMPLATE_SLOTS_H diff --git a/src/tester_factories.cpp b/src/tester_factories.cpp index 2a3c452ba..8136a2b80 100644 --- a/src/tester_factories.cpp +++ b/src/tester_factories.cpp @@ -22,6 +22,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -149,4 +151,5 @@ TesterPtr CreateTester(std::string testerName, const CallParamsInfo& params, Ren return std::make_shared(std::move(testerName), params, std::move(callable)); } +} // namespace v2 } // namespace jinja2 diff --git a/src/testers.cpp b/src/testers.cpp index caba25367..a44b4af05 100644 --- a/src/testers.cpp +++ b/src/testers.cpp @@ -23,7 +23,11 @@ using namespace std::string_literals; -namespace jinja2::testers +namespace jinja2 +{ +inline namespace v2 +{ +namespace testers { Comparator::Comparator(const TesterParams& params, BinaryExpression::Operation op) @@ -598,4 +602,6 @@ bool UserDefinedTester::Test(const InternalValue& baseVal, RenderContext& contex return ConvertToBool(callable->GetExpressionCallable()(callParams, context)); } -} // namespace jinja2::testers +} // namespace testers +} // namespace v2 +} // namespace jinja2 diff --git a/src/testers.h b/src/testers.h index 37f906679..0c2e8298c 100644 --- a/src/testers.h +++ b/src/testers.h @@ -13,6 +13,8 @@ namespace jinja2 { +inline namespace v2 +{ using TesterPtr = std::shared_ptr; using TesterParams = CallParamsInfo; @@ -117,6 +119,7 @@ class UserDefinedTester : public TesterBase InternalValue m_callable; }; } // namespace testers +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_TESTERS_H diff --git a/src/undefined.cpp b/src/undefined.cpp index a397e1107..ce865586c 100644 --- a/src/undefined.cpp +++ b/src/undefined.cpp @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { @@ -132,4 +134,5 @@ const char* UndefinedClassName(const UndefinedInfo& info) return "Undefined"; } +} // namespace v2 } // namespace jinja2 diff --git a/src/undefined.h b/src/undefined.h index a1ae94598..a7b1c614e 100644 --- a/src/undefined.h +++ b/src/undefined.h @@ -12,6 +12,8 @@ namespace jinja2 { +inline namespace v2 +{ // What a named lookup missed, and the policy that decides how the undefined result may be // used. It travels with the value, as the class of a Python Undefined does: a variable set @@ -102,6 +104,7 @@ inline void CheckStrictUndefined(const UndefinedValue& val) } } +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_UNDEFINED_H diff --git a/src/unicode_case.h b/src/unicode_case.h index 75a163e37..0e6d46c9d 100644 --- a/src/unicode_case.h +++ b/src/unicode_case.h @@ -10,7 +10,11 @@ #include #include -namespace jinja2::unicode +namespace jinja2 +{ +inline namespace v2 +{ +namespace unicode { // cp + delta for every stride-th code point from first to last @@ -1413,6 +1417,8 @@ inline size_t ToTitle(uint32_t cp, uint32_t (&out)[3]) { return MapCase(TitleRun inline bool IsCased(uint32_t cp) { return InRanges(CasedRanges, cp); } inline bool IsCaseIgnorable(uint32_t cp) { return InRanges(CaseIgnorableRanges, cp); } -} // namespace jinja2::unicode +} // namespace unicode +} // namespace v2 +} // namespace jinja2 #endif // JINJA2CPP_SRC_UNICODE_CASE_H diff --git a/src/unicode_printable.h b/src/unicode_printable.h index 8162d7607..8fa21ec43 100644 --- a/src/unicode_printable.h +++ b/src/unicode_printable.h @@ -7,7 +7,11 @@ #include #include -namespace jinja2::detail +namespace jinja2 +{ +inline namespace v2 +{ +namespace detail { // True when Python's str.isprintable() holds for the code point, so repr() keeps it as is @@ -216,6 +220,8 @@ inline bool IsPythonPrintable(uint32_t cp) return cp > it->last; } -} // namespace jinja2::detail +} // namespace detail +} // namespace v2 +} // namespace jinja2 #endif // JINJA2CPP_SRC_UNICODE_PRINTABLE_H diff --git a/src/unicode_tables.h b/src/unicode_tables.h index b43614642..fce4ba143 100644 --- a/src/unicode_tables.h +++ b/src/unicode_tables.h @@ -9,6 +9,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace unicode { @@ -877,6 +879,7 @@ inline bool IsSpace(uint32_t cp) } } // namespace unicode +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_UNICODE_TABLES_H diff --git a/src/value.cpp b/src/value.cpp index 9594ad940..b6824083c 100644 --- a/src/value.cpp +++ b/src/value.cpp @@ -14,6 +14,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace { // Every printing form of Value meets here, so none can print differently from {{ value }}: @@ -115,4 +117,5 @@ std::to_chars_result Value::ToChars(char* first, char* last) const noexcept } } +} // namespace v2 } // namespace jinja2 diff --git a/src/value_methods.cpp b/src/value_methods.cpp index f098d3879..39d77bc39 100644 --- a/src/value_methods.cpp +++ b/src/value_methods.cpp @@ -31,7 +31,11 @@ using namespace std::string_literals; -namespace jinja2::methods +namespace jinja2 +{ +inline namespace v2 +{ +namespace methods { namespace { @@ -2388,4 +2392,6 @@ void ThrowNoAttribute(const InternalValue& obj, const std::string& name) Raise("'" + TypeName(obj) + " object' has no attribute '" + name + "'"); } -} // namespace jinja2::methods +} // namespace methods +} // namespace v2 +} // namespace jinja2 diff --git a/src/value_methods.h b/src/value_methods.h index 9d6f2df47..ff9283195 100644 --- a/src/value_methods.h +++ b/src/value_methods.h @@ -8,6 +8,8 @@ namespace jinja2 { +inline namespace v2 +{ struct CallParams; // Python's builtin methods on template values: s.upper(), l.append(x), d.items(), ... @@ -65,6 +67,7 @@ bool IsContainer(const InternalValue& value); [[noreturn]] void ThrowNoAttribute(const InternalValue& obj, const std::string& name); } // namespace methods +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_VALUE_METHODS_H diff --git a/src/value_visitors.h b/src/value_visitors.h index 982d139ff..a3ac1af01 100644 --- a/src/value_visitors.h +++ b/src/value_visitors.h @@ -37,6 +37,8 @@ namespace jinja2 { +inline namespace v2 +{ namespace detail { @@ -2069,6 +2071,7 @@ JINJA2CPP_HOT_INLINE InternalValue ApplyToNumbers(const InternalValue& left, con } // namespace visitors +} // namespace v2 } // namespace jinja2 #endif // JINJA2CPP_SRC_VALUE_VISITORS_H From a55332875f404f893195e387ce38e1e1bc937771 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 00:53:06 +0000 Subject: [PATCH 3/6] ABI gates for the inline namespace v2: nm test, link probes, SONAME, abidiff (0076) - format-check runs scripts/abi_namespace.py --check - CTest jinja2cpp_abi_namespace: no jinja2 symbol outside v2, no global xyz - test/package: abi_probe_v2 links, abi_probe_v1 (1.x mangling) must not - check_package.cmake checks the installed SONAME; version asserts need major 2 - linux-build: one shared RelWithDebInfo row per standard dumps the ABI with abidw and reports abidiff against the last master build (baselines at the RC, task 0285) - README: Compatibility and versioning; find_package(jinja2cpp 2) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Ua8ABM1TRsAvso2sWGHFSw --- .git-blame-ignore-revs | 3 + .github/workflows/format-check.yml | 5 ++ .github/workflows/linux-build.yml | 51 +++++++++++- CMakeLists.txt | 6 ++ README.md | 23 +++++- abi/suppressions.ini | 22 +++++ docs/tasks/0076-abi-namespace-and-headers.md | 16 +++- docs/tasks/0285-abi-baselines-at-rc.md | 26 ++++++ .../polymorphic_value/indirect_cxx14.h | 2 +- .../polymorphic_value/polymorphic_cxx14.h | 2 +- sbom/components.json | 18 ++++ sbom/jinja2cpp-full.cdx.json | 73 +++++++++++++++-- sbom/jinja2cpp-full.spdx.json | 36 ++++++-- sbom/jinja2cpp.cdx.json | 16 ++-- sbom/jinja2cpp.spdx.json | 10 +-- scripts/abi_report.sh | 82 +++++++++++++++++++ test/abi_symbols.cmake | 48 +++++++++++ test/package/CMakeLists.txt | 15 ++++ test/package/abi_probe.cpp | 28 +++++++ test/package/check_package.cmake | 22 +++++ test/package/main.cpp | 5 +- test/template_api_test.cpp | 3 +- 22 files changed, 475 insertions(+), 37 deletions(-) create mode 100644 abi/suppressions.ini create mode 100644 docs/tasks/0285-abi-baselines-at-rc.md create mode 100755 scripts/abi_report.sh create mode 100644 test/abi_symbols.cmake create mode 100644 test/package/abi_probe.cpp diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index c1dbb6264..aadb81db3 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -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 diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index c7f97435f..876aeb361 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -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). diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index e2cdfc8f0..a75d72278 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -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 @@ -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 } @@ -99,6 +104,46 @@ 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.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 }} + + - if: matrix.abi + uses: actions/upload-artifact@v4 + with: + name: abi-cxx${{ matrix.std }} + path: .abi/ + - name: Installed package if: matrix.package run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index fc21e80f1..5321c38ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -454,6 +454,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=$ + -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") diff --git a/README.md b/README.md index 3b63ff5c1..716818956 100644 --- a/README.md +++ b/README.md @@ -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) ``` @@ -201,6 +201,27 @@ 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 ``: a hand-written + `namespace jinja2 { class Value; }` declares a different, undefined class and makes later + uses ambiguous. `` 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. +- **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: diff --git a/abi/suppressions.ini b/abi/suppressions.ini new file mode 100644 index 000000000..27f5f5e65 --- /dev/null +++ b/abi/suppressions.ini @@ -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):: diff --git a/docs/tasks/0076-abi-namespace-and-headers.md b/docs/tasks/0076-abi-namespace-and-headers.md index cbd7b3bf9..f0ae981e9 100644 --- a/docs/tasks/0076-abi-namespace-and-headers.md +++ b/docs/tasks/0076-abi-namespace-and-headers.md @@ -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 @@ -36,3 +36,15 @@ 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 (``), 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. diff --git a/docs/tasks/0285-abi-baselines-at-rc.md b/docs/tasks/0285-abi-baselines-at-rc.md new file mode 100644 index 000000000..82cb6ada8 --- /dev/null +++ b/docs/tasks/0285-abi-baselines-at-rc.md @@ -0,0 +1,26 @@ +--- +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. + +**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. diff --git a/include/jinja2cpp/polymorphic_value/indirect_cxx14.h b/include/jinja2cpp/polymorphic_value/indirect_cxx14.h index ad16920f1..89b27cdf4 100644 --- a/include/jinja2cpp/polymorphic_value/indirect_cxx14.h +++ b/include/jinja2cpp/polymorphic_value/indirect_cxx14.h @@ -30,7 +30,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // Local change: upstream includes "feature_check.h" here for // XYZ_HAS_TEMPLATE_ARGUMENT_DEDUCTION; the C++17 floor always has it, and this // copy stays a single header. Vendored from jbcoe/value_types 57f2e7a (task 0072). -// Local change (task 0076): namespace xyz moved into jinja2::detail and the +// Local change (task 0076): namespace xyz moved into jinja2::v2::detail and the // XYZ_ macros renamed JINJA2CPP_XYZ_, so a user's own copy of value_types // neither shadows this one through the shared include guards nor collides // with it at link time. diff --git a/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h b/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h index 6ea3042d2..489ae76f9 100644 --- a/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h +++ b/include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h @@ -29,7 +29,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // Vendored from jbcoe/value_types 57f2e7a (compatibility/polymorphic_cxx14.h) // with these local changes: -// - namespace xyz moved into jinja2::detail and the XYZ_ macros renamed +// - namespace xyz moved into jinja2::v2::detail and the XYZ_ macros renamed // JINJA2CPP_XYZ_ (task 0076), so a user's own copy of value_types neither // shadows this one through the shared include guards nor collides with it at // link time; diff --git a/sbom/components.json b/sbom/components.json index 1a33e1033..c2012833a 100644 --- a/sbom/components.json +++ b/sbom/components.json @@ -571,6 +571,24 @@ "lighten": "Not part of any build", "evidence": [{"file": ".github/workflows/format-check.yml", "contains": ["clang-format-18"]}] }, + { + "id": "libabigail", + "name": "libabigail (abigail-tools)", + "version": "2.4", + "license": "Apache-2.0 WITH LLVM-exception", + "supplier": "Red Hat / sourceware.org", + "url": "https://sourceware.org/libabigail/", + "purl": "pkg:generic/libabigail@2.4", + "delivery": "system", + "role": "tooling", + "phases": ["ci"], + "exposure": "none", + "selected_by": "linux-build CI abi rows (apt abigail-tools, Ubuntu 24.04)", + "used_for": "ABI dump and comparison of the shared library (abidw, abidiff)", + "why": "SOVERSION policy gate (docs/tasks/0076)", + "lighten": "Not part of any build", + "evidence": [{"file": ".github/workflows/linux-build.yml", "contains": ["abigail-tools"]}] + }, { "id": "cmake", "name": "CMake", diff --git a/sbom/jinja2cpp-full.cdx.json b/sbom/jinja2cpp-full.cdx.json index f241ed167..0af193245 100644 --- a/sbom/jinja2cpp-full.cdx.json +++ b/sbom/jinja2cpp-full.cdx.json @@ -1,7 +1,7 @@ { "bomFormat": "CycloneDX", "specVersion": "1.6", - "serialNumber": "urn:uuid:3496cdbc-cf00-54e8-9a51-38efe8127a5d", + "serialNumber": "urn:uuid:05ea2675-8382-5679-ad05-ef9a8b7bf331", "version": 1, "metadata": { "timestamp": "2026-10-18T00:00:00Z", @@ -1163,11 +1163,11 @@ }, { "name": "jinja2cpp:file", - "value": "include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:b00dba6183edea9532030a373956ecab3638b6c5cfe61f40cbe862fba701dce3" + "value": "include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a" }, { "name": "jinja2cpp:file", - "value": "include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:eb0e46a2c95e693be7a192c1a992ece6e6fb900235ac44620f220bd646b2e372" + "value": "include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" } ] }, @@ -1229,15 +1229,15 @@ }, { "name": "jinja2cpp:file", - "value": "src/unicode_printable.h sha256:bb7537033f18a1847f7cdb89bb19bcdee61f8afe6fd0d24891ab6967bccb38dc" + "value": "src/unicode_printable.h sha256:7cc795a76469cb82072a59c4f19bf92c231668a28497236e9f90f68eb7887f4d" }, { "name": "jinja2cpp:file", - "value": "src/unicode_tables.h sha256:5d3160113753deb6394ff66d867522a1c0303da0bfcc83bafaa027cfbbe3b96b" + "value": "src/unicode_tables.h sha256:67feccba784328f3fa4f8ea65d5eb03ab0a118532df178c209777a72d38437c0" }, { "name": "jinja2cpp:file", - "value": "src/unicode_case.h sha256:5e7881179bb8cedcc76abef282540138f317cda02286077ced320d06640ce3c3" + "value": "src/unicode_case.h sha256:6e6bc5f2a2b505fa82477a2f88c0f8a77e2d2adc2e7e136e869bae9a15fe2243" }, { "name": "jinja2cpp:generator", @@ -1265,7 +1265,7 @@ "hashes": [ { "alg": "SHA-256", - "content": "83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e" + "content": "faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6" } ], "licenses": [ @@ -1317,7 +1317,7 @@ }, { "name": "jinja2cpp:file", - "value": "src/html_entities.h sha256:83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e" + "value": "src/html_entities.h sha256:faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6" }, { "name": "jinja2cpp:generator", @@ -2093,6 +2093,62 @@ } ] }, + { + "type": "application", + "bom-ref": "libabigail", + "name": "libabigail (abigail-tools)", + "version": "2.4", + "supplier": { + "name": "Red Hat / sourceware.org" + }, + "scope": "excluded", + "licenses": [ + { + "expression": "Apache-2.0 WITH LLVM-exception" + } + ], + "purl": "pkg:generic/libabigail@2.4", + "externalReferences": [ + { + "type": "website", + "url": "https://sourceware.org/libabigail/" + } + ], + "properties": [ + { + "name": "jinja2cpp:role", + "value": "tooling" + }, + { + "name": "jinja2cpp:phases", + "value": "ci" + }, + { + "name": "jinja2cpp:exposure", + "value": "none" + }, + { + "name": "jinja2cpp:delivery", + "value": "system" + }, + { + "name": "jinja2cpp:selected-by", + "value": "linux-build CI abi rows (apt abigail-tools, Ubuntu 24.04)" + }, + { + "name": "jinja2cpp:used-for", + "value": "ABI dump and comparison of the shared library (abidw, abidiff)" + }, + { + "name": "jinja2cpp:why", + "value": "SOVERSION policy gate (docs/tasks/0076)" + }, + { + "name": "jinja2cpp:lighten", + "value": "Not part of any build" + } + ] + }, { "type": "application", "bom-ref": "cmake", @@ -2710,6 +2766,7 @@ "clang-tidy", "pyyaml", "clang-format", + "libabigail", "cmake", "conan", "lars-bilke-codecoverage", diff --git a/sbom/jinja2cpp-full.spdx.json b/sbom/jinja2cpp-full.spdx.json index 1af5aa90a..83ec1c886 100644 --- a/sbom/jinja2cpp-full.spdx.json +++ b/sbom/jinja2cpp-full.spdx.json @@ -427,7 +427,7 @@ } ], "primaryPackagePurpose": "SOURCE", - "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:b00dba6183edea9532030a373956ecab3638b6c5cfe61f40cbe862fba701dce3; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:eb0e46a2c95e693be7a192c1a992ece6e6fb900235ac44620f220bd646b2e372" + "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" }, { "SPDXID": "SPDXRef-Package-unicode-data", @@ -448,7 +448,7 @@ } ], "primaryPackagePurpose": "FILE", - "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Code-point tables for str.isprintable/isalpha/..., upper/lower/title/casefold, int() digit handling; why: Python-identical string predicates and case mapping without ICU; lighten: Data only, no dependency at build time (Python is needed only to regenerate). Acquisition decision is still open in docs/tasks/0187 and 0186; file: src/unicode_printable.h sha256:bb7537033f18a1847f7cdb89bb19bcdee61f8afe6fd0d24891ab6967bccb38dc; file: src/unicode_tables.h sha256:5d3160113753deb6394ff66d867522a1c0303da0bfcc83bafaa027cfbbe3b96b; file: src/unicode_case.h sha256:5e7881179bb8cedcc76abef282540138f317cda02286077ced320d06640ce3c3; generator: scripts/gen_unicode_printable.py; generator: scripts/gen_unicode_tables.py; generator: scripts/gen_unicode_case.py" + "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Code-point tables for str.isprintable/isalpha/..., upper/lower/title/casefold, int() digit handling; why: Python-identical string predicates and case mapping without ICU; lighten: Data only, no dependency at build time (Python is needed only to regenerate). Acquisition decision is still open in docs/tasks/0187 and 0186; file: src/unicode_printable.h sha256:7cc795a76469cb82072a59c4f19bf92c231668a28497236e9f90f68eb7887f4d; file: src/unicode_tables.h sha256:67feccba784328f3fa4f8ea65d5eb03ab0a118532df178c209777a72d38437c0; file: src/unicode_case.h sha256:6e6bc5f2a2b505fa82477a2f88c0f8a77e2d2adc2e7e136e869bae9a15fe2243; generator: scripts/gen_unicode_printable.py; generator: scripts/gen_unicode_tables.py; generator: scripts/gen_unicode_case.py" }, { "SPDXID": "SPDXRef-Package-html-entities", @@ -460,7 +460,7 @@ "checksums": [ { "algorithm": "SHA256", - "checksumValue": "83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e" + "checksumValue": "faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6" } ], "homepage": "https://docs.python.org/3.12/library/html.entities.html", @@ -475,7 +475,7 @@ } ], "primaryPackagePurpose": "FILE", - "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Entity table for MarkupSafe-compatible unescape/striptags; why: Same entity set as Python's html.unescape; lighten: Data only; keep; file: src/html_entities.h sha256:83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e; generator: scripts/gen_html_entities.py" + "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Entity table for MarkupSafe-compatible unescape/striptags; why: Same entity set as Python's html.unescape; lighten: Data only; keep; file: src/html_entities.h sha256:faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6; generator: scripts/gen_html_entities.py" }, { "SPDXID": "SPDXRef-Package-googletest", @@ -748,6 +748,27 @@ "primaryPackagePurpose": "APPLICATION", "comment": "role: tooling; phases: dev; exposure: none; delivery: system; selected-by: format-check CI job (apt clang-format-18); used-for: Formatting gate; why: Code style; lighten: Not part of any build" }, + { + "SPDXID": "SPDXRef-Package-libabigail", + "name": "libabigail (abigail-tools)", + "versionInfo": "2.4", + "supplier": "Organization: Red Hat / sourceware.org", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "homepage": "https://sourceware.org/libabigail/", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "Apache-2.0 WITH LLVM-exception", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:generic/libabigail@2.4" + } + ], + "primaryPackagePurpose": "APPLICATION", + "comment": "role: tooling; phases: ci; exposure: none; delivery: system; selected-by: linux-build CI abi rows (apt abigail-tools, Ubuntu 24.04); used-for: ABI dump and comparison of the shared library (abidw, abidiff); why: SOVERSION policy gate (docs/tasks/0076); lighten: Not part of any build" + }, { "SPDXID": "SPDXRef-Package-cmake", "name": "CMake", @@ -1130,6 +1151,11 @@ "relationshipType": "DEV_TOOL_OF", "relatedSpdxElement": "SPDXRef-Package-jinja2cpp" }, + { + "spdxElementId": "SPDXRef-Package-libabigail", + "relationshipType": "DEV_TOOL_OF", + "relatedSpdxElement": "SPDXRef-Package-jinja2cpp" + }, { "spdxElementId": "SPDXRef-Package-cmake", "relationshipType": "BUILD_TOOL_OF", @@ -1181,5 +1207,5 @@ "relatedSpdxElement": "SPDXRef-Package-jinja2cpp" } ], - "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-full-6aee0b571f4d138f" + "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-full-5176e9284b3785f5" } diff --git a/sbom/jinja2cpp.cdx.json b/sbom/jinja2cpp.cdx.json index 258ac8517..9e0d616db 100644 --- a/sbom/jinja2cpp.cdx.json +++ b/sbom/jinja2cpp.cdx.json @@ -1,7 +1,7 @@ { "bomFormat": "CycloneDX", "specVersion": "1.6", - "serialNumber": "urn:uuid:11349e2e-8f15-5505-bdfe-c51fdcc762fb", + "serialNumber": "urn:uuid:96e351fa-fe32-5440-b764-2411a152ef20", "version": 1, "metadata": { "timestamp": "2026-10-18T00:00:00Z", @@ -1163,11 +1163,11 @@ }, { "name": "jinja2cpp:file", - "value": "include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:b00dba6183edea9532030a373956ecab3638b6c5cfe61f40cbe862fba701dce3" + "value": "include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a" }, { "name": "jinja2cpp:file", - "value": "include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:eb0e46a2c95e693be7a192c1a992ece6e6fb900235ac44620f220bd646b2e372" + "value": "include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" } ] }, @@ -1229,15 +1229,15 @@ }, { "name": "jinja2cpp:file", - "value": "src/unicode_printable.h sha256:bb7537033f18a1847f7cdb89bb19bcdee61f8afe6fd0d24891ab6967bccb38dc" + "value": "src/unicode_printable.h sha256:7cc795a76469cb82072a59c4f19bf92c231668a28497236e9f90f68eb7887f4d" }, { "name": "jinja2cpp:file", - "value": "src/unicode_tables.h sha256:5d3160113753deb6394ff66d867522a1c0303da0bfcc83bafaa027cfbbe3b96b" + "value": "src/unicode_tables.h sha256:67feccba784328f3fa4f8ea65d5eb03ab0a118532df178c209777a72d38437c0" }, { "name": "jinja2cpp:file", - "value": "src/unicode_case.h sha256:5e7881179bb8cedcc76abef282540138f317cda02286077ced320d06640ce3c3" + "value": "src/unicode_case.h sha256:6e6bc5f2a2b505fa82477a2f88c0f8a77e2d2adc2e7e136e869bae9a15fe2243" }, { "name": "jinja2cpp:generator", @@ -1265,7 +1265,7 @@ "hashes": [ { "alg": "SHA-256", - "content": "83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e" + "content": "faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6" } ], "licenses": [ @@ -1317,7 +1317,7 @@ }, { "name": "jinja2cpp:file", - "value": "src/html_entities.h sha256:83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e" + "value": "src/html_entities.h sha256:faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6" }, { "name": "jinja2cpp:generator", diff --git a/sbom/jinja2cpp.spdx.json b/sbom/jinja2cpp.spdx.json index 25e53833f..405f03ff0 100644 --- a/sbom/jinja2cpp.spdx.json +++ b/sbom/jinja2cpp.spdx.json @@ -427,7 +427,7 @@ } ], "primaryPackagePurpose": "SOURCE", - "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:b00dba6183edea9532030a373956ecab3638b6c5cfe61f40cbe862fba701dce3; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:eb0e46a2c95e693be7a192c1a992ece6e6fb900235ac44620f220bd646b2e372" + "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" }, { "SPDXID": "SPDXRef-Package-unicode-data", @@ -448,7 +448,7 @@ } ], "primaryPackagePurpose": "FILE", - "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Code-point tables for str.isprintable/isalpha/..., upper/lower/title/casefold, int() digit handling; why: Python-identical string predicates and case mapping without ICU; lighten: Data only, no dependency at build time (Python is needed only to regenerate). Acquisition decision is still open in docs/tasks/0187 and 0186; file: src/unicode_printable.h sha256:bb7537033f18a1847f7cdb89bb19bcdee61f8afe6fd0d24891ab6967bccb38dc; file: src/unicode_tables.h sha256:5d3160113753deb6394ff66d867522a1c0303da0bfcc83bafaa027cfbbe3b96b; file: src/unicode_case.h sha256:5e7881179bb8cedcc76abef282540138f317cda02286077ced320d06640ce3c3; generator: scripts/gen_unicode_printable.py; generator: scripts/gen_unicode_tables.py; generator: scripts/gen_unicode_case.py" + "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Code-point tables for str.isprintable/isalpha/..., upper/lower/title/casefold, int() digit handling; why: Python-identical string predicates and case mapping without ICU; lighten: Data only, no dependency at build time (Python is needed only to regenerate). Acquisition decision is still open in docs/tasks/0187 and 0186; file: src/unicode_printable.h sha256:7cc795a76469cb82072a59c4f19bf92c231668a28497236e9f90f68eb7887f4d; file: src/unicode_tables.h sha256:67feccba784328f3fa4f8ea65d5eb03ab0a118532df178c209777a72d38437c0; file: src/unicode_case.h sha256:6e6bc5f2a2b505fa82477a2f88c0f8a77e2d2adc2e7e136e869bae9a15fe2243; generator: scripts/gen_unicode_printable.py; generator: scripts/gen_unicode_tables.py; generator: scripts/gen_unicode_case.py" }, { "SPDXID": "SPDXRef-Package-html-entities", @@ -460,7 +460,7 @@ "checksums": [ { "algorithm": "SHA256", - "checksumValue": "83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e" + "checksumValue": "faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6" } ], "homepage": "https://docs.python.org/3.12/library/html.entities.html", @@ -475,7 +475,7 @@ } ], "primaryPackagePurpose": "FILE", - "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Entity table for MarkupSafe-compatible unescape/striptags; why: Same entity set as Python's html.unescape; lighten: Data only; keep; file: src/html_entities.h sha256:83c2e14ba7b52dfd5befbba8f3bca85e0f8bbaf5696cc647c8372602ebc5ed1e; generator: scripts/gen_html_entities.py" + "comment": "role: mandatory; phases: build; exposure: internal; delivery: generated; selected-by: always; used-for: Entity table for MarkupSafe-compatible unescape/striptags; why: Same entity set as Python's html.unescape; lighten: Data only; keep; file: src/html_entities.h sha256:faabd9fba5045264f159834f0f6d9eec1a281bb3349c7315231b6ac56d5f4ea6; generator: scripts/gen_html_entities.py" } ], "relationships": [ @@ -580,5 +580,5 @@ "relatedSpdxElement": "SPDXRef-Package-html-entities" } ], - "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-library-5b4dd302002fa2d1" + "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-library-289c5f2348cb91b6" } diff --git a/scripts/abi_report.sh b/scripts/abi_report.sh new file mode 100755 index 000000000..5064717dd --- /dev/null +++ b/scripts/abi_report.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# Compares the library's ABI with the previous one for one C++ standard (docs/tasks/0076). +# scripts/abi_report.sh # after abidw wrote .abi/libjinja2cpp.so.2.cxx.abi +# Run by the abidiff steps of .github/workflows/linux-build.yml. +# +# Until the 2.0 RC no baseline is committed and the dump of the last master build +# (.abi-base/, restored from the Actions cache) is the reference: the report shows what a +# pull request changes and never fails. From the RC on, abi/libjinja2cpp.so..cxx.abi +# is the reference, and an incompatible change (abidiff bit 8) fails unless the library's +# SONAME differs from the baseline's, that is unless SOVERSION was bumped (in which case the +# pull request also commits the new dump as the baseline). +set -uo pipefail + +std=${1:?usage: abi_report.sh } +current=$(ls .abi/libjinja2cpp.so.*.cxx"$std".abi 2>/dev/null | head -n 1) +baseline=$(ls abi/libjinja2cpp.so.*.cxx"$std".abi 2>/dev/null | head -n 1) +mode=gate +if [ -z "$baseline" ]; then + mode=report + baseline=$(ls .abi-base/libjinja2cpp.so.*.cxx"$std".abi 2>/dev/null | head -n 1) +fi +summary=${GITHUB_STEP_SUMMARY:-/dev/null} + +if [ -z "$current" ]; then + echo "::error::no ABI dump for C++$std in .abi/" + exit 1 +fi +if [ -z "$baseline" ]; then + echo "No previous ABI dump for C++$std yet: nothing to compare." | tee -a "$summary" + exit 0 +fi + +abidiff --suppressions abi/suppressions.ini --no-show-locs "$baseline" "$current" > .abi/abidiff.txt 2>&1 +rc=$? +cat .abi/abidiff.txt + +soname() { grep -o -m 1 "soname='[^']*'" "$1" | cut -d"'" -f2; } +verdict="no ABI change" +if (( rc & 3 )); then + verdict="abidiff failed (exit $rc)" +elif (( rc & 8 )); then + verdict="incompatible ABI change" +elif (( rc & 4 )); then + verdict="compatible ABI change (additions)" +fi + +{ + echo "### ABI, C++$std: $verdict" + echo + echo "Compared with \`$baseline\` ($mode mode)." + if (( rc != 0 )); then + echo + echo '
abidiff report' + echo + echo '```' + head -n 400 .abi/abidiff.txt + echo '```' + echo '
' + fi +} >> "$summary" + +if [ "$mode" = report ]; then + if (( rc & 3 )); then + echo "::warning::$verdict; report mode does not fail" + elif (( rc & 8 )); then + echo "::notice::C++$std: $verdict against the last master build (report only until the 2.0 RC)" + fi + exit 0 +fi +if (( rc & 3 )); then + echo "::error::$verdict" + exit 1 +fi +if (( rc & 8 )); then + if [ "$(soname "$baseline")" != "$(soname "$current")" ]; then + echo "::notice::C++$std: $verdict with SONAME $(soname "$current") (was $(soname "$baseline")); commit the new dump as the baseline" + exit 0 + fi + echo "::error::C++$std: $verdict without a SOVERSION bump. Bump SOVERSION in CMakeLists.txt and commit the job's dump (artifact abi-cxx$std) as abi/, or keep the ABI (docs/tasks/0076)." + exit 1 +fi +exit 0 diff --git a/test/abi_symbols.cmake b/test/abi_symbols.cmake new file mode 100644 index 000000000..71bdc935c --- /dev/null +++ b/test/abi_symbols.cmake @@ -0,0 +1,48 @@ +# Fails when the library defines a jinja2 symbol outside the inline ABI namespace (docs/tasks/0076). +# cmake -DNM= -DLIBRARY= -P test/abi_symbols.cmake +# Every mangled jinja2:: name must start jinja2::v2:: (`6jinja22v2`), and the vendored +# polymorphic/indirect types must not be left in a global `xyz`, where a user's own copy of +# the upstream headers would collide with them. scripts/abi_namespace.py keeps the sources +# in that shape; this checks what the compiler actually emitted. +foreach (_var NM LIBRARY) + if (NOT ${_var}) + message(FATAL_ERROR "${_var} is required") + endif () +endforeach () +# Defined symbols only: a static library repeats every reference in each object +execute_process(COMMAND "${NM}" --defined-only "${LIBRARY}" OUTPUT_VARIABLE _symbols ERROR_VARIABLE _err RESULT_VARIABLE _rc) +if (NOT _rc EQUAL 0) + execute_process(COMMAND "${NM}" "${LIBRARY}" OUTPUT_VARIABLE _symbols ERROR_VARIABLE _err RESULT_VARIABLE _rc) +endif () +if (NOT _rc EQUAL 0) + message(FATAL_ERROR "${NM} ${LIBRARY} failed (${_rc}): ${_err}") +endif () +# Mach-O prefixes every symbol with an extra underscore; the match starts after it +string(REGEX MATCHALL "_Z[^ \n]*" _syms "${_symbols}") +set(_tagged ${_syms}) +list(FILTER _tagged INCLUDE REGEX "(^|[^0-9])6jinja22v2") +list(LENGTH _tagged _tagged) +set(_rest ${_syms}) +list(TRANSFORM _rest REPLACE "(^|[^0-9])6jinja22v2" "\\1") +list(FILTER _rest INCLUDE REGEX "(^|[^0-9])6jinja2|^_ZN?K?3xyz") +# report the symbols as nm printed them +set(_bad "") +if (_rest) + foreach (_sym IN LISTS _syms) + string(REGEX REPLACE "(^|[^0-9])6jinja22v2" "\\1" _stripped "${_sym}") + if (_stripped MATCHES "(^|[^0-9])6jinja2|^_ZN?K?3xyz") + list(APPEND _bad "${_sym}") + endif () + endforeach () +endif () +list(REMOVE_DUPLICATES _bad) +list(LENGTH _bad _nbad) +if (_nbad GREATER 0) + list(SUBLIST _bad 0 20 _shown) + string(REPLACE ";" "\n " _shown "${_shown}") + message(FATAL_ERROR "${_nbad} symbols outside jinja2::v2 in ${LIBRARY}, run scripts/abi_namespace.py:\n ${_shown}") +endif () +if (_tagged EQUAL 0) + message(FATAL_ERROR "no jinja2::v2 symbols in ${LIBRARY}: is ${NM} reading the right file?") +endif () +message(STATUS "${_tagged} jinja2::v2 symbols, none outside it") diff --git a/test/package/CMakeLists.txt b/test/package/CMakeLists.txt index 0a78725e6..e455b2538 100644 --- a/test/package/CMakeLists.txt +++ b/test/package/CMakeLists.txt @@ -18,6 +18,21 @@ enable_testing() add_test(NAME consumer COMMAND consumer ${CMAKE_CURRENT_BINARY_DIR}/templates) add_test(NAME consumer_compat COMMAND consumer_compat ${CMAKE_CURRENT_BINARY_DIR}/templates) +# A program built against 1.x headers must fail to link with this library, not misbehave: +# the inline namespace v2 is in every mangled name (docs/tasks/0076). abi_probe_v2 is the +# positive control; abi_probe_v1 is built only by the test, which expects the link error. +add_executable(abi_probe_v2 abi_probe.cpp) +target_compile_definitions(abi_probe_v2 PRIVATE PROBE_V2) +target_link_libraries(abi_probe_v2 PRIVATE jinja2cpp::jinja2cpp) +add_test(NAME abi_probe_v2 COMMAND abi_probe_v2) +add_executable(abi_probe_v1 EXCLUDE_FROM_ALL abi_probe.cpp) +target_link_libraries(abi_probe_v1 PRIVATE jinja2cpp::jinja2cpp) +add_test(NAME abi_probe_v1_does_not_link + COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target abi_probe_v1 --config $) +# GNU ld, lld, Apple ld and MSVC link; a compile error or a successful link does not match +set_tests_properties(abi_probe_v1_does_not_link PROPERTIES PASS_REGULAR_EXPRESSION + "undefined reference to [`']?jinja2::operator==|undefined symbol: jinja2::operator==|Undefined symbols for architecture|unresolved external symbol") + # check_package.cmake points PKG_CONFIG_PATH at the install and turns this on. pkg-config # lists the private dependencies of a static build only in the internal deps mode, where # it knows them. diff --git a/test/package/abi_probe.cpp b/test/package/abi_probe.cpp new file mode 100644 index 000000000..4747abe8b --- /dev/null +++ b/test/package/abi_probe.cpp @@ -0,0 +1,28 @@ +// Negative link check for the inline ABI namespace (docs/tasks/0076). No Jinja2C++ header is +// included: the declaration below is what a program compiled against the 1.x headers refers +// to. Built with PROBE_V2 it names jinja2::v2::operator== and must link and run; without it, +// it names the 1.x jinja2::operator==, which the 2.x library must not define, so the link +// has to fail (test/package/CMakeLists.txt). +#ifdef PROBE_V2 +namespace jinja2 +{ +inline namespace v2 +{ +class Value; +bool operator==(const Value& lhs, const Value& rhs); +} // namespace v2 +} // namespace jinja2 +#else +namespace jinja2 +{ +class Value; +bool operator==(const Value& lhs, const Value& rhs); +} // namespace jinja2 +#endif + +int main() +{ + // volatile keeps the reference to the library symbol in the object file + bool (*volatile compare)(const jinja2::Value&, const jinja2::Value&) = &jinja2::operator==; + return compare ? 0 : 1; +} diff --git a/test/package/check_package.cmake b/test/package/check_package.cmake index 4739b5d07..c3d320c5f 100644 --- a/test/package/check_package.cmake +++ b/test/package/check_package.cmake @@ -51,6 +51,28 @@ foreach (_req "${_major}.0;TRUE" "${_next}.0;FALSE" "${_prev}.0;FALSE") message(FATAL_ERROR "version file: ${PACKAGE_FIND_VERSION} compatible=${PACKAGE_VERSION_COMPATIBLE}, want ${_want}") endif () endforeach () +# SOVERSION is the major version (docs/tasks/0076): the loader refuses a shared library of +# another major, which is what protects shared consumers across an ABI break +if (EXPECT_SHARED AND NOT CMAKE_HOST_WIN32) + if (CMAKE_HOST_APPLE) + set(_soname "libjinja2cpp.${_major}.dylib") + else () + set(_soname "libjinja2cpp.so.${_major}") + endif () + file(GLOB _solib "${PREFIX}/lib/${_soname}" "${PREFIX}/lib64/${_soname}" "${PREFIX}/lib/*/${_soname}") + if (NOT _solib) + file(GLOB_RECURSE _installed RELATIVE "${PREFIX}" "${PREFIX}/*jinja2cpp*") + message(FATAL_ERROR "${_soname} is not installed; found: ${_installed}") + endif () + list(GET _solib 0 _solib) + find_program(_readelf readelf) + if (_readelf AND NOT CMAKE_HOST_APPLE) + execute_process(COMMAND "${_readelf}" -d "${_solib}" OUTPUT_VARIABLE _dynamic) + if (NOT _dynamic MATCHES "SONAME\\)? *Library soname: \\[${_soname}\\]") + message(FATAL_ERROR "SONAME of ${_solib} is not ${_soname}:\n${_dynamic}") + endif () + endif () +endif () set(ENV{PKG_CONFIG_PATH} "") file(GLOB_RECURSE _pcdirs LIST_DIRECTORIES true "${PREFIX}/*/pkgconfig") string(REPLACE ";" ":" _pcpath "${_pcdirs}") diff --git a/test/package/main.cpp b/test/package/main.cpp index 63cac50d2..1a7196d5c 100644 --- a/test/package/main.cpp +++ b/test/package/main.cpp @@ -12,8 +12,9 @@ static_assert(nsel_USES_STD_EXPECTED == 0, "Result must be expected-lite's ow #if defined(EXPECT_SHARED) != defined(JINJA2CPP_LINK_AS_SHARED) #error "JINJA2CPP_LINK_AS_SHARED does not match the installed library's link type" #endif -// The generated version.h is installed with the headers (docs/tasks/0069) -static_assert(JINJA2CPP_VERSION >= 10302, "jinja2cpp/version.h is stale or missing"); +// The generated version.h is installed with the headers (docs/tasks/0069), and its major +// version is the one in the inline ABI namespace v2 (docs/tasks/0076) +static_assert(JINJA2CPP_VERSION_MAJOR == 2, "jinja2cpp/version.h is stale or missing, or the ABI namespace tag needs a new major"); namespace { diff --git a/test/template_api_test.cpp b/test/template_api_test.cpp index 666a2bf9e..af1c4ccb6 100644 --- a/test/template_api_test.cpp +++ b/test/template_api_test.cpp @@ -338,7 +338,8 @@ TEST(TemplateApiTest, ConcurrentRenderOfOneTemplate) TEST(TemplateApiTest, VersionMacrosMatchProjectVersion) { static_assert(JINJA2CPP_VERSION == (JINJA2CPP_VERSION_MAJOR * 10000) + (JINJA2CPP_VERSION_MINOR * 100) + JINJA2CPP_VERSION_PATCH); - static_assert(JINJA2CPP_VERSION >= 10302); + // the inline ABI namespace is v2 (docs/tasks/0076); a new major retags it with scripts/abi_namespace.py + static_assert(JINJA2CPP_VERSION_MAJOR == 2); const auto expected = std::to_string(JINJA2CPP_VERSION_MAJOR) + "." + std::to_string(JINJA2CPP_VERSION_MINOR) + "." + std::to_string(JINJA2CPP_VERSION_PATCH); EXPECT_EQ(expected, JINJA2CPP_VERSION_STRING); } From 577860d33861a092019f127e0f79b9d81e0d7d6b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 01:53:23 +0000 Subject: [PATCH 4/6] 0076 PR-2 verifier fixes: generators emit v2, abidiff gates layout changes - gen_html_entities/unicode_case/unicode_tables/unicode_printable emit the inline namespace v2 form, so their --check matches the wrapped headers - abi_report.sh: any change left after --no-added-syms is a break (abidiff reports a changed layout with bit 4, not 8); artifact uploaded on failure - abi_namespace.py rejects spellings it does not rewrite (indented, one-line) - docs/dependencies.md and SBOM name jinja2::v2::detail::xyz Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Ua8ABM1TRsAvso2sWGHFSw --- .github/workflows/linux-build.yml | 3 ++- docs/dependencies.md | 2 +- docs/tasks/0076-abi-namespace-and-headers.md | 6 ++++- docs/tasks/0285-abi-baselines-at-rc.md | 7 +++++- sbom/components.json | 4 ++-- sbom/jinja2cpp-full.cdx.json | 8 +++---- sbom/jinja2cpp-full.spdx.json | 4 ++-- sbom/jinja2cpp.cdx.json | 8 +++---- sbom/jinja2cpp.spdx.json | 4 ++-- scripts/abi_namespace.py | 6 +++++ scripts/abi_report.sh | 25 +++++++++++++------- scripts/gen_html_entities.py | 4 ++-- scripts/gen_unicode_case.py | 4 ++-- scripts/gen_unicode_printable.py | 4 ++-- scripts/gen_unicode_tables.py | 4 ++-- 15 files changed, 58 insertions(+), 35 deletions(-) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index a75d72278..504d73be8 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -138,7 +138,8 @@ jobs: path: .abi-base key: abi-cxx${{ matrix.std }}-${{ github.sha }} - - if: matrix.abi + # 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 }} diff --git a/docs/dependencies.md b/docs/dependencies.md index fc5f47a53..2252efab7 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -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 | diff --git a/docs/tasks/0076-abi-namespace-and-headers.md b/docs/tasks/0076-abi-namespace-and-headers.md index f0ae981e9..6ad4b1fbf 100644 --- a/docs/tasks/0076-abi-namespace-and-headers.md +++ b/docs/tasks/0076-abi-namespace-and-headers.md @@ -47,4 +47,8 @@ major 2; README "Compatibility and versioning". The abidiff job runs on one shar 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++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. diff --git a/docs/tasks/0285-abi-baselines-at-rc.md b/docs/tasks/0285-abi-baselines-at-rc.md index 82cb6ada8..b842a200d 100644 --- a/docs/tasks/0285-abi-baselines-at-rc.md +++ b/docs/tasks/0285-abi-baselines-at-rc.md @@ -20,7 +20,12 @@ incompatible change unless SOVERSION was bumped. 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. +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. diff --git a/sbom/components.json b/sbom/components.json index c2012833a..1d94569ac 100644 --- a/sbom/components.json +++ b/sbom/components.json @@ -278,12 +278,12 @@ "purl": "pkg:github/jbcoe/value_types@57f2e7a", "delivery": "vendored", "files": ["include/jinja2cpp/polymorphic_value/indirect_cxx14.h", "include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h"], - "modified": "both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T", + "modified": "both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T", "role": "mandatory", "phases": ["build", "consumer-build"], "exposure": "public-api", "selected_by": "always", - "used_for": "polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap", + "used_for": "polymorphic and indirect (jinja2::v2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap", "why": "Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26", "lighten": "Keep (header-only, installed). Switch to std:: at a C++26 floor" }, diff --git a/sbom/jinja2cpp-full.cdx.json b/sbom/jinja2cpp-full.cdx.json index 0af193245..5f9660898 100644 --- a/sbom/jinja2cpp-full.cdx.json +++ b/sbom/jinja2cpp-full.cdx.json @@ -1,7 +1,7 @@ { "bomFormat": "CycloneDX", "specVersion": "1.6", - "serialNumber": "urn:uuid:05ea2675-8382-5679-ad05-ef9a8b7bf331", + "serialNumber": "urn:uuid:a7ddc095-8bb8-5764-b9ea-9b60eb28ef13", "version": 1, "metadata": { "timestamp": "2026-10-18T00:00:00Z", @@ -1122,7 +1122,7 @@ } ], "pedigree": { - "notes": "both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" + "notes": "both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" }, "properties": [ { @@ -1147,7 +1147,7 @@ }, { "name": "jinja2cpp:used-for", - "value": "polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap" + "value": "polymorphic and indirect (jinja2::v2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap" }, { "name": "jinja2cpp:why", @@ -1159,7 +1159,7 @@ }, { "name": "jinja2cpp:modified", - "value": "both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" + "value": "both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" }, { "name": "jinja2cpp:file", diff --git a/sbom/jinja2cpp-full.spdx.json b/sbom/jinja2cpp-full.spdx.json index 83ec1c886..9bf8e4c0c 100644 --- a/sbom/jinja2cpp-full.spdx.json +++ b/sbom/jinja2cpp-full.spdx.json @@ -427,7 +427,7 @@ } ], "primaryPackagePurpose": "SOURCE", - "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" + "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::v2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" }, { "SPDXID": "SPDXRef-Package-unicode-data", @@ -1207,5 +1207,5 @@ "relatedSpdxElement": "SPDXRef-Package-jinja2cpp" } ], - "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-full-5176e9284b3785f5" + "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-full-45343781f625a58f" } diff --git a/sbom/jinja2cpp.cdx.json b/sbom/jinja2cpp.cdx.json index 9e0d616db..e35ee8cd0 100644 --- a/sbom/jinja2cpp.cdx.json +++ b/sbom/jinja2cpp.cdx.json @@ -1,7 +1,7 @@ { "bomFormat": "CycloneDX", "specVersion": "1.6", - "serialNumber": "urn:uuid:96e351fa-fe32-5440-b764-2411a152ef20", + "serialNumber": "urn:uuid:6517a686-4ed3-5d9a-9ee7-94f64aa14c22", "version": 1, "metadata": { "timestamp": "2026-10-18T00:00:00Z", @@ -1122,7 +1122,7 @@ } ], "pedigree": { - "notes": "both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" + "notes": "both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" }, "properties": [ { @@ -1147,7 +1147,7 @@ }, { "name": "jinja2cpp:used-for", - "value": "polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap" + "value": "polymorphic and indirect (jinja2::v2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap" }, { "name": "jinja2cpp:why", @@ -1159,7 +1159,7 @@ }, { "name": "jinja2cpp:modified", - "value": "both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" + "value": "both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T" }, { "name": "jinja2cpp:file", diff --git a/sbom/jinja2cpp.spdx.json b/sbom/jinja2cpp.spdx.json index 405f03ff0..fe2683991 100644 --- a/sbom/jinja2cpp.spdx.json +++ b/sbom/jinja2cpp.spdx.json @@ -427,7 +427,7 @@ } ], "primaryPackagePurpose": "SOURCE", - "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" + "comment": "role: mandatory; phases: build,consumer-build; exposure: public-api; delivery: vendored; selected-by: always; used-for: polymorphic and indirect (jinja2::v2::detail::xyz) behind jinja2::types and jinja2::detail (value_ptr.h), used by Value, GenericList and GenericMap; why: Value-semantic owning pointers for the public value model; standardised as std::indirect/std::polymorphic in C++26; lighten: Keep (header-only, installed). Switch to std:: at a C++26 floor; modified: both at upstream 57f2e7a, moved to namespace jinja2::v2::detail::xyz with JINJA2CPP_XYZ_ macros (0076); indirect: Clang incomplete-type fix (0edc34d), tag tested first (0072); polymorphic: default constructor constrained on the allocator for MSVC std::variant (0071), cb_ initialised (0089), in-place initializer_list constructor creates U, not T; file: include/jinja2cpp/polymorphic_value/indirect_cxx14.h sha256:14f328d0de7e26f047178a58d443b0316a2ecd69e1ec05007d6a3aeb23290d6a; file: include/jinja2cpp/polymorphic_value/polymorphic_cxx14.h sha256:3d46bb50b19a3f22284aa89866a9a521325456bf3e00f7245a42852ffb9a4db1" }, { "SPDXID": "SPDXRef-Package-unicode-data", @@ -580,5 +580,5 @@ "relatedSpdxElement": "SPDXRef-Package-html-entities" } ], - "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-library-289c5f2348cb91b6" + "documentNamespace": "https://github.com/jinja2cpp/Jinja2Cpp/sbom/jinja2cpp-2.0.0-library-751d894c115a1ae6" } diff --git a/scripts/abi_namespace.py b/scripts/abi_namespace.py index 9b6e3652b..b400f1c30 100755 --- a/scripts/abi_namespace.py +++ b/scripts/abi_namespace.py @@ -36,6 +36,7 @@ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOTS = ("include", "src") OPENING = re.compile(r"^namespace jinja2(?P(?:::\w+)*)(?P\s*\{)?\s*$") +ANY_OPENING = re.compile(r"(?v\d+)(?P\s*\{)?\s*$") CLOSE_COMMENT = re.compile(r"^\}(?P\s*)//\s*namespace\s+(?P[\w:]+)\s*$") @@ -162,6 +163,11 @@ def rewrite(text): edit = (open_line + 1, open_line + 1, insert, close_line, ["}" + space + f"// namespace {TAG}", close_text]) break if edit is None: + # Any other spelling (indented, one-line `namespace jinja2 { ... }`) would stay + # outside v2 unnoticed: a free function in it becomes a second overload + for li, code in enumerate(mask_lines): + if ANY_OPENING.search(code) and not OPENING.match(code.rstrip()): + raise ValueError(f"line {li + 1}: write `namespace jinja2` alone at the start of a line") return "\n".join(lines) if edit == (): continue diff --git a/scripts/abi_report.sh b/scripts/abi_report.sh index 5064717dd..53569c6dc 100755 --- a/scripts/abi_report.sh +++ b/scripts/abi_report.sh @@ -6,9 +6,11 @@ # Until the 2.0 RC no baseline is committed and the dump of the last master build # (.abi-base/, restored from the Actions cache) is the reference: the report shows what a # pull request changes and never fails. From the RC on, abi/libjinja2cpp.so..cxx.abi -# is the reference, and an incompatible change (abidiff bit 8) fails unless the library's +# is the reference, and an incompatible change fails unless the library's # SONAME differs from the baseline's, that is unless SOVERSION was bumped (in which case the -# pull request also commits the new dump as the baseline). +# pull request also replaces the baseline with the new dump). An incompatible change is any +# change left once added symbols are ignored: removed or changed functions and variables, +# and changed layouts of the types they use. set -uo pipefail std=${1:?usage: abi_report.sh } @@ -33,12 +35,17 @@ fi abidiff --suppressions abi/suppressions.ini --no-show-locs "$baseline" "$current" > .abi/abidiff.txt 2>&1 rc=$? cat .abi/abidiff.txt +# Without the added symbols, any remaining change is a break: abidiff sets bit 8 only for +# removed symbols and vtable changes, and reports a changed layout (a new Settings field, a +# Value alternative, a widened member) with bit 4 alone +abidiff --suppressions abi/suppressions.ini --no-show-locs --no-added-syms "$baseline" "$current" > /dev/null 2>&1 +break_rc=$? soname() { grep -o -m 1 "soname='[^']*'" "$1" | cut -d"'" -f2; } verdict="no ABI change" -if (( rc & 3 )); then - verdict="abidiff failed (exit $rc)" -elif (( rc & 8 )); then +if (( (rc | break_rc) & 3 )); then + verdict="abidiff failed (exit $rc, $break_rc)" +elif (( break_rc & 12 )); then verdict="incompatible ABI change" elif (( rc & 4 )); then verdict="compatible ABI change (additions)" @@ -60,18 +67,18 @@ fi } >> "$summary" if [ "$mode" = report ]; then - if (( rc & 3 )); then + if (( (rc | break_rc) & 3 )); then echo "::warning::$verdict; report mode does not fail" - elif (( rc & 8 )); then + elif (( break_rc & 12 )); then echo "::notice::C++$std: $verdict against the last master build (report only until the 2.0 RC)" fi exit 0 fi -if (( rc & 3 )); then +if (( (rc | break_rc) & 3 )); then echo "::error::$verdict" exit 1 fi -if (( rc & 8 )); then +if (( break_rc & 12 )); then if [ "$(soname "$baseline")" != "$(soname "$current")" ]; then echo "::notice::C++$std: $verdict with SONAME $(soname "$current") (was $(soname "$baseline")); commit the new dump as the baseline" exit 0 diff --git a/scripts/gen_html_entities.py b/scripts/gen_html_entities.py index 713b747d0..8a5149bce 100644 --- a/scripts/gen_html_entities.py +++ b/scripts/gen_html_entities.py @@ -32,12 +32,12 @@ def main(): "// Generated by scripts/gen_html_entities.py from Python {}.{}'s html.entities.html5; do not edit.\n".format(*sys.version_info[:2]), "#ifndef JINJA2CPP_SRC_HTML_ENTITIES_H\n#define JINJA2CPP_SRC_HTML_ENTITIES_H\n", "#include \n#include \n#include \n#include \n", - "namespace jinja2::html\n{\n", + "namespace jinja2\n{\ninline namespace v2\n{\nnamespace html\n{\n", "// A named character reference and the one or two code points it stands for (0 when only one)\n" "struct HtmlEntity\n{\n const char* name;\n uint32_t first;\n uint32_t second;\n};\n", "// Sorted by name\nconstexpr HtmlEntity HtmlEntities[] = {\n" + "\n".join(rows) + "\n};\n", HELPER, - "} // namespace jinja2::html\n\n#endif // JINJA2CPP_SRC_HTML_ENTITIES_H\n", + "} // namespace html\n} // namespace v2\n} // namespace jinja2\n\n#endif // JINJA2CPP_SRC_HTML_ENTITIES_H\n", ] target = ROOT / "src" / "html_entities.h" text = "\n".join(parts) diff --git a/scripts/gen_unicode_case.py b/scripts/gen_unicode_case.py index 6cd74f592..572af3e6e 100644 --- a/scripts/gen_unicode_case.py +++ b/scripts/gen_unicode_case.py @@ -141,7 +141,7 @@ def main(): "// Generated by scripts/gen_unicode_case.py from Unicode {}; do not edit.\n".format(unicodedata.unidata_version), "#ifndef JINJA2CPP_SRC_UNICODE_CASE_H\n#define JINJA2CPP_SRC_UNICODE_CASE_H\n", '#include "unicode_tables.h"\n\n#include \n#include \n#include \n#include \n', - "namespace jinja2::unicode\n{\n", + "namespace jinja2\n{\ninline namespace v2\n{\nnamespace unicode\n{\n", "// cp + delta for every stride-th code point from first to last\n" "struct CaseRun\n{\n uint32_t first;\n uint32_t last;\n uint32_t stride;\n int32_t delta;\n};\n", "// A mapping to two or three code points; unused entries are 0\n" @@ -154,7 +154,7 @@ def main(): parts.append(range_table("Cased", "Cased and not Case_Ignorable", ranges(cased))) parts.append(range_table("CaseIgnorable", "Case_Ignorable (cased or not)", ranges(case_ignorable))) parts.append(HELPERS) - parts.append("} // namespace jinja2::unicode\n\n#endif // JINJA2CPP_SRC_UNICODE_CASE_H\n") + parts.append("} // namespace unicode\n} // namespace v2\n} // namespace jinja2\n\n#endif // JINJA2CPP_SRC_UNICODE_CASE_H\n") target = ROOT / "src" / "unicode_case.h" text = "\n".join(parts) # --check: CI (parity-expectations.yml) fails when the committed header is stale diff --git a/scripts/gen_unicode_printable.py b/scripts/gen_unicode_printable.py index 377c35d0e..6a9810d48 100644 --- a/scripts/gen_unicode_printable.py +++ b/scripts/gen_unicode_printable.py @@ -38,7 +38,7 @@ def generate(out): out.write("// Do not edit by hand.\n") out.write("#ifndef JINJA2CPP_SRC_UNICODE_PRINTABLE_H\n#define JINJA2CPP_SRC_UNICODE_PRINTABLE_H\n\n") out.write("#include \n#include \n#include \n\n") - out.write("namespace jinja2::detail\n{\n\n") + out.write("namespace jinja2\n{\ninline namespace v2\n{\nnamespace detail\n{\n\n") out.write("// True when Python's str.isprintable() holds for the code point, so repr() keeps it as is\n") out.write("inline bool IsPythonPrintable(uint32_t cp)\n{\n") out.write(" if (cp >= 0x20 && cp < 0x7f)\n {\n return true;\n }\n\n") @@ -51,7 +51,7 @@ def generate(out): " [](uint32_t val, const Range& r) { return val < r.first; });\n") out.write(" if (it == std::begin(nonPrintable))\n {\n return true;\n }\n") out.write(" --it;\n return cp > it->last;\n}\n\n") - out.write("} // namespace jinja2::detail\n\n#endif // JINJA2CPP_SRC_UNICODE_PRINTABLE_H\n") + out.write("} // namespace detail\n} // namespace v2\n} // namespace jinja2\n\n#endif // JINJA2CPP_SRC_UNICODE_PRINTABLE_H\n") def main(): diff --git a/scripts/gen_unicode_tables.py b/scripts/gen_unicode_tables.py index 9ca02b799..dd71049f9 100755 --- a/scripts/gen_unicode_tables.py +++ b/scripts/gen_unicode_tables.py @@ -37,7 +37,7 @@ def main(): "// Generated by scripts/gen_unicode_tables.py from Unicode {}; do not edit.\n".format(unicodedata.unidata_version), "#ifndef JINJA2CPP_SRC_UNICODE_TABLES_H\n#define JINJA2CPP_SRC_UNICODE_TABLES_H\n", "#include \n#include \n#include \n", - "namespace jinja2\n{\nnamespace unicode\n{\n", + "namespace jinja2\n{\ninline namespace v2\n{\nnamespace unicode\n{\n", "struct CodePointRange\n{\n uint32_t first;\n uint32_t last;\n};\n", table("WordRanges", "Python \\w above ASCII: str.isalnum()", ranges(lambda c: c.isalnum())), table("DecimalRanges", "Python \\d above ASCII: str.isdecimal()", ranges(lambda c: c.isdecimal())), @@ -73,7 +73,7 @@ def main(): return InRanges(SpaceRanges, cp); } """, - "} // namespace unicode\n} // namespace jinja2\n\n#endif // JINJA2CPP_SRC_UNICODE_TABLES_H\n", + "} // namespace unicode\n} // namespace v2\n} // namespace jinja2\n\n#endif // JINJA2CPP_SRC_UNICODE_TABLES_H\n", ] target = ROOT / "src" / "unicode_tables.h" text = "\n".join(parts) From 1c84b543d3d8af9003856ce84e40533e38c17ca8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 01:59:02 +0000 Subject: [PATCH 5/6] Tests: define detail specialisations by qualified name for MSVC (0076) MSVC does not implement CWG 2061: reopening namespace detail inside namespace jinja2 declares a new jinja2::detail beside jinja2::v2::detail. containers_api_test and node_arena_test now use the qualified form, and the README tells 1.x detail::Reflector users to do the same. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Ua8ABM1TRsAvso2sWGHFSw --- README.md | 7 ++++++- test/containers_api_test.cpp | 16 ++++++++-------- test/node_arena_test.cpp | 9 ++++----- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 716818956..037989d0d 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,12 @@ it in the default internal dependencies mode). The un-namespaced `jinja2cpp` tar `namespace jinja2 { class Value; }` declares a different, undefined class and makes later uses ambiguous. `` 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. + one of them. Specialise the extension points inside `namespace jinja2 { ... }` or with their + qualified name (`template<> struct jinja2::Reflector`). A 1.x-style + `detail::Reflector` specialisation must use the qualified form + (`template<> struct jinja2::detail::Reflector`): 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 diff --git a/test/containers_api_test.cpp b/test/containers_api_test.cpp index 5f3dda65c..46f24ad62 100644 --- a/test/containers_api_test.cpp +++ b/test/containers_api_test.cpp @@ -166,11 +166,13 @@ struct Reflector static Value CreateFromPtr(const Celsius* val) { return Create(*val); } }; -namespace detail -{ -// A 1.x-style specialisation keeps working +} // namespace jinja2 + +// A 1.x-style specialisation keeps working. It is written with the qualified name: MSVC does +// not implement CWG 2061, so `namespace jinja2 { namespace detail {` there declares a new +// jinja2::detail beside jinja2::v2::detail instead of reopening it (docs/tasks/0076, README) template<> -struct Reflector +struct jinja2::detail::Reflector { static Value Create(const LegacyReflected& val) { return Value(static_cast(val.value * 10)); } static Value CreateFromPtr(const LegacyReflected* val) { return Create(*val); } @@ -178,20 +180,18 @@ struct Reflector // 1.x code could delegate to the library's reflectors, and override one for a type the library also covers template<> -struct Reflector +struct jinja2::detail::Reflector { static Value Create(const LegacyDelegating& val) { return Reflector::Create(val.text + "!"); } static Value CreateFromPtr(const LegacyDelegating* val) { return Create(*val); } }; template<> -struct Reflector +struct jinja2::detail::Reflector { static Value Create(char16_t val) { return Value(std::string(1, static_cast(val))); } static Value CreateFromPtr(const char16_t* val) { return Create(*val); } }; -} // namespace detail -} // namespace jinja2 TEST(ContainersApiTest, ErrorCodeValuesAreStable) { diff --git a/test/node_arena_test.cpp b/test/node_arena_test.cpp index b204d31b9..9ec3f8cff 100644 --- a/test/node_arena_test.cpp +++ b/test/node_arena_test.cpp @@ -31,10 +31,10 @@ using namespace jinja2; -namespace jinja2::detail -{ -// Handles from raw offsets and the bytes of a sealed tree, to corrupt a tree on purpose -struct ArenaTestAccess +// Handles from raw offsets and the bytes of a sealed tree, to corrupt a tree on purpose. +// Defined through its qualified name: reopening `namespace jinja2::detail` here would make +// MSVC declare a new jinja2::detail beside jinja2::v2::detail (docs/tasks/0076) +struct jinja2::detail::ArenaTestAccess { template static NodeRef Ref(std::uint32_t offset) @@ -65,7 +65,6 @@ struct ArenaTestAccess return header.objects; } }; -} // namespace jinja2::detail namespace { From ede670728577f5b2a7dd4bb923b0307bb3a9fc24 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Oct 2026 03:46:52 +0000 Subject: [PATCH 6/6] 0076 PR-2 Codex fixes: SONAME from SOVERSION, baseline must match SONAME - check_package.cmake reads SOVERSION from CMakeLists.txt instead of using the project major, so a within-2.x SOVERSION bump installs and checks cleanly - abi_report.sh gate fails while the committed baseline's SONAME differs from the library's, so a SOVERSION bump cannot land without its new baseline Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Ua8ABM1TRsAvso2sWGHFSw --- scripts/abi_report.sh | 23 +++++++++++++---------- test/package/check_package.cmake | 14 ++++++++++---- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/scripts/abi_report.sh b/scripts/abi_report.sh index 53569c6dc..1ea53fa57 100755 --- a/scripts/abi_report.sh +++ b/scripts/abi_report.sh @@ -6,11 +6,12 @@ # Until the 2.0 RC no baseline is committed and the dump of the last master build # (.abi-base/, restored from the Actions cache) is the reference: the report shows what a # pull request changes and never fails. From the RC on, abi/libjinja2cpp.so..cxx.abi -# is the reference, and an incompatible change fails unless the library's -# SONAME differs from the baseline's, that is unless SOVERSION was bumped (in which case the -# pull request also replaces the baseline with the new dump). An incompatible change is any -# change left once added symbols are ignored: removed or changed functions and variables, -# and changed layouts of the types they use. +# is the reference and the gate fails +# - on an incompatible change: any change left once added symbols are ignored (removed or +# changed functions and variables, changed layouts of the types they use). The way out is +# to bump SOVERSION and replace the baseline with the new dump in the same pull request; +# - when the baseline's SONAME differs from the library's, so that a SOVERSION bump cannot +# land without its baseline. set -uo pipefail std=${1:?usage: abi_report.sh } @@ -78,12 +79,14 @@ if (( (rc | break_rc) & 3 )); then echo "::error::$verdict" exit 1 fi +# A SOVERSION bump is only complete with its baseline: until the committed dump carries the +# new SONAME, every later break would pass as "SONAME differs" +if [ "$(soname "$baseline")" != "$(soname "$current")" ]; then + echo "::error::C++$std: the library's SONAME is $(soname "$current") but $baseline is for $(soname "$baseline"). Replace the baseline with the job's dump (artifact abi-cxx$std) in the same pull request as the SOVERSION bump (docs/tasks/0076)." + exit 1 +fi if (( break_rc & 12 )); then - if [ "$(soname "$baseline")" != "$(soname "$current")" ]; then - echo "::notice::C++$std: $verdict with SONAME $(soname "$current") (was $(soname "$baseline")); commit the new dump as the baseline" - exit 0 - fi - echo "::error::C++$std: $verdict without a SOVERSION bump. Bump SOVERSION in CMakeLists.txt and commit the job's dump (artifact abi-cxx$std) as abi/, or keep the ABI (docs/tasks/0076)." + echo "::error::C++$std: $verdict without a SOVERSION bump. Bump SOVERSION in CMakeLists.txt and replace abi/ with the job's dump (artifact abi-cxx$std), or keep the ABI (docs/tasks/0076)." exit 1 fi exit 0 diff --git a/test/package/check_package.cmake b/test/package/check_package.cmake index c3d320c5f..5b2467329 100644 --- a/test/package/check_package.cmake +++ b/test/package/check_package.cmake @@ -51,13 +51,19 @@ foreach (_req "${_major}.0;TRUE" "${_next}.0;FALSE" "${_prev}.0;FALSE") message(FATAL_ERROR "version file: ${PACKAGE_FIND_VERSION} compatible=${PACKAGE_VERSION_COMPATIBLE}, want ${_want}") endif () endforeach () -# SOVERSION is the major version (docs/tasks/0076): the loader refuses a shared library of -# another major, which is what protects shared consumers across an ABI break +# The installed SONAME carries SOVERSION (docs/tasks/0076): the loader refuses a shared +# library of another ABI version, which is what protects shared consumers. SOVERSION tracks +# the ABI, not the major, so it is read from CMakeLists.txt rather than derived from _major. if (EXPECT_SHARED AND NOT CMAKE_HOST_WIN32) + file(STRINGS "${_src}/../../CMakeLists.txt" _soversion REGEX "^[ \t]*SOVERSION[ \t]+[0-9]+") + string(REGEX MATCH "[0-9]+" _soversion "${_soversion}") + if (NOT _soversion) + message(FATAL_ERROR "no SOVERSION found in ${_src}/../../CMakeLists.txt") + endif () if (CMAKE_HOST_APPLE) - set(_soname "libjinja2cpp.${_major}.dylib") + set(_soname "libjinja2cpp.${_soversion}.dylib") else () - set(_soname "libjinja2cpp.so.${_major}") + set(_soname "libjinja2cpp.so.${_soversion}") endif () file(GLOB _solib "${PREFIX}/lib/${_soname}" "${PREFIX}/lib64/${_soname}" "${PREFIX}/lib/*/${_soname}") if (NOT _solib)