Skip to content

Commit 876482e

Browse files
SineStrikerclaude
andcommitted
ci: build, test and check the installed package, and document the build and the ABI policy
The workflow builds and tests wolf on Linux and Windows, runs the script tests and the declaration lint, installs the package and builds a consumer that checks the exported variables and the absence of the Support headers. The README describes the linguist identity, the ABI policy and the vcpkg build, and docs/Status.md records the state of the implementation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 03af778 commit 876482e

6 files changed

Lines changed: 490 additions & 46 deletions

File tree

‎.github/consumer/CMakeLists.txt‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# This project consumes wolf through the installed CMake package in the same way as a host.
2+
#
3+
# The build of the library itself does not verify that package. The install rules write every path
4+
# in the package, and no step before this project reads those paths. A target file that names a
5+
# library that was not installed, a config file without find_dependency(synthrt), and an include
6+
# directory that points into the build tree all pass the test suite of this repository and fail in
7+
# a host. CI therefore installs wolf and configures this project, and the assertions below check
8+
# the package rather than the code.
9+
cmake_minimum_required(VERSION 3.19)
10+
project(wolf_package_consumer LANGUAGES CXX)
11+
12+
set(CMAKE_CXX_STANDARD 17)
13+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
14+
15+
find_package(wolf CONFIG REQUIRED)
16+
17+
if(NOT TARGET wolf::wolf)
18+
message(FATAL_ERROR "the wolf package defines no wolf::wolf target")
19+
endif()
20+
21+
# The plugin tree is a path rather than a target. No target links against it, so no other check
22+
# detects its absence. A host passes one directory per category to SynthUnit::setPluginPaths, which
23+
# requires the package to export the location of those directories.
24+
if(NOT DEFINED WOLF_PLUGINS_DIR)
25+
message(FATAL_ERROR "the wolf package exports no WOLF_PLUGINS_DIR")
26+
endif()
27+
# The package must provide one directory for each category in which the library ships plugins. Every
28+
# library that adds inference contracts also uses the inferenceinterpreters category, so a host
29+
# lists the wolf directory next to the directories of those libraries.
30+
foreach(_category IN ITEMS linguistproviders inferenceinterpreters)
31+
if(NOT IS_DIRECTORY "${WOLF_PLUGINS_DIR}/${_category}")
32+
message(FATAL_ERROR "WOLF_PLUGINS_DIR (${WOLF_PLUGINS_DIR}) contains no "
33+
"${_category} directory")
34+
endif()
35+
endforeach()
36+
message(STATUS "wolf plugins: ${WOLF_PLUGINS_DIR}")
37+
if(DEFINED WOLF_PLUGINS_DIR_DEBUG)
38+
message(STATUS "wolf plugins (debug): ${WOLF_PLUGINS_DIR_DEBUG}")
39+
endif()
40+
41+
# wolf/Support contains the internal helpers of the library and is not installed. Its stateless
42+
# helpers are exported from no binary, and the symbols that libwolf exports from it
43+
# (WOLF_INTERNAL_EXPORT) serve only the plugins built with wolf and carry no API or ABI guarantee. A
44+
# Support header in the package would expose exactly that surface to a host, so its absence is
45+
# checked here.
46+
get_target_property(_wolf_include_dirs wolf::wolf INTERFACE_INCLUDE_DIRECTORIES)
47+
foreach(_dir IN LISTS _wolf_include_dirs)
48+
if(EXISTS "${_dir}/wolf/Support")
49+
message(FATAL_ERROR "the wolf package installs wolf/Support under ${_dir}")
50+
endif()
51+
endforeach()
52+
53+
add_executable(wolf_package_consumer main.cpp)
54+
target_link_libraries(wolf_package_consumer PRIVATE wolf::wolf)

‎.github/consumer/main.cpp‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Includes the headers offered to a host and references one symbol from the library, so that the
2+
// program links against the installed library.
3+
//
4+
// The package installs the Api, Linguist and Session headers. The wolf/Support headers are
5+
// deliberately not installed, so an include of a wolf/Support header here causes the check to fail.
6+
7+
#include <synthrt/Core/SynthUnit.h>
8+
9+
#include <wolf/Api/Linguists/Linguist/1/LinguistApiL1.h>
10+
#include <wolf/Linguist/LinguistContrib.h>
11+
#include <wolf/Session/LinguistSession.h>
12+
13+
int main() {
14+
srt::SynthUnit unit;
15+
wolf::linkLinguistCategory();
16+
const wolf::LinguistSession session(unit);
17+
// No package is loaded, so the return value is irrelevant. The call verifies that the program
18+
// links and that the headers are at the locations the package exports.
19+
(void) session.catalog();
20+
return 0;
21+
}

‎.github/workflows/ci.yml‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Builds and tests wolf on Linux and Windows.
2+
#
3+
# The dependency tree comes from vcpkg through the manifest in scripts/vcpkg-manifest, with the
4+
# in-repo ports in scripts/vcpkg-ports searched before the shared overlay submodule.
5+
name: ci
6+
7+
on:
8+
push:
9+
# The workflow runs on the integration branches as well as on main. Work stays on an
10+
# integration branch for weeks before it reaches main, and a workflow restricted to main does
11+
# not run in that period.
12+
branches: [main, "linguistic-**"]
13+
pull_request:
14+
15+
jobs:
16+
build:
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
include:
21+
- os: ubuntu-24.04
22+
triplet: x64-linux
23+
features: "--x-feature=onnx --x-feature=tests"
24+
options: ""
25+
- os: windows-2022
26+
triplet: x64-windows
27+
features: "--x-feature=onnx --x-feature=tests"
28+
options: ""
29+
runs-on: ${{ matrix.os }}
30+
env:
31+
VCPKG_DEFAULT_TRIPLET: ${{ matrix.triplet }}
32+
steps:
33+
- uses: actions/checkout@v4
34+
with:
35+
submodules: recursive
36+
37+
- name: Install build tools
38+
if: runner.os == 'Linux'
39+
run: |
40+
sudo apt-get update
41+
sudo apt-get install -y ninja-build python3
42+
43+
- name: Set up the MSVC environment
44+
if: runner.os == 'Windows'
45+
uses: ilammy/msvc-dev-cmd@v1
46+
47+
- name: Set up vcpkg
48+
uses: lukka/run-vcpkg@v11
49+
with:
50+
vcpkgDirectory: ${{ runner.temp }}/vcpkg
51+
vcpkgGitCommitId: 1fddddc280dfed63956e15ef74f4321bc6a219c9
52+
runVcpkgInstall: false
53+
54+
- name: Install dependencies
55+
shell: bash
56+
run: |
57+
"${{ runner.temp }}/vcpkg/vcpkg" install \
58+
--x-manifest-root=scripts/vcpkg-manifest \
59+
--x-install-root=build/vcpkg_installed \
60+
${{ matrix.features }}
61+
62+
- name: Configure
63+
shell: bash
64+
run: |
65+
cmake -S . -B build/cmake -G Ninja \
66+
-DCMAKE_BUILD_TYPE=Release \
67+
-DCMAKE_TOOLCHAIN_FILE="${{ runner.temp }}/vcpkg/scripts/buildsystems/vcpkg.cmake" \
68+
-DVCPKG_INSTALLED_DIR="$PWD/build/vcpkg_installed" \
69+
-DVCPKG_MANIFEST_MODE=OFF \
70+
-DWOLF_BUILD_TESTS=ON ${{ matrix.options }}
71+
72+
- name: Build
73+
run: cmake --build build/cmake
74+
75+
- name: Run the lint unit tests
76+
run: python3 -m unittest discover -s scripts -p "test_*.py"
77+
78+
- name: Lint the shipped package
79+
# The test fixtures under src/tests/auto/packages include deliberately broken packages, so
80+
# the lint runs on the package that ships with the repository.
81+
run: python3 scripts/check-declarations.py packages/wolf-lang-zxx
82+
83+
- name: Test
84+
# Data-dependent tests exit with the skip status when their packages are absent.
85+
run: ctest --test-dir build/cmake --output-on-failure
86+
87+
- name: Check the installed CMake package
88+
# The test suite above exercises the code, and this step exercises the installed package
89+
# that a host consumes. The install rules write the targets, the include directories and
90+
# the plugin directory of that package, and no earlier step reads them.
91+
shell: bash
92+
run: |
93+
cmake --install build/cmake --prefix build/install
94+
cmake -S .github/consumer -B build/consumer -G Ninja \
95+
-DCMAKE_BUILD_TYPE=Release \
96+
-DCMAKE_TOOLCHAIN_FILE="${{ runner.temp }}/vcpkg/scripts/buildsystems/vcpkg.cmake" \
97+
-DVCPKG_INSTALLED_DIR="$PWD/build/vcpkg_installed" \
98+
-DVCPKG_MANIFEST_MODE=OFF \
99+
-DCMAKE_PREFIX_PATH="$PWD/build/install"
100+
cmake --build build/consumer

‎README.md‎

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Each entry is a path to a linguist manifest:
2828
"interface": "org.openvpi.wolf.linguist.WolfLinguist",
2929
"level": 1,
3030
"variant": "wolf",
31+
"language": "cmn",
32+
"scheme": "pinyin",
3133
"exports": {
3234
"phonemes": [ "a", "o", "e", "i", "u", "v" ]
3335
},
@@ -41,16 +43,20 @@ Each entry is a path to a linguist manifest:
4143

4244
The `(interface, level, variant)` triple selects a provider through synthrt's interpreter discovery. The provider interprets `exports`, `configuration`, import options, execution factories, validators, and extensions for that contract.
4345

44-
The split follows the one synthrt already uses. `exports` is what the linguist declares about itself. `configuration` contains provider-specific resources. Relative paths resolve against the declaration file's directory.
46+
The split follows the convention of synthrt. `language` and `scheme` are fields that the `linguist` category adds to every declaration and reads before a provider is selected. Together they form the identity of the linguist: an ISO 639-3 language code and the notation in which its pronunciations are written. `exports` contains the declarations of the linguist about itself. `configuration` contains provider-specific resources. Relative paths resolve against the declaration file's directory.
4547

4648
A linguist is referred to like any other contribute:
4749

4850
```
49-
vendor/sample=1.0:linguist/mandarin # fully qualified
50-
vendor/sample:linguist/mandarin # version resolved from dependencies
51-
:linguist/mandarin # within the referring package
51+
vendor/sample:linguist/mandarin # a module in a resolved direct dependency
52+
:linguist/mandarin # a module in the referring package
5253
```
5354

55+
A reference carries no version. The version of `vendor/sample` to which the reference binds is
56+
determined by the entry for that package in the `dependencies` of the referring package. The
57+
package id must appear in `dependencies`, and a reference to a module in the referring package
58+
must use the leading `:` form.
59+
5460
## Providing a linguist
5561

5662
Implement `wolf::LinguistProvider` and expose it through a `wolf::LinguistProviderPlugin`. The provider owns all contract-specific behavior, including import options, execution factories, validators, and spec extensions:
@@ -78,15 +84,75 @@ auto spec = package->contribution("linguist", "mandarin")
7884

7985
A `SynthUnit` reads the list of registered categories once, when it is constructed. wolf registers `linguist` before `main`, so any unit built afterwards has it. A plugin could not do the same: plugins load lazily *through* a unit, so by the time one runs its static initializers, that unit has already built its categories. Contributing a category is something a linked library does.
8086

87+
Linking alone is not sufficient. A linker that drops unreferenced libraries, as ELF linkers do under `--as-needed` and the MSVC linker does for every import library, drops wolf from a host that uses only the category and references no wolf symbol. Such a host calls `wolf::linkLinguistCategory()` once before it constructs a unit. The function has an empty body; the reference to it keeps the library on the link line.
88+
89+
## Versioning and ABI
90+
91+
The current version is 0.1.0.0. **No ABI guarantee applies before version 1.0.** Most types that a
92+
host uses are value types in public headers, namely `LanguageStatus`, `LanguageEntry`,
93+
`SingerEntry`, `SingerRef` and every payload under `Api/`. A new field changes the size of such a
94+
type, and a new virtual function changes the vtable of a contract interface such as
95+
`WolfPipelineExtension`. Mixing objects compiled against two versions of these headers is undefined
96+
behavior rather than a link error, and therefore produces no diagnostic.
97+
98+
The public headers are those under `Api/`, `Linguist/` and `Session/`, and the package installs no
99+
other header. `wolf/Support` contains the internal helpers of the plugins in this repository and is
100+
not installed. Its stateless helpers are compiled into the static library `wolfsupport` with hidden
101+
visibility; libwolf and each plugin link that library privately, and no binary exports its
102+
symbols. The two components that must exist once per process, the resource cache and the log
103+
category, remain in libwolf and are exported under `WOLF_INTERNAL_EXPORT` for use by the plugins
104+
of this repository. These symbols are not public API, carry no ABI guarantee at any version, and
105+
must not be linked by a third-party provider. A provider built outside this repository depends
106+
only on the installed headers and on synthrt.
107+
108+
**A host must be rebuilt whenever the version of wolf changes.** The version is raised in the same
109+
commit as every ABI-breaking change, so a changed version number indicates that a rebuild is
110+
required.
111+
The installed CMake package is written with `ExactVersion` compatibility, so `find_package(wolf)`
112+
with a version accepts only that exact version. `wolf::LinguistSession` is the only public type
113+
held behind a pimpl, and its layout is therefore stable across versions.
114+
81115
## Requirements
82116

83-
CMake 3.19 or later.
117+
CMake 3.19 or later, and a checkout of the vcpkg overlay submodule:
118+
119+
```sh
120+
git submodule update --init
121+
```
122+
123+
Every dependency, including synthrt, is installed through vcpkg from the manifest in
124+
`scripts/vcpkg-manifest`. Ports resolve through two overlays in order: `scripts/vcpkg-ports` in
125+
this repository, which holds the `synthrt-main` and `wolf-lang-packages` ports, and then the shared
126+
`scripts/vcpkg` submodule.
84127

85-
+ [synthrt](https://github.com/diffscope/synthrt)
128+
+ [synthrt](https://github.com/diffscope/synthrt), through the in-repo `synthrt-main` port. The
129+
port pins the synthrt branch `onnxruntime-builds-uptake`, which is based on the synthrt main
130+
line. The `synthrt` port of the shared overlay pins the refactor line and is not used here.
86131
+ [stdcorelib](https://github.com/SineStriker/stdcorelib)
87132
+ [qmsetup](https://github.com/stdware/qmsetup)
88-
89-
Boost.Test is needed only to build the tests.
133+
+ [BLAKE3](https://github.com/BLAKE3-team/BLAKE3), for content hashing in the resource cache.
134+
+ [RE2](https://github.com/google/re2), for word classification patterns. The shipped language
135+
packages use Unicode property classes such as `\p{Han}`, which `std::regex` does not support.
136+
+ [cpp-pinyin](https://github.com/wolfgitpr/cpp-pinyin), the G2P engine for Mandarin and Cantonese.
137+
138+
Boost.Test is required only to build the tests.
139+
140+
Two variants depend on an external backend and are built only if that backend is present, in the
141+
same way as synthrt omits its ONNX driver if ONNX Runtime is absent:
142+
143+
+ `multig2p-onnx`, the shared model backend, requires dsinfer and its ONNX Runtime driver. The
144+
`onnx` feature (`--x-feature=onnx`) adds `synthrt-main[onnx]`; ONNX Runtime itself comes from
145+
the `onnxruntime-builds` port of the shared overlay (DirectML on Windows, CPU on other platforms,
146+
no CUDA).
147+
+ `lua`, the scripted S2P and Onset variants, requires LuaJIT, which the manifest lists as a plain
148+
dependency.
149+
150+
`-DWOLF_DISABLE_DSINFER=ON` and `-DWOLF_DISABLE_LUAJIT=ON` exclude either backend even if it is
151+
installed. A build without either backend still loads every package that the shipped plugins
152+
support. The test stub always declares a `stub-miscount` variant for the fixtures that violate the
153+
provider contract deliberately, and declares `multig2p-onnx` only in a build without dsinfer, so
154+
that the loading tests also run in that build. The shipped minimal build contains no stub, and a
155+
package that requires the model backend fails to load in it.
90156

91157
## Setup Environment
92158

@@ -100,23 +166,61 @@ bootstrap-vcpkg.bat # or ./bootstrap-vcpkg.sh
100166
vcpkg install --x-manifest-root=../scripts/vcpkg-manifest --x-install-root=./installed
101167
```
102168

103-
Add `--x-feature=tests` to bring in Boost.Test.
169+
The manifest defines three optional features:
104170

105-
### Build
171+
| Feature | Effect |
172+
| :-- | :-- |
173+
| `onnx` | Builds dsinfer and its ONNX Runtime driver through `synthrt-main[onnx]`, which enables the `multig2p-onnx` variant. |
174+
| `lang-packages` | Installs the default suites of the published language packages (`cmn`, `jpn`, `yue`, `zxx` and the backends they require) through the `wolf-lang-packages` port, for use by the tests. |
175+
| `tests` | Adds Boost.Test. |
106176

107-
synthrt is not a vcpkg dependency here. Build and install it separately, then point at that install:
177+
> The pin of the overlay submodule must match the pin that synthrt uses. Two revisions of
178+
> `stdcorelib-plugin` share the version string `0.1.0.0#1` but fetch different upstream refs, and
179+
> they name the CMake helper differently (`stdc_add_plugin_metadata` and
180+
> `stdc_add_plugin_manifest`). A mismatched pin therefore fails at configure time.
181+
182+
### Build
108183

109184
```sh
110185
cmake -B build -G Ninja \
111-
-DCMAKE_PREFIX_PATH=<synthrt install dir> \
186+
-DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake \
187+
-DVCPKG_INSTALLED_DIR=<install root passed above> \
188+
-DVCPKG_MANIFEST_MODE=OFF \
112189
-DCMAKE_INSTALL_PREFIX=<dir> \
113-
-DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake \
114190
-DCMAKE_BUILD_TYPE=Release
115191

116192
cmake --build build --target all
117193
cmake --build build --target install
118194
```
119195

196+
### Tests
197+
198+
`-DWOLF_BUILD_TESTS=ON` builds the tests and registers them with CTest; `ctest --test-dir build`
199+
runs them. A full build registers 17 test binaries. A build without LuaJIT or without dsinfer
200+
omits `test_LuaVariants` or `test_MultiG2P` respectively.
201+
202+
The tests that load real language packages read them from the location given by the cache
203+
variable `WOLF_LANG_PACKAGES_SOURCE`, which names a directory of unpacked packages. If the variable
204+
is empty, the build uses the tree installed by the `wolf-lang-packages` port (the `lang-packages`
205+
feature). The current release is `lang-v0.1.2.0`, which contains 15 packages. The host flow test
206+
additionally reads the voicebank fixture that `scripts/make-voicebank-fixture.py` generates, from
207+
the cache variable `WOLF_VOICEBANK_FIXTURE_SOURCE`. A test whose data is absent exits with status
208+
77, which CTest reports as skipped.
209+
210+
CI (`.github/workflows/ci.yml`) builds with the `onnx` and `tests` features on Linux and Windows,
211+
runs the self tests of the declaration lint and the lint on `packages/wolf-lang-zxx`, runs CTest,
212+
and then installs wolf and builds `.github/consumer` against the installed package. The consumer
213+
check verifies the `wolf::wolf` target, `WOLF_PLUGINS_DIR` and the absence of Support headers.
214+
215+
### Installed Layout
216+
217+
The package installs the headers under `include/`, the library, and the CMake package config. The
218+
plugins are installed under `lib/plugins/wolf/<category>/<name>/`, where `<category>` is
219+
`inferenceinterpreters` or `linguistproviders`; the build tree uses the same layout. The config
220+
defines `WOLF_PLUGINS_DIR` as the plugin directory of the release tree. If a `debug/` prefix tree
221+
exists, as in a vcpkg installation, the config also defines `WOLF_PLUGINS_DIR_DEBUG`. A host passes
222+
each category directory to `SynthUnit::setPluginPaths`.
223+
120224
## How to Use
121225

122226
```cmake

0 commit comments

Comments
 (0)