Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

build: Meta-issue for follow-ups to initial CMake merge (#1113) #1235

Open
12 of 14 tasks
hebasto opened this issue Mar 10, 2023 · 23 comments
Open
12 of 14 tasks

build: Meta-issue for follow-ups to initial CMake merge (#1113) #1235

hebasto opened this issue Mar 10, 2023 · 23 comments
Labels
build user-documentation user-facing documentation

Comments

@hebasto
Copy link
Member

hebasto commented Mar 10, 2023

Issues that have been (re)discovered in #1113 but have not been addressed there:

Affecting both build systems:

Affecting only CMake:

@theuni
Copy link
Contributor

theuni commented Mar 10, 2023

Another one for me: my mingw32 build with ctests fails due to missing valgrind:

Valgrind .............................. OFF
...
ctime_tests ......................... ON
...

/home/cory/dev/secp256k1/src/ctime_tests.c:14:4: error: #error "This tool cannot be compiled without memory-checking interface (valgrind or msan)"
14 | # error "This tool cannot be compiled without memory-checking interface (valgrind or msan)"

Should ctime_tests be a dependent option, depending on valgrind?

@hebasto
Copy link
Member Author

hebasto commented Mar 10, 2023

@theuni

Another one for me: my mingw32 build with ctests fails due to missing valgrind:

Valgrind .............................. OFF
...
ctime_tests ......................... ON
...

/home/cory/dev/secp256k1/src/ctime_tests.c:14:4: error: #error "This tool cannot be compiled without memory-checking interface (valgrind or msan)"
14 | # error "This tool cannot be compiled without memory-checking interface (valgrind or msan)"

Do I understand correctly, that option -DSECP256K1_BUILD_CTIME_TESTS=ON is forced, right?

Should ctime_tests be a dependent option, depending on valgrind?

I don't think so, as MSan is another option to use for building the ctime_tests.

@theuni
Copy link
Contributor

theuni commented Mar 10, 2023

Do I understand correctly, that option -DSECP256K1_BUILD_CTIME_TESTS=ON is forced, right?

Yes, it was leftover on my cmdline from a previous build.

But on my win32 build with no valid runtime (valgrind/msan/whatever) I believe cmake should throw an error telling me "SECP256K1_BUILD_CTIME_TESTS can't be set without valgrind/msan".

@sipa
Copy link
Contributor

sipa commented Mar 10, 2023

Currently the build system (neither autotools nor cmake, I think) has any knowledge of whether msan is enabled (in CI it's done by passing explicit CFLAGS), so it has to conservatively assume that it may be enabled.

Bringing msan (and maybe sanitizers in general?) into scope of the configure scripts may be useful, and if that's the case, the detection could be made more precise too (without valgrind or msan, no ctime tests).

@hebasto
Copy link
Member Author

hebasto commented Mar 10, 2023

But on my win32 build with no valid runtime (valgrind/msan/whatever) I believe cmake should throw an error telling me "SECP256K1_BUILD_CTIME_TESTS can't be set without valgrind/msan".

Sounds like a nice feature for both build systems--CMake-based one and Autotools-based other.

@hebasto
Copy link
Member Author

hebasto commented Mar 12, 2023

The current Autotools-based build system provides a user with an option to override compiler flags using the CFLAGS variable.

That is not the case in the new CMake-based build system for now. The order of compiler flags is as follows:

Additionally, CMAKE_C_FLAGS can be initialized using CMake's builtin defaults (for example, /DWIN32 /D_WINDOWS for MSVC) or by the CFLAGS environment variable.

If a user sets the latter, its value is consumed first without a chance to override flags set by the build system.

@hebasto
Copy link
Member Author

hebasto commented Mar 13, 2023

GCC:

cc1: warning: command-line option ‘-fvisibility-inlines-hidden’ is valid for C++/ObjC++ but not for C

So removing this task from the list.

@hebasto
Copy link
Member Author

hebasto commented Mar 14, 2023

During reviewing of #1113, there was a suggestion:

And expressing Coverage as build type in CMake seems to use build types in the right way.

Later, #1188 introduced a new target noverify_tests and made the other target tests dependent on whether the build system is configured for coverage analysis or not.

Unfortunately, CMake has no any straightforward way to skip targets in certain configuration.
My draft attempt uses the EXCLUDE_FROM_DEFAULT_BUILD_COVERAGE target property and generator expressions in the EXCLUDE_FROM_ALL target property which, in turn, requires CMake 3.19+. Needless to say, such an implementation increases the complexity of the code.

As @real-or-random rightly noted, the current code works only for single-config generators.

Suggesting to re-introduce a trivial option(SECP256K1_COVERAGE OFF) which enforces the only available build configuration "Coverage".

@hebasto
Copy link
Member Author

hebasto commented Mar 18, 2023

In case it would be interested for other developers, here is my preset for cross-building from Linux (I use Ubuntu 22.04) to macOS:

{
  "version": 1,
  "configurePresets": [
    {
      "name": "macos-x86_64",
      "displayName": "Cross-compiling for macOS x86_64",
      "generator": "Unix Makefiles",
      "binaryDir": "${sourceDir}/build",
      "cacheVariables": {
        "CMAKE_SYSTEM_NAME": "Darwin",
        "CMAKE_SYSTEM_PROCESSOR": "x86_64",
        "CMAKE_C_COMPILER": "/usr/bin/clang-14;--target=x86_64-apple-darwin",
        "CMAKE_OSX_SYSROOT": "/home/hebasto/SDKs/Xcode-12.2-12B45b-extracted-SDK-with-libcxx-headers"
      },
      "environment": {
        "LDFLAGS": "-fuse-ld=/usr/bin/ld64.lld-14 -mmacosx-version-min=10.15 -mlinker-version=609"
      },
      "warnings": {
        "dev": true,
        "uninitialized": true
      }
    }
  ]
}

The llvm and lld packages are required to be installed.

Regarding Xcode-12.2-12B45b-extracted-SDK-with-libcxx-headers see https://github.com/bitcoin/bitcoin/blob/master/contrib/macdeploy/README.md.

@hebasto
Copy link
Member Author

hebasto commented Mar 22, 2023

... I figured out that we could set COMPILE_FLAGS instead, which supports generator expressions. https://cmake.org/cmake/help/latest/prop_sf/COMPILE_FLAGS.html But if you ask me, that belongs exactly to the category of "making things more CMake-ish", which should not hold up this PR.

Revisiting this issue as we now have the minimum required CMake 3.13.

Different cases, when we set compiler flags, are as follows:

  1. Specific to a build configuration. We use dedicated CMAKE_C_FLAGS_<CONFIG> variables for them.
  2. Global flags to request or suppress warnings and errors. We use the COMPILE_OPTIONS top-directory property to handles them which is a well known CMake practice for such cases. Please note that cmake: Improve and document compiler flag checks #1240 suggests to use add_compile_options() command instead of direct manipulation of the COMPILE_OPTIONS property.
  3. For target-specific flags we use the target_compile_options() which is the best CMake practice.

I cannot see what we can do about the original issue, so removing it from the task list. Let me know if I should revert it back.

@real-or-random
Copy link
Contributor

During reviewing of #1113, there was a suggestion:

And expressing Coverage as build type in CMake seems to use build types in the right way.

Later, #1188 introduced a new target noverify_tests and made the other target tests dependent on whether the build system is configured for coverage analysis or not.

Unfortunately, CMake has no any straightforward way to skip targets in certain configuration. My draft attempt uses the EXCLUDE_FROM_DEFAULT_BUILD_COVERAGE target property and generator expressions in the EXCLUDE_FROM_ALL target property which, in turn, requires CMake 3.19+. Needless to say, such an implementation increases the complexity of the code.

As @real-or-random rightly noted, the current code works only for single-config generators.

Suggesting to re-introduce a trivial option(SECP256K1_COVERAGE OFF) which enforces the only available build configuration "Coverage".

What about using a CMake preset for Coverage builds, which just sets the right values? I think this could be a bit nicer than an option SECP256K1_COVERAGE but we'd need to try.

@hebasto
Copy link
Member Author

hebasto commented Mar 28, 2023

What about using a CMake preset for Coverage builds, which just sets the right values? I think this could be a bit nicer than an option SECP256K1_COVERAGE but we'd need to try.

Done in #1251.

@hebasto
Copy link
Member Author

hebasto commented Apr 13, 2023

From the recent discussion in #1274:

I think that's a reason to keep the macOS Valgrind support in the build system (plus it works on x86_64).

Removing this task from the list. Please let me know if you would like me to revert it back.

real-or-random added a commit to real-or-random/secp256k1 that referenced this issue Apr 20, 2023
To use, invoke `cmake` with argument `--preset dev-mode`.

Solves one item in bitcoin-core#1235.

One disadvantage over `./configure --enable-dev-mode` is that CMake
does not provide a way to "hide" presets from users. That is,
`cmake --list-presets` will list dev-mode, and it will also appear
in `cmake-gui`, even though it's not selectable there due to bug
https://gitlab.kitware.com/cmake/cmake/-/issues/23341. (So in our
case, that's probably rather a feature than a bug.)

We curently use version 3 presets which require CMake 3.21+.
Unfortunately, CMake versions before 3.19 may ignore the `--preset`
argument silently. So if the preset is not picked up, make sure you
have a recent enough CMake version.

More unfortunately, we can't even spell this warning out in
CMakePresets.json because CMake does not support officially support
comments in JSON, see
 - https://gitlab.kitware.com/cmake/cmake/-/issues/21858
 - https://gitlab.kitware.com/cmake/cmake/-/merge_requests/5853 .
We could use a hack hinted at in
https://gitlab.kitware.com/cmake/cmake/-/issues/21858#note_908543
but that's risky, because it could simply break for future versions,
and we probably want to use presets not only for dev mode.
real-or-random added a commit that referenced this issue Apr 27, 2023
… up to 3.13

a273d74 cmake: Improve version comparison (Hennadii Stepanov)
6a58b48 cmake: Use `if(... IN_LIST ...)` command (Hennadii Stepanov)
2445808 cmake: Use dedicated `GENERATOR_IS_MULTI_CONFIG` property (Hennadii Stepanov)
9f8703e cmake: Use dedicated `CMAKE_HOST_APPLE` variable (Hennadii Stepanov)
8c20170 cmake: Use recommended `add_compile_definitions` command (Hennadii Stepanov)
04d4cc0 cmake: Add `DESCRIPTION` and `HOMEPAGE_URL` options to `project` command (Hennadii Stepanov)
8a8b653 cmake: Use `SameMinorVersion` compatibility mode (Hennadii Stepanov)

Pull request description:

  This PR:
  - resolves two items from #1235, including a bugfix with package version compatibility
  - includes other improvements which have become available for CMake 3.13+.

  To test the `GENERATOR_IS_MULTI_CONFIG` property on Linux, one can use the "[Ninja Multi-Config](https://cmake.org/cmake/help/latest/generator/Ninja%20Multi-Config.html)" generator:
  ```sh
  cmake -S . -B build -G "Ninja Multi-Config"
  ```

ACKs for top commit:
  real-or-random:
    ACK a273d74
  theuni:
    ACK a273d74

Tree-SHA512: f31c4f0f30bf368303e70ab8952cde5cc8c70a5e79a04f879abcbee3d0a8d8c598379fb38f5142cb1f8ff5f9dcfc8b8eb4c13c975a1d05fdcc92d9c805a59d9a
real-or-random added a commit that referenced this issue May 12, 2023
…orted

c6bb29b build: Rename `64bit` to `x86_64` (Hennadii Stepanov)
0324645 autotools: Add `SECP_ARM32_ASM_CHECK` macro (Hennadii Stepanov)
ed4ba23 cmake: Add `check_arm32_assembly` function (Hennadii Stepanov)
e5cf4bf build: Rename `arm` to `arm32` (Hennadii Stepanov)

Pull request description:

  Closes #1034.

  Solves one item in #1235.

ACKs for top commit:
  real-or-random:
    ACK c6bb29b tested on x86_64 but not on ARM

Tree-SHA512: c3615a18cfa30bb2cc53be18c09ccab08fc800b84444d8c6b333347b4db039a3981da61e7da5086dd9f4472838d7c031d554be9ddc7c435ba906852bba593982
dderjoel pushed a commit to dderjoel/secp256k1 that referenced this issue May 23, 2023
To use, invoke `cmake` with argument `--preset dev-mode`.

Solves one item in bitcoin-core#1235.

One disadvantage over `./configure --enable-dev-mode` is that CMake
does not provide a way to "hide" presets from users. That is,
`cmake --list-presets` will list dev-mode, and it will also appear
in `cmake-gui`, even though it's not selectable there due to bug
https://gitlab.kitware.com/cmake/cmake/-/issues/23341. (So in our
case, that's probably rather a feature than a bug.)

We curently use version 3 presets which require CMake 3.21+.
Unfortunately, CMake versions before 3.19 may ignore the `--preset`
argument silently. So if the preset is not picked up, make sure you
have a recent enough CMake version.

More unfortunately, we can't even spell this warning out in
CMakePresets.json because CMake does not support officially support
comments in JSON, see
 - https://gitlab.kitware.com/cmake/cmake/-/issues/21858
 - https://gitlab.kitware.com/cmake/cmake/-/merge_requests/5853 .
We could use a hack hinted at in
https://gitlab.kitware.com/cmake/cmake/-/issues/21858#note_908543
but that's risky, because it could simply break for future versions,
and we probably want to use presets not only for dev mode.
real-or-random added a commit that referenced this issue May 26, 2023
1549db0 build: Level up MSVC warnings (Hennadii Stepanov)

Pull request description:

  Solves one item in #1235.

ACKs for top commit:
  sipa:
    utACK 1549db0
  real-or-random:
    ACK 1549db0

Tree-SHA512: 769386f734709537291ddee45c7fbee501185d3eebe9daa117d36e13e8504fabd1127857bc661a751fdf63f2eee1e7e9507121bdb020c97eb87b8758cb0879f8
real-or-random added a commit that referenced this issue May 31, 2023
e83801f test: Warn if both `VERIFY` and `COVERAGE` are defined (Hennadii Stepanov)

Pull request description:

  Solves one item in #1235.

  Also see: #1113 (comment).

ACKs for top commit:
  sipa:
    utACK e83801f
  real-or-random:
    ACK e83801f

Tree-SHA512: 25e10a09ba2c3585148becd06f2a03d85306208bda333827c9ba73eb7fd94ad15536f10daf1b335703e5cb0539584f001501ce9c578f478ff1ebc1051aefde7d
@hebasto
Copy link
Member Author

hebasto commented Jun 23, 2023

  • Consider setting CMake's default of /MD for MSVC also in autotools builds.

#1113 (comment):

Nevertheless, as for reasonable defaults, my impression is that /MD has fewer issues and is what most users would expect, in particular from a library (see also jedisct1/libsodium#1215 ), and I guess there's a reason why the CMake default is MultiThreadedDLL (where the DLL part corresponds to /MD)

@sipsorcery What do you think about that?

@sipsorcery
Copy link
Member

Without looking into which dll or exe it was applying to /MD would be a BIG change for the Windows build. The fact that the final bitcoind is a statically linked exe cascades down to most, if not all, of the dependencies.

In my experience /MD (dynamically linked) is the default but then so are dynamically linked exe's. I suspect someone decided to make bitcoind statically linked to avoid dll hell and/or a dependency introducing a vulnerability. Either way it is currently statically linked and again it would be a BIG change to make it not so.

If bitcoind on Windows is going to stay statically linked then I think it would end up being a lot of fighting to change any of the artifacts to /MD.

/MT and /MTd get my vote.

@real-or-random
Copy link
Contributor

@sipsorcery Thanks for commenting.

Perhaps there's a slight misunderstanding here. I think there are two separate issues:

  1. How should the bitcoind build libsecp256k1?
  2. What is the default for libsecp256k1?

The latter is our question. libsecp256k1 has many other in the ecosystem users beside bitcoind. (In fact, that's the reason why it is a separate library.) And we would like to set a reasonable default for them.

In my experience /MD (dynamically linked) is the default but then so are dynamically linked exe's.

This makes me think that we should make /MD the default. Just because that's most common on Windows.

I suspect someone decided to make bitcoind statically linked to avoid dll hell and/or a dependency introducing a vulnerability. Either way it is currently statically linked and again it would be a BIG change to make it not so.
If bitcoind on Windows is going to stay statically linked then I think it would end up being a lot of fighting to change any of the artifacts to /MD.

Yes, I see that this will be a mess. But when building libsecp256k1 for bitcoind, we can override anything we want and simply set /MT or /MTd.

@hebasto
Copy link
Member Author

hebasto commented Jun 27, 2023

On the current master branch, the user is able to specify the MSVC runtime library by providing the CMAKE_MSVC_RUNTIME_LIBRARY variable at the configuration stage. I'm not sure how the caching of it might improve the user experience.

Going to skip this goal for now.

cc @real-or-random

@hebasto
Copy link
Member Author

hebasto commented Jun 27, 2023

I think it's OK to leave the defaults as they are for the following reason: After introducing the CMake-based build system, using MSVC with Autotools can be reasonably considered for testing purposes only. Additionally, it is beneficial to test Windows binaries with different MSVC runtime library options.

Anyway, both routines, in CMake and in Autotools, allow the user to specify the MSVC runtime library explicitly.

Removing this task from the list. Please let me know if you would like me to revert it back.

@hebasto
Copy link
Member Author

hebasto commented Jun 27, 2023

CMake continuously introduces new abstractions for different aspects of the building process. Another example is DLL_NAME_WITH_SOVERSION: #1270 (comment).

I don't think it would be beneficial to have two code paths depending on the actual CMake version. It would introduce additional complexity to the code and potentially lead to deviations in the user experience.

We might review such new CMake's features in the future when we are about to bump the minimum required CMale version.

It seems unreasonable to have a tracking issue for such features.

Removing this task from the list. Please let me know if you would like me to revert it back.

@real-or-random
Copy link
Contributor

I think it's OK to leave the defaults as they are for the following reason: After introducing the CMake-based build system, using MSVC with Autotools can be reasonably considered for testing purposes only.

That's a good point, I buy that argument.

@real-or-random
Copy link
Contributor

I don't think it would be beneficial to have two code paths depending on the actual CMake version. It would introduce additional complexity to the code and potentially lead to deviations in the user experience.

I agree in general. Though there may be exceptions on a case-by-case basis. We have a few places where we check the value of CMAKE_VERSION, and I think they all make sense.

We might review such new CMake's features in the future when we are about to bump the minimum required CMale version.

You mean when we bump, we should just read CMake's changelog? That makes sense to me.

Another possibility is that we add a comment to the affected code, e.g., TODO: Consider DLL_NAME_WITH_SOVERSION when bumping to CMake x.y. I would ACK such a comment, but yeah, just reading the changelog seems good enough, too.

@hebasto
Copy link
Member Author

hebasto commented Jun 28, 2023

I don't think it would be beneficial to have two code paths depending on the actual CMake version. It would introduce additional complexity to the code and potentially lead to deviations in the user experience.

I agree in general. Though there may be exceptions on a case-by-case basis. We have a few places where we check the value of CMAKE_VERSION, and I think they all make sense.

Agree. That was my idea that I failed to express clearly and fully :)

We might review such new CMake's features in the future when we are about to bump the minimum required CMale version.

You mean when we bump, we should just read CMake's changelog?

Yes.

Another possibility is that we add a comment to the affected code, e.g., TODO: Consider DLL_NAME_WITH_SOVERSION when bumping to CMake x.y.

Once we start adding such comments, we have to do some work at every CMake release.

I would ACK such a comment, but yeah, just reading the changelog seems good enough, too.

I'd prefer the latter :)

real-or-random added a commit to real-or-random/secp256k1 that referenced this issue Jun 28, 2023
real-or-random added a commit to real-or-random/secp256k1 that referenced this issue Jun 28, 2023
real-or-random added a commit that referenced this issue Jul 3, 2023
…s (attempt 3)

c6cd2b1 ci: Add task for static library on Windows + CMake (Hennadii Stepanov)
020bf69 build: Add extensive docs on visibility issues (Tim Ruffing)
0196e8a build: Introduce `SECP256k1_DLL_EXPORT` macro (Hennadii Stepanov)
9f1b190 refactor: Replace `SECP256K1_API_VAR` with `SECP256K1_API` (Hennadii Stepanov)
ae9db95 build: Introduce `SECP256K1_STATIC` macro for Windows users (Hennadii Stepanov)

Pull request description:

  Previous attempts:
  - #1346
  - #1362

  The result is as follows:
  1. Simple, concise and extensively documented code.
  2. Explicitly documented use cases with no ambiguities.
  3. No workarounds for linker warnings.
  4. Solves one item in #1235.

ACKs for top commit:
  real-or-random:
    utACK c6cd2b1

Tree-SHA512: d58694452d630aefbd047916033249891bc726b7475433aaaa7c3ea2a07ded8f185a598385b67c2ee3440ec5904ff9d9452c97b0961d84dcb2eb2cf46caa171e
real-or-random added a commit that referenced this issue Jan 17, 2024
42f8c51 cmake: Add `SECP256K1_LATE_CFLAGS` configure option (Hennadii Stepanov)

Pull request description:

  This PR enables users to override compiler flags that have been set by the CMake-based build system, such as warning flags.

  The Autotools-based build system has the same feature out-of-the-box.

  See more details [here](#1235 (comment)).

  Here are some examples of the new option usage:
  ```
  cmake -S . -B build -DSECP256K1_LATE_CFLAGS="-Wno-extra -Wlong-long"
  ```

  ```
  cmake -S . -B build -DSECP256K1_BUILD_EXAMPLES=ON -DSECP256K1_LATE_CFLAGS=-O1
  cmake --build build
  ...
  In function ‘secp256k1_ecmult_strauss_wnaf’,
      inlined from ‘secp256k1_ecmult’ at /home/hebasto/git/secp256k1/src/ecmult_impl.h:353:5:
  /home/hebasto/git/secp256k1/src/ecmult_impl.h:291:5: warning: ‘aux’ may be used uninitialized [-Wmaybe-uninitialized]
    291 |     secp256k1_ge_table_set_globalz(ECMULT_TABLE_SIZE(WINDOW_A) * no, state->pre_a, state->aux);
        |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  In file included from /home/hebasto/git/secp256k1/src/secp256k1.c:29:
  /home/hebasto/git/secp256k1/src/ecmult_impl.h: In function ‘secp256k1_ecmult’:
  /home/hebasto/git/secp256k1/src/group_impl.h:174:13: note: by argument 3 of type ‘const secp256k1_fe *’ to ‘secp256k1_ge_table_set_globalz’ declared here
    174 | static void secp256k1_ge_table_set_globalz(size_t len, secp256k1_ge *a, const secp256k1_fe *zr) {
        |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  In file included from /home/hebasto/git/secp256k1/src/secp256k1.c:30:
  /home/hebasto/git/secp256k1/src/ecmult_impl.h:345:18: note: ‘aux’ declared here
    345 |     secp256k1_fe aux[ECMULT_TABLE_SIZE(WINDOW_A)];
        |                  ^~~
  ...
  ```

  Please note that in the last case providing `env CFLAGS=-O1` or `-DCMAKE_C_FLAGS=-O1` won't work.

ACKs for top commit:
  real-or-random:
    ACK 42f8c51

Tree-SHA512: 2b152e420a4a8ffd5f67857de03ae5ba9f2223e535ac01a867c1025e0619180d8255fdd1e5fb8279b290f0a1c96bcc874043ef968fcd99b1ff4e13041a91b1e1
real-or-random added a commit to real-or-random/secp256k1 that referenced this issue Jan 17, 2024
theStack pushed a commit to theStack/secp256k1 that referenced this issue Jan 24, 2024
@hebasto
Copy link
Member Author

hebasto commented Mar 22, 2024

The recommended practice is to treat a project name and target names as independent concepts.

From the book, section 4.6:

Target names need not be related to the project name. While they are sometimes the same, the two
things are separate concepts. Changing one shouldn’t imply that the other must also change. Project
and target names should rarely change anyway, since doing so would break any downstream
consumer that relied on the existing names. Therefore, set the project name directly rather than via
a variable. Choose a target name according to what the target does rather than the project it is part
of. Assume the project will eventually need to define more than one target. These practices
reinforce better habits, which will be important when working on more complex, multi-target
projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
build user-documentation user-facing documentation
Projects
None yet
Development

No branches or pull requests

5 participants