summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml45
-rw-r--r--README.md5
-rw-r--r--doc/.clang-tidy11
-rw-r--r--doc/Doxyfile.in72
-rw-r--r--doc/api/meson.build9
-rw-r--r--doc/conf.py95
-rw-r--r--doc/index.rst15
-rw-r--r--doc/meson.build78
-rw-r--r--doc/overview_code.c51
-rw-r--r--doc/string_views.rst56
-rw-r--r--doc/summary.rst5
-rw-r--r--doc/using_zix.rst12
-rw-r--r--doc/xml/meson.build19
-rw-r--r--meson.build9
-rw-r--r--meson_options.txt3
-rwxr-xr-xscripts/dox_to_sphinx.py698
-rw-r--r--scripts/meson.build14
17 files changed, 1165 insertions, 32 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1c7d73e..0f3c41b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -4,58 +4,59 @@
arm32_dbg:
image: lv2plugin/debian-arm32
script:
- - meson setup build --cross-file=/usr/share/meson/cross/arm-linux-gnueabihf.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/arm-linux-gnueabihf.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
arm32_rel:
image: lv2plugin/debian-arm32
script:
- - meson setup build --cross-file=/usr/share/meson/cross/arm-linux-gnueabihf.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/arm-linux-gnueabihf.ini -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
arm64_dbg:
image: lv2plugin/debian-arm64
script:
- - meson setup build --cross-file=/usr/share/meson/cross/aarch64-linux-gnu.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/aarch64-linux-gnu.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
arm64_rel:
image: lv2plugin/debian-arm64
script:
- - meson setup build --cross-file=/usr/share/meson/cross/aarch64-linux-gnu.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/aarch64-linux-gnu.ini -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
x64_dbg:
image: lv2plugin/debian-x64
script:
- - meson setup build -Dbuildtype=debug -Dstrict=true -Dwerror=true -Db_coverage=true
+ - meson setup build -Dbuildtype=debug -Dstrict=true -Dwerror=true -Db_coverage=true -Ddocs=enabled
- ninja -C build test
- ninja -C build coverage-html
coverage: '/ *lines\.*: \d+\.\d+.*/'
artifacts:
paths:
+ - build/doc
- build/meson-logs/coveragereport
x64_rel:
image: lv2plugin/debian-x64
script:
- - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
x64_static:
image: lv2plugin/debian-x64
script:
- - meson setup build -Ddefault_library=static -Dstrict=true -Dwerror=true
+ - meson setup build -Ddefault_library=static -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
x64_sanitize:
image: lv2plugin/debian-x64-clang
script:
- - meson setup build -Db_lundef=false -Dbuildtype=plain -Dstrict=true -Dwerror=true -Dc_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dc_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dcpp_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" -Dcpp_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero"
+ - meson setup build -Db_lundef=false -Dbuildtype=plain -Dstrict=true -Dwerror=true -Dc_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dc_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero -fsanitize=implicit-conversion -fsanitize=local-bounds -fsanitize=nullability" -Dcpp_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" -Dcpp_link_args="-fno-sanitize-recover=all -fsanitize=address -fsanitize=undefined -fsanitize=float-divide-by-zero" -Ddocs=disabled
- ninja -C build test
- meson configure build -Dbuildtype=debugoptimized -Dc_args="" -Dc_link_args="" -Dcpp_args="" -Dcpp_link_args=""
- meson configure build -Db_sanitize=thread
@@ -78,20 +79,20 @@ fedora:
freebsd_dbg:
tags: [freebsd,meson]
script:
- - meson setup build -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - meson setup build -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
freebsd_rel:
tags: [freebsd,meson]
script:
- - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
mingw32_dbg:
image: lv2plugin/debian-mingw32
script:
- - meson setup build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
variables:
WINEPATH: "Z:\\usr\\lib\\gcc\\i686-w64-mingw32\\10-win32"
@@ -99,7 +100,7 @@ mingw32_dbg:
mingw32_rel:
image: lv2plugin/debian-mingw32
script:
- - meson setup build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/i686-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
variables:
WINEPATH: "Z:\\usr\\lib\\gcc\\i686-w64-mingw32\\10-win32"
@@ -108,7 +109,7 @@ mingw32_rel:
mingw64_dbg:
image: lv2plugin/debian-mingw64
script:
- - meson setup build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
variables:
WINEPATH: "Z:\\usr\\lib\\gcc\\x86_64-w64-mingw32\\10-win32"
@@ -116,7 +117,7 @@ mingw64_dbg:
mingw64_rel:
image: lv2plugin/debian-mingw64
script:
- - meson setup build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build --cross-file=/usr/share/meson/cross/x86_64-w64-mingw32.ini -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
variables:
WINEPATH: "Z:\\usr\\lib\\gcc\\x86_64-w64-mingw32\\10-win32"
@@ -131,45 +132,47 @@ mac_dbg:
mac_rel:
tags: [macos]
script:
- - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
win_dbg:
tags: [windows,meson]
script:
- - meson setup build -Dbuildtype=debug -Dstrict=true -Dwerror=true
+ - meson setup build -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
win_rel:
tags: [windows,meson]
script:
- - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true
+ - meson setup build -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddocs=disabled
- ninja -C build test
wasm_dbg:
image: lv2plugin/debian-wasm
script:
- - meson setup build --cross-file=/usr/share/meson/cross/wasm.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddefault_library=static
+ - meson setup build --cross-file=/usr/share/meson/cross/wasm.ini -Dbuildtype=debug -Dstrict=true -Dwerror=true -Ddefault_library=static -Ddocs=disabled
- ninja -C build test
wasm_rel:
image: lv2plugin/debian-wasm
script:
- - meson setup build --cross-file=/usr/share/meson/cross/wasm.ini -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddefault_library=static
+ - meson setup build --cross-file=/usr/share/meson/cross/wasm.ini -Dbuildtype=release -Dstrict=true -Dwerror=true -Ddefault_library=static -Ddocs=disabled
- ninja -C build test
pages:
script:
- - mkdir -p .public
+ - mkdir -p .public/doc
- mv build/meson-logs/coveragereport/ .public/coverage
+ - mv build/doc/singlehtml .public/doc/
+ - mv build/doc/html .public/doc/
- mv .public public
needs:
- x64_dbg
artifacts:
paths:
- - public
+ - public
only:
- main
diff --git a/README.md b/README.md
index 46122e3..829a72c 100644
--- a/README.md
+++ b/README.md
@@ -38,9 +38,8 @@ None, except the C standard library.
Documentation
-------------
-Public interfaces are well-documented in the [headers](include/zix/). There is
-no external documentation at this time.
-
* [Installation Instructions](INSTALL.md)
+ * [API reference (single page)](https://drobilla.gitlab.io/zix/doc/singlehtml)
+ * [API reference (paginated)](https://drobilla.gitlab.io/zix/doc/html)
-- David Robillard <d@drobilla.net>
diff --git a/doc/.clang-tidy b/doc/.clang-tidy
new file mode 100644
index 0000000..3d0f771
--- /dev/null
+++ b/doc/.clang-tidy
@@ -0,0 +1,11 @@
+# Copyright 2020-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+Checks: >
+ *,
+ -altera-*,
+ -clang-analyzer-deadcode.DeadStores,
+ -llvmlibc-*,
+WarningsAsErrors: '*'
+HeaderFilterRegex: '.*'
+FormatStyle: file
diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in
new file mode 100644
index 0000000..52f6dda
--- /dev/null
+++ b/doc/Doxyfile.in
@@ -0,0 +1,72 @@
+# Copyright 2021-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+PROJECT_NAME = Zix
+PROJECT_BRIEF = "A lightweight C library of portability wrappers and data structures"
+
+QUIET = YES
+WARN_AS_ERROR = YES
+WARN_IF_UNDOCUMENTED = YES
+WARN_NO_PARAMDOC = NO
+
+RECURSIVE=YES
+
+JAVADOC_AUTOBRIEF = YES
+
+FULL_PATH_NAMES = NO
+CASE_SENSE_NAMES = YES
+HIDE_IN_BODY_DOCS = YES
+REFERENCES_LINK_SOURCE = NO
+
+GENERATE_HTML = NO
+GENERATE_LATEX = NO
+GENERATE_XML = YES
+XML_PROGRAMLISTING = NO
+SHOW_FILES = NO
+
+
+EXPAND_ONLY_PREDEF = YES
+MACRO_EXPANSION = YES
+SKIP_FUNCTION_MACROS = NO
+PREDEFINED = ZIX_ALLOCATED= \
+ ZIX_ALWAYS_INLINE_FUNC= \
+ ZIX_API= \
+ ZIX_BEGIN_DECLS= \
+ ZIX_CONST_API= \
+ ZIX_CONST_FUNC= \
+ ZIX_END_DECLS= \
+ ZIX_MALLOC_API= \
+ ZIX_MALLOC_FUNC= \
+ ZIX_NONNULL= \
+ ZIX_NULLABLE= \
+ ZIX_PURE_API= \
+ ZIX_PURE_FUNC= \
+ ZIX_PURE_WIN_API= \
+ ZIX_THREAD_FUNC= \
+
+STRIP_FROM_PATH = @ZIX_SRCDIR@
+INPUT = @ZIX_SRCDIR@/include/zix/zix.h \
+ \
+ @ZIX_SRCDIR@/include/zix/attributes.h \
+ @ZIX_SRCDIR@/include/zix/status.h \
+ @ZIX_SRCDIR@/include/zix/string_view.h \
+ \
+ @ZIX_SRCDIR@/include/zix/allocator.h \
+ @ZIX_SRCDIR@/include/zix/bump_allocator.h \
+ \
+ @ZIX_SRCDIR@/include/zix/digest.h \
+ @ZIX_SRCDIR@/include/zix/function_types.h \
+ \
+ @ZIX_SRCDIR@/include/zix/bitset.h \
+ @ZIX_SRCDIR@/include/zix/btree.h \
+ @ZIX_SRCDIR@/include/zix/hash.h \
+ @ZIX_SRCDIR@/include/zix/ring.h \
+ @ZIX_SRCDIR@/include/zix/tree.h \
+ \
+ @ZIX_SRCDIR@/include/zix/sem.h \
+ @ZIX_SRCDIR@/include/zix/thread.h \
+ \
+ @ZIX_SRCDIR@/include/zix/filesystem.h \
+ @ZIX_SRCDIR@/include/zix/path.h \
+
+OUTPUT_DIRECTORY = @DOX_OUTPUT@
diff --git a/doc/api/meson.build b/doc/api/meson.build
new file mode 100644
index 0000000..b6a1adb
--- /dev/null
+++ b/doc/api/meson.build
@@ -0,0 +1,9 @@
+# Copyright 2021 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+c_zix_rst = custom_target(
+ 'zix.rst',
+ command: [dox_to_sphinx, '-f', '@INPUT0@', '@OUTDIR@'],
+ input: [c_index_xml] + c_rst_files,
+ output: 'zix.rst',
+)
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..e8a057c
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,95 @@
+# Copyright 2021-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+# Project information
+
+project = "Zix"
+copyright = "2011-2022, David Robillard"
+author = "David Robillard"
+release = "dev"
+desc = "A lightweight C library of portability wrappers and data structures"
+
+# General configuration
+
+exclude_patterns = ["xml"]
+language = "en"
+nitpicky = True
+pygments_style = "friendly"
+
+# Ignore everything opaque or external for nitpicky mode
+_opaque = [
+ "FILE",
+ "ZixAllocator",
+ "ZixAllocatorImpl",
+ "ZixBTree",
+ "ZixBTreeImpl",
+ "ZixBTreeNode",
+ "ZixBTreeNodeImpl",
+ "ZixHash",
+ "ZixHashImpl",
+ "ZixRing",
+ "ZixRingImpl",
+ "ZixSem",
+ "ZixSemImpl",
+ "ZixTree",
+ "ZixTreeImpl",
+ "ZixTreeIter",
+ "ZixTreeNode",
+ "ZixTreeNodeImpl",
+ "int64_t",
+ "pthread_t",
+ "ptrdiff_t",
+ "size_t",
+ "uint16_t",
+ "uint32_t",
+ "uint64_t",
+ "uint8_t",
+]
+
+_c_nitpick_ignore = map(lambda x: ("c:identifier", x), _opaque)
+_cpp_nitpick_ignore = map(lambda x: ("cpp:identifier", x), _opaque)
+nitpick_ignore = list(_c_nitpick_ignore) + list(_cpp_nitpick_ignore)
+
+# HTML output
+
+html_copy_source = False
+html_short_title = "Zix"
+html_theme = "sphinx_lv2_theme"
+
+if tags.has("singlehtml"):
+ html_sidebars = {
+ "**": [
+ "globaltoc.html",
+ ]
+ }
+
+ html_theme_options = {
+ "body_max_width": "48em",
+ "body_min_width": "48em",
+ "description": desc,
+ "show_footer_version": True,
+ "show_logo_version": True,
+ "logo_name": True,
+ "logo_width": "8em",
+ "nosidebar": False,
+ "page_width": "80em",
+ "sidebar_width": "18em",
+ "globaltoc_maxdepth": 3,
+ "globaltoc_collapse": False,
+ }
+
+else:
+ html_theme_options = {
+ "body_max_width": "60em",
+ "body_min_width": "40em",
+ "description": desc,
+ "show_footer_version": True,
+ "show_logo_version": True,
+ "logo_name": True,
+ "logo_width": "8em",
+ "nosidebar": True,
+ "page_width": "60em",
+ "sidebar_width": "14em",
+ "globaltoc_maxdepth": 1,
+ "globaltoc_collapse": True,
+ }
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..56062e6
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,15 @@
+..
+ Copyright 2020-2022 David Robillard <d@drobilla.net>
+ SPDX-License-Identifier: ISC
+
+###
+Zix
+###
+
+.. include:: summary.rst
+
+.. toctree::
+ :numbered:
+
+ using_zix
+ api/zix
diff --git a/doc/meson.build b/doc/meson.build
new file mode 100644
index 0000000..f406624
--- /dev/null
+++ b/doc/meson.build
@@ -0,0 +1,78 @@
+# Copyright 2021-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+docdir = get_option('datadir') / 'doc'
+
+doxygen = find_program('doxygen', required: get_option('docs'))
+dox_to_sphinx = find_program('../scripts/dox_to_sphinx.py')
+sphinx_build = find_program('sphinx-build', required: get_option('docs'))
+
+build_docs = doxygen.found() and sphinx_build.found()
+if build_docs
+
+ # Documentation Code
+
+ test(
+ 'overview_code',
+ executable(
+ 'overview_code',
+ files('overview_code.c'),
+ dependencies: [zix_dep],
+ c_args: c_suppressions,
+ )
+ )
+
+ # Generated API Reference
+
+ c_rst_files = files(
+ 'index.rst',
+ 'string_views.rst',
+ 'summary.rst',
+ 'using_zix.rst',
+ )
+
+ c_doc_files = c_rst_files + files(
+ 'conf.py',
+ 'overview_code.c',
+ )
+
+ foreach f : c_doc_files
+ configure_file(copy: true, input: f, output: '@PLAINNAME@')
+ endforeach
+
+ subdir('xml')
+ subdir('api')
+
+ sphinx_options = [
+ '-D', 'release=@0@'.format(meson.project_version()),
+ '-E',
+ '-W',
+ '-a',
+ '-q',
+ ]
+
+ doc_inputs = c_rst_files + [c_zix_rst, c_index_xml]
+
+ custom_target(
+ 'singlehtml',
+ build_by_default: true,
+ command: [sphinx_build, '-M', 'singlehtml', '@OUTDIR@', '@OUTDIR@',
+ '-t', 'singlehtml'] + sphinx_options,
+ input: doc_inputs,
+ install: true,
+ install_dir: docdir / versioned_name,
+ output: 'singlehtml',
+ )
+
+ custom_target(
+ 'html',
+ build_by_default: true,
+ command: [sphinx_build, '-M', 'html', '@OUTDIR@', '@OUTDIR@',
+ '-t', 'html'] + sphinx_options,
+ input: doc_inputs,
+ install: true,
+ install_dir: docdir / versioned_name,
+ output: 'html',
+ )
+
+endif
diff --git a/doc/overview_code.c b/doc/overview_code.c
new file mode 100644
index 0000000..1370292
--- /dev/null
+++ b/doc/overview_code.c
@@ -0,0 +1,51 @@
+// Copyright 2021-2022 David Robillard <d@drobilla.net>
+// SPDX-License-Identifier: ISC
+
+/*
+ Example code that is included in the documentation. Code in the
+ documentation is included from here rather than written inline so that it can
+ be tested and avoid rotting. The code here doesn't make much sense, but is
+ written such that it at least compiles and will run without crashing.
+*/
+
+#include "zix/attributes.h"
+#include "zix/string_view.h"
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wunused-variable"
+#endif
+
+static void
+string_views(void)
+{
+ const char* const string_pointer = "some string";
+
+ // begin make-empty-string
+ ZixStringView empty = zix_empty_string();
+ // end make-empty-string
+
+ // begin make-static-string
+ ZixStringView hello = zix_string("hello");
+ // end make-static-string
+
+ // begin measure-string
+ ZixStringView view = zix_string(string_pointer);
+ // end measure-string
+
+ // begin make-string-view
+ ZixStringView slice = zix_substring(string_pointer, 4);
+ // end make-string-view
+}
+
+ZIX_CONST_FUNC
+int
+main(void)
+{
+ string_views();
+ return 0;
+}
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
diff --git a/doc/string_views.rst b/doc/string_views.rst
new file mode 100644
index 0000000..b6e3fd9
--- /dev/null
+++ b/doc/string_views.rst
@@ -0,0 +1,56 @@
+..
+ Copyright 2020-2022 David Robillard <d@drobilla.net>
+ SPDX-License-Identifier: ISC
+
+String Views
+============
+
+.. default-domain:: c
+.. highlight:: c
+
+For performance reasons,
+many functions in zix that take a string take a :struct:`ZixStringView`,
+rather than a bare pointer.
+This forces code to be explicit about string measurement,
+which discourages common patterns of repeated measurement of the same string.
+For convenience, several macros and functions are provided for constructing string views:
+
+:func:`zix_empty_string`
+
+ Constructs a view of an empty string, for example:
+
+ .. literalinclude:: overview_code.c
+ :start-after: begin make-empty-string
+ :end-before: end make-empty-string
+ :dedent: 2
+
+:func:`zix_string`
+
+ Constructs a view of an arbitrary string, for example:
+
+ .. literalinclude:: overview_code.c
+ :start-after: begin measure-string
+ :end-before: end measure-string
+ :dedent: 2
+
+ This calls ``strlen`` to measure the string.
+ Modern compilers should optimize this away if the parameter is a literal.
+
+:func:`zix_substring`
+
+ Constructs a view of a slice of a string with an explicit length,
+ for example:
+
+ .. literalinclude:: overview_code.c
+ :start-after: begin make-string-view
+ :end-before: end make-string-view
+ :dedent: 2
+
+ This can also be used to create a view of a pre-measured string.
+ If the length a dynamic string is already known,
+ this is faster than :func:`zix_string`,
+ since it avoids redundant measurement.
+
+These constructors can be used inline when passing parameters,
+but if the same dynamic string is used several times,
+it is better to make a string view variable to avoid redundant measurement.
diff --git a/doc/summary.rst b/doc/summary.rst
new file mode 100644
index 0000000..aa169df
--- /dev/null
+++ b/doc/summary.rst
@@ -0,0 +1,5 @@
+..
+ Copyright 2020-2022 David Robillard <d@drobilla.net>
+ SPDX-License-Identifier: ISC
+
+Zix is a lightweight C library of portability wrappers and data structures.
diff --git a/doc/using_zix.rst b/doc/using_zix.rst
new file mode 100644
index 0000000..f97ecf0
--- /dev/null
+++ b/doc/using_zix.rst
@@ -0,0 +1,12 @@
+..
+ Copyright 2020-2022 David Robillard <d@drobilla.net>
+ SPDX-License-Identifier: ISC
+
+##########
+Using Zix
+##########
+
+.. toctree::
+
+ string_views
+
diff --git a/doc/xml/meson.build b/doc/xml/meson.build
new file mode 100644
index 0000000..b614580
--- /dev/null
+++ b/doc/xml/meson.build
@@ -0,0 +1,19 @@
+# Copyright 2021-2022 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: 0BSD OR ISC
+
+config = configuration_data()
+config.set('ZIX_SRCDIR', zix_src_root)
+config.set('DOX_OUTPUT', meson.current_build_dir() / '..')
+
+c_doxyfile = configure_file(
+ configuration: config,
+ input: files('../Doxyfile.in'),
+ output: 'Doxyfile',
+)
+
+c_index_xml = custom_target(
+ 'index.xml',
+ command: [doxygen, '@INPUT0@'],
+ input: [c_doxyfile] + c_headers,
+ output: 'index.xml',
+)
diff --git a/meson.build b/meson.build
index 5d5eaa7..0e0afac 100644
--- a/meson.build
+++ b/meson.build
@@ -12,6 +12,7 @@ project('zix', ['c'],
'cpp_std=c++17',
])
+zix_src_root = meson.current_source_dir()
major_version = meson.project_version().split('.')[0]
version_suffix = '-@0@'.format(major_version)
versioned_name = 'zix' + version_suffix
@@ -521,8 +522,16 @@ if not get_option('benchmarks').disabled()
endif
endif
+#############################
+# Scripts and Documentation #
+#############################
+
subdir('scripts')
+if not get_option('docs').disabled()
+ subdir('doc')
+endif
+
if not meson.is_subproject()
summary('Benchmarks', build_benchmarks, bool_yn: true)
summary('Tests', not get_option('tests').disabled(), bool_yn: true)
diff --git a/meson_options.txt b/meson_options.txt
index bc25d2f..476b3d5 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -4,6 +4,9 @@ option('benchmarks', type: 'feature', value: 'auto', yield: true,
option('checks', type: 'boolean', value: true, yield: true,
description: 'Check for features with the build system')
+option('docs', type: 'feature', value: 'auto', yield: true,
+ description: 'Build documentation')
+
option('posix', type: 'feature', value: 'auto', yield: true,
description: 'Use POSIX system facilities')
diff --git a/scripts/dox_to_sphinx.py b/scripts/dox_to_sphinx.py
new file mode 100755
index 0000000..49fbdfd
--- /dev/null
+++ b/scripts/dox_to_sphinx.py
@@ -0,0 +1,698 @@
+#!/usr/bin/env python3
+
+# Copyright 2020 David Robillard <d@drobilla.net>
+# SPDX-License-Identifier: ISC
+
+"""
+Write Sphinx markup from Doxygen XML.
+
+Takes a path to a directory of XML generated by Doxygen, and emits a directory
+with a reStructuredText file for every documented symbol.
+"""
+
+import argparse
+import os
+import re
+import sys
+import textwrap
+import xml.etree.ElementTree
+
+__author__ = "David Robillard"
+__date__ = "2020-11-18"
+__email__ = "d@drobilla.net"
+__license__ = "ISC"
+__version__ = __date__.replace("-", ".")
+
+
+def load_index(index_path):
+ """
+ Load the index from XML.
+
+ :returns: A dictionary from ID to skeleton records with basic information
+ for every documented entity. Some records have an ``xml_filename`` key
+ with the filename of a definition file. These files will be loaded later
+ to flesh out the records in the index.
+ """
+
+ root = xml.etree.ElementTree.parse(index_path).getroot()
+ index = {}
+
+ for compound in root:
+ compound_id = compound.get("refid")
+ compound_kind = compound.get("kind")
+ compound_name = compound.find("name").text
+ if compound_kind in ["dir", "file", "page"]:
+ continue
+
+ # Add record for compound (compounds appear only once in the index)
+ assert compound_id not in index
+ index[compound_id] = {
+ "kind": compound_kind,
+ "name": compound_name,
+ "xml_filename": compound_id + ".xml",
+ "children": [],
+ }
+
+ name_prefix = (
+ ("%s::" % compound_name) if compound_kind == "namespace" else ""
+ )
+
+ for child in compound.findall("member"):
+ if child.get("refid") in index:
+ assert compound_kind == "group"
+ continue
+
+ # Everything has a kind and a name
+ child_record = {
+ "kind": child.get("kind"),
+ "name": name_prefix + child.find("name").text,
+ }
+
+ if child.get("kind") == "enum":
+ # Enums are not compounds, but we want to resolve the parent of
+ # their values so they are not written as top level documents
+ child_record["children"] = []
+
+ if child.get("kind") == "enumvalue":
+ # Remove namespace prefix
+ child_record["name"] = child.find("name").text
+
+ if child.get("kind") == "variable":
+ if child_record["name"][0] == "@":
+ # Remove placeholder name from anonymous struct or union
+ child_record["name"] = ""
+ else:
+ # Remove namespace prefix
+ child_record["name"] = child.find("name").text
+
+ index[child.get("refid")] = child_record
+
+ return index
+
+
+def resolve_index(index, root):
+ """
+ Walk a definition document and extend the index for linking.
+
+ This does two things: sets the "parent" and "children" fields of all
+ applicable records, and sets the "strong" field of enums so that the
+ correct Sphinx role can be used when referring to them.
+ """
+
+ def add_child(index, parent_id, child_id):
+ parent = index[parent_id]
+ child = index[child_id]
+
+ if child["kind"] == "enumvalue":
+ assert parent["kind"] == "enum"
+ assert "parent" not in child or child["parent"] == parent_id
+ child["parent"] = parent_id
+
+ else:
+ if parent["kind"] in ["class", "struct", "union"]:
+ assert "parent" not in child or child["parent"] == parent_id
+ child["parent"] = parent_id
+
+ if child_id not in parent["children"]:
+ parent["children"] += [child_id]
+
+ compound = root.find("compounddef")
+ compound_kind = compound.get("kind")
+
+ if compound_kind == "group":
+ for subgroup in compound.findall("innergroup"):
+ add_child(index, compound.get("id"), subgroup.get("refid"))
+
+ for klass in compound.findall("innerclass"):
+ add_child(index, compound.get("id"), klass.get("refid"))
+
+ for section in compound.findall("sectiondef"):
+ if section.get("kind").startswith("private"):
+ for member in section.findall("memberdef"):
+ if member.get("id") in index:
+ del index[member.get("id")]
+ else:
+ for member in section.findall("memberdef"):
+ if member.find("definition") is not None:
+ definition = plain_text(member.find("definition"))
+
+ # Skip constructors, which Doxygen also lists as functions
+ # in groups for some reason. Unfortunately the only way I
+ # can see to distinguish them is to assume things like
+ # symbol::Symbol::Symbol are constructors.
+ if re.match(
+ "[_0-9A-Za-z]*::[_0-9A-Za-z]*::[_0-9A-Za-z]*",
+ definition,
+ ):
+ continue
+
+ member_id = member.get("id")
+ add_child(index, compound.get("id"), member_id)
+
+ if member.get("kind") == "enum":
+ index[member_id]["strong"] = member.get("strong") == "yes"
+ for value in member.findall("enumvalue"):
+ add_child(index, member_id, value.get("id"))
+
+
+def sphinx_role(record, lang):
+ """
+ Return the Sphinx role used for a record.
+
+ This is used for the description directive like ".. c:function::", and
+ links like ":c:func:`foo`.
+ """
+
+ kind = record["kind"]
+
+ if kind in ["class", "function", "namespace", "struct", "union"]:
+ return lang + ":" + kind
+
+ if kind == "define":
+ return "c:macro"
+
+ if kind == "group":
+ return "ref"
+
+ if kind == "enum":
+ return lang + (":enum-class" if record["strong"] else ":enum")
+
+ if kind == "typedef":
+ return lang + ":type"
+
+ if kind == "enumvalue":
+ return lang + ":enumerator"
+
+ if kind == "variable":
+ return lang + (":member" if "parent" in record else ":var")
+
+ raise RuntimeError("No known role for kind '%s'" % kind)
+
+
+def child_identifier(lang, parent_name, child_name):
+ """
+ Return the identifier for an enum value or struct member.
+
+ Sphinx, for some reason, uses a different syntax for this in C and C++.
+ """
+
+ separator = "::" if lang == "cpp" else "."
+
+ return "%s%s%s" % (parent_name, separator, child_name)
+
+
+def link_markup(index, lang, refid):
+ """Return a Sphinx link for a Doxygen reference."""
+
+ record = index[refid]
+ kind, name = record["kind"], record["name"]
+ role = sphinx_role(record, lang)
+
+ if kind in ["class", "define", "enum", "struct", "typedef", "union"]:
+ return ":%s:`%s`" % (role, name)
+
+ if kind == "group":
+ return ":ref:`%s`" % name
+
+ if kind == "function":
+ return ":%s:func:`%s`" % (lang, name)
+
+ if kind == "enumvalue":
+ parent_name = index[record["parent"]]["name"]
+ return ":%s:`%s`" % (role, child_identifier(lang, parent_name, name))
+
+ if kind == "variable":
+ if "parent" not in record:
+ return ":%s:var:`%s`" % (lang, name)
+
+ parent_name = index[record["parent"]]["name"]
+ return ":%s:`%s`" % (role, child_identifier(lang, parent_name, name))
+
+ raise RuntimeError("Unknown link target kind: %s" % kind)
+
+
+def indent(markup, depth):
+ """
+ Indent markup to a depth level.
+
+ Like textwrap.indent() but takes an integer and works in reST indentation
+ levels for clarity."
+ """
+
+ return textwrap.indent(markup, " " * depth)
+
+
+def heading(text, level):
+ """
+ Return a ReST heading at a given level.
+
+ Follows the style in the Python documentation guide, see
+ <https://devguide.python.org/documenting/#sections>.
+ """
+
+ assert 1 <= level <= 6
+
+ chars = ("#", "*", "=", "-", "^", '"')
+ line = chars[level] * len(text)
+
+ return "%s%s\n%s\n\n" % (line + "\n" if level < 3 else "", text, line)
+
+
+def dox_to_rst(index, lang, node):
+ """
+ Convert documentation commands (docCmdGroup) to Sphinx markup.
+
+ This is used to convert the content of descriptions in the documentation.
+ It recursively parses all children tags and raises a RuntimeError if any
+ unknown tag is encountered.
+ """
+
+ def field_value(markup):
+ """Return a value for a field as a single line or indented block."""
+ if "\n" in markup.strip():
+ return "\n" + indent(markup, 1)
+
+ return " " + markup.strip()
+
+ if node.tag == "emphasis":
+ return "*%s*" % plain_text(node)
+
+ if node.tag == "lsquo":
+ return "‘"
+
+ if node.tag == "rsquo":
+ return "’"
+
+ if node.tag == "computeroutput":
+ return "``%s``" % plain_text(node)
+
+ if node.tag == "itemizedlist":
+ markup = ""
+ for item in node.findall("listitem"):
+ assert len(item) == 1
+ markup += "\n- %s" % dox_to_rst(index, lang, item[0])
+
+ return markup
+
+ if node.tag == "para":
+ markup = node.text if node.text is not None else ""
+ for child in node:
+ markup += dox_to_rst(index, lang, child)
+ markup += child.tail if child.tail is not None else ""
+
+ return markup.strip() + "\n\n"
+
+ if node.tag == "parameterlist":
+ markup = ""
+ for item in node.findall("parameteritem"):
+ name = item.find("parameternamelist/parametername")
+ description = item.find("parameterdescription")
+ assert len(description) == 1
+ markup += "\n\n:param %s:%s" % (
+ name.text,
+ field_value(dox_to_rst(index, lang, description[0])),
+ )
+
+ return markup + "\n"
+
+ if node.tag == "programlisting":
+ return "\n.. code-block:: %s\n\n%s" % (
+ lang,
+ indent(plain_text(node), 1),
+ )
+
+ if node.tag == "ref":
+ refid = node.get("refid")
+ if refid not in index:
+ sys.stderr.write("warning: Unresolved link: %s\n" % refid)
+ return node.text
+
+ assert len(node) == 0
+ assert len(link_markup(index, lang, refid)) > 0
+ return link_markup(index, lang, refid)
+
+ if node.tag == "simplesect":
+ assert len(node) == 1
+
+ if node.get("kind") == "return":
+ return "\n:returns:" + field_value(
+ dox_to_rst(index, lang, node[0])
+ )
+
+ if node.get("kind") == "see":
+ return dox_to_rst(index, lang, node[0])
+
+ raise RuntimeError("Unknown simplesect kind: %s" % node.get("kind"))
+
+ if node.tag == "ulink":
+ return "`%s <%s>`_" % (node.text, node.get("url"))
+
+ raise RuntimeError("Unknown documentation command: %s" % node.tag)
+
+
+def description_markup(index, lang, node):
+ """Return the markup for a brief or detailed description."""
+
+ assert node.tag == "briefdescription" or node.tag == "detaileddescription"
+ assert not (node.tag == "briefdescription" and len(node) > 1)
+ assert len(node.text.strip()) == 0
+
+ return "".join([dox_to_rst(index, lang, child) for child in node]).strip()
+
+
+def set_descriptions(index, lang, definition, record):
+ """Set a record's brief/detailed descriptions from the XML definition."""
+
+ for tag in ["briefdescription", "detaileddescription"]:
+ node = definition.find(tag)
+ if node is not None:
+ record[tag] = description_markup(index, lang, node)
+
+
+def set_template_params(node, record):
+ """Set a record's template_params from the XML definition."""
+
+ template_param_list = node.find("templateparamlist")
+ if template_param_list is not None:
+ params = []
+ for param in template_param_list.findall("param"):
+ if param.find("declname") is not None:
+ # Value parameter
+ type_text = plain_text(param.find("type"))
+ name_text = plain_text(param.find("declname"))
+
+ params += ["%s %s" % (type_text, name_text)]
+ else:
+ # Type parameter
+ params += ["%s" % (plain_text(param.find("type")))]
+
+ record["template_params"] = "%s" % ", ".join(params)
+
+
+def plain_text(node):
+ """
+ Return the plain text of a node with all tags ignored.
+
+ This is needed where Doxygen may include refs but Sphinx needs plain text
+ because it parses things itself to generate links.
+ """
+
+ if node.tag == "sp":
+ markup = " "
+ elif node.text is not None:
+ markup = node.text
+ else:
+ markup = ""
+
+ for child in node:
+ markup += plain_text(child)
+ markup += child.tail if child.tail is not None else ""
+
+ return markup
+
+
+def local_name(name):
+ """Return a name with all namespace prefixes stripped."""
+
+ try:
+ sep_end = name.rindex("::") + 2
+ return name[sep_end:]
+ except ValueError:
+ return name
+
+
+def read_definition_doc(index, lang, root):
+ """Walk a definition document and update described records in the index."""
+
+ # Set descriptions for the compound itself
+ compound = root.find("compounddef")
+ compound_record = index[compound.get("id")]
+ set_descriptions(index, lang, compound, compound_record)
+ set_template_params(compound, compound_record)
+
+ if compound.find("title") is not None:
+ compound_record["title"] = compound.find("title").text.strip()
+
+ # Set documentation for all children
+ for section in compound.findall("sectiondef"):
+ if section.get("kind").startswith("private"):
+ continue
+
+ for member in section.findall("memberdef"):
+ kind = member.get("kind")
+ record = index[member.get("id")]
+ set_descriptions(index, lang, member, record)
+ set_template_params(member, record)
+
+ if compound.get("kind") in ["class", "struct", "union"]:
+ assert kind in ["function", "typedef", "variable"]
+ record["type"] = plain_text(member.find("type"))
+
+ if kind == "define":
+ if member.find("param") is not None:
+ param_names = []
+ for param in member.findall("param"):
+ defname = param.find("defname")
+ param_names += (
+ [defname.text] if defname is not None else []
+ )
+
+ record["prototype"] = "%s(%s)" % (
+ record["name"],
+ ", ".join(param_names),
+ )
+
+ elif kind == "enum":
+ for value in member.findall("enumvalue"):
+ set_descriptions(
+ index, lang, value, index[value.get("id")]
+ )
+
+ elif kind == "function":
+ record["prototype"] = "%s %s%s" % (
+ plain_text(member.find("type")),
+ member.find("name").text,
+ member.find("argsstring").text,
+ )
+
+ elif kind == "typedef":
+ name = local_name(record["name"])
+ args_text = member.find("argsstring").text
+ target_text = plain_text(member.find("type"))
+ if args_text is not None: # Function pointer
+ assert target_text[-2:] == "(*" and args_text[0] == ")"
+ record["type"] = target_text + args_text
+ record["definition"] = target_text + name + args_text
+ else: # Normal named typedef
+ assert target_text is not None
+ record["type"] = target_text
+ if member.find("definition").text.startswith("using"):
+ record["definition"] = "%s = %s" % (
+ name,
+ target_text,
+ )
+ else:
+ record["definition"] = "%s %s" % (
+ target_text,
+ name,
+ )
+
+ elif kind == "variable":
+ record["type"] = plain_text(member.find("type"))
+ record["name"] = plain_text(member.find("name"))
+ record["definition"] = plain_text(member.find("definition"))
+
+
+def declaration_string(record):
+ """
+ Return the string that describes a declaration.
+
+ This is what follows the directive, and is in C/C++ syntax, except without
+ keywords like "typedef" and "using" as expected by Sphinx. For example,
+ "struct ThingImpl Thing" or "void run(int value)".
+ """
+
+ kind = record["kind"]
+ result = ""
+
+ if "template_params" in record:
+ result = "template <%s> " % record["template_params"]
+
+ if kind == "function":
+ result += record["prototype"]
+ elif kind == "typedef":
+ result += record["definition"]
+ elif kind == "variable":
+ if "type" in record and "name" in record:
+ result += "%s %s" % (record["type"], local_name(record["name"]))
+ else:
+ result += record["definition"]
+ elif "type" in record:
+ result += "%s %s" % (record["type"], local_name(record["name"]))
+ else:
+ result += local_name(record["name"])
+
+ return result
+
+
+def document_markup(index, lang, record):
+ """Return the complete document that describes some documented entity."""
+
+ kind = record["kind"]
+ role = sphinx_role(record, lang)
+ name = record["name"]
+ markup = ""
+
+ if name != local_name(name):
+ sep = name.rindex("::")
+ markup += ".. cpp:namespace:: %s\n\n" % name[0:sep]
+
+ # Write top-level directive
+ markup += ".. %s:: %s\n" % (role, declaration_string(record))
+
+ # Write main description blurb
+ markup += "\n" + indent(record["briefdescription"] + "\n", 1)
+ if len(record["detaileddescription"]) > 0:
+ markup += "\n" + indent(record["detaileddescription"], 1) + "\n"
+
+ assert (
+ kind in ["class", "enum", "namespace", "struct", "union"]
+ or "children" not in record
+ )
+
+ # Sphinx C++ namespaces work by setting a scope, they have no content
+ child_indent = 0 if kind == "namespace" else 1
+
+ # Write inline children if applicable
+ markup += "\n" if "children" in record else ""
+ for child_id in record.get("children", []):
+ child_record = index[child_id]
+ child_role = sphinx_role(child_record, lang)
+
+ if not child_record["name"]:
+ continue # Skip anonymous union member
+
+ child_header = ".. %s:: %s\n\n" % (
+ child_role,
+ declaration_string(child_record),
+ )
+
+ markup += "\n"
+ markup += indent(child_header, child_indent)
+ markup += indent(child_record["briefdescription"], child_indent + 1)
+ markup += indent(child_record["detaileddescription"], child_indent + 1)
+ markup += "\n"
+
+ return markup
+
+
+def symbol_filename(name):
+ """Adapt the name of a symbol to be suitable for use as a filename."""
+
+ return name.replace("::", "__")
+
+
+def emit_groups(index, lang, output_dir, force):
+ """Write a description file for every group documented in the index."""
+
+ for record in index.values():
+ if record["kind"] != "group":
+ continue
+
+ name = record["name"]
+ filename = os.path.join(output_dir, "%s.rst" % name)
+ if not force and os.path.exists(filename):
+ raise FileExistsError("File already exists: '%s'" % filename)
+
+ with open(filename, "w") as rst:
+ rst.write(".. _%s:\n\n" % name)
+ rst.write(heading(record["title"], 1))
+
+ # Get all child group and symbol names
+ child_groups = {}
+ child_symbols = {}
+ for child_id in record["children"]:
+ child = index[child_id]
+ assert child["name"][0] != "@"
+ if child["kind"] == "group":
+ child_groups[child["name"]] = child
+ else:
+ child_symbols[child["name"]] = child
+
+ # Emit description (document body)
+ if len(record["briefdescription"]) > 0:
+ rst.write(record["briefdescription"] + "\n\n")
+ if len(record["detaileddescription"]) > 0:
+ rst.write(record["detaileddescription"] + "\n\n")
+
+ if len(child_groups) > 0:
+ # Emit TOC for child groups
+ rst.write(".. toctree::\n\n")
+ for name, group in child_groups.items():
+ rst.write(indent(group["name"], 1) + "\n")
+
+ # Emit symbols in sorted order
+ for name, symbol in child_symbols.items():
+ rst.write("\n")
+ rst.write(document_markup(index, lang, symbol))
+ rst.write("\n")
+
+
+def run(index_xml_path, output_dir, language, force):
+ """Write a directory of Sphinx files from a Doxygen XML directory."""
+
+ # Build skeleton index from index.xml
+ xml_dir = os.path.dirname(index_xml_path)
+ index = load_index(index_xml_path)
+
+ # Load all definition documents
+ definition_docs = []
+ for record in index.values():
+ if "xml_filename" in record:
+ xml_path = os.path.join(xml_dir, record["xml_filename"])
+ definition_docs += [xml.etree.ElementTree.parse(xml_path)]
+
+ # Do an initial pass of the definition documents to resolve the index
+ for root in definition_docs:
+ resolve_index(index, root)
+
+ # Finally read the documentation from definition documents
+ for root in definition_docs:
+ read_definition_doc(index, language, root)
+
+ # Create output directory
+ try:
+ os.makedirs(output_dir)
+ except OSError:
+ pass
+
+ # Emit output files
+ emit_groups(index, language, output_dir, force)
+
+
+if __name__ == "__main__":
+ ap = argparse.ArgumentParser(
+ usage="%(prog)s [OPTION]... XML_DIR OUTPUT_DIR",
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ ap.add_argument(
+ "-f",
+ "--force",
+ action="store_true",
+ help="overwrite files",
+ )
+
+ ap.add_argument(
+ "-l",
+ "--language",
+ default="c",
+ choices=["c", "cpp"],
+ help="language domain for output",
+ )
+
+ ap.add_argument("index_xml_path", help="path index.xml from Doxygen")
+ ap.add_argument("output_dir", help="output directory")
+
+ run(**vars(ap.parse_args(sys.argv[1:])))
diff --git a/scripts/meson.build b/scripts/meson.build
index f3f347e..5b04f99 100644
--- a/scripts/meson.build
+++ b/scripts/meson.build
@@ -6,22 +6,20 @@ if get_option('strict') and not meson.is_subproject()
pylint = find_program('pylint', required: get_option('tests'))
black = find_program('black', required: get_option('tests'))
- # Scripts that pass with everything including pylint
- scripts = files(
- 'benchmark.py',
- 'plot.py',
- )
+ strict_scripts = files('benchmark.py', 'plot.py')
+ linty_scripts = files('dox_to_sphinx.py')
+ all_scripts = strict_scripts + linty_scripts
if is_variable('black') and black.found()
black_opts = ['-l', '79', '-q', '--check']
- test('black', black, args: black_opts + scripts, suite: 'scripts')
+ test('black', black, args: black_opts + all_scripts, suite: 'scripts')
endif
if is_variable('flake8') and flake8.found()
- test('flake8', flake8, args: scripts, suite: 'scripts')
+ test('flake8', flake8, args: all_scripts, suite: 'scripts')
endif
if is_variable('pylint') and pylint.found()
- test('pylint', pylint, args: scripts, suite: 'scripts')
+ test('pylint', pylint, args: strict_scripts, suite: 'scripts')
endif
endif