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

Standardize on cargo-c for building rustls-ffi, CMake for building test programs #493

Merged
merged 23 commits into from
Dec 12, 2024

Conversation

cpu
Copy link
Member

@cpu cpu commented Nov 26, 2024

I recommend reviewing this branch commit-by-commit. There are two main goals:

  1. Standardizing cargo-c for building a rustls-ffi library to use as a consumer.
  2. Standardizing cmake for building the client/server examples, and for driving dev. tools (formatting/running integration tests/etc).

Standardizing on cargo-c

We introduced support for building a static or dynamic rustls-ffi library using cargo-c back in #274. Since then it's been used successfully by several of our downstream packagers (archlinux, homebrew, gentoo, nixpkgs, etc). We're also seeing downstream projects like curl working around missing .pc files for the Makefile build.

I propose we standardize on cargo c as the preferred way to build and install the library both in our docs and CI. It does an excellent job of providing a convenient way to both build and install the library for both static and dynamic linking, with pkg-config consideration, on all of our supported OSes, without needing to get into the nitty gritty of rustc. We benefit from being able to focus on rustls-ffi instead of the minutiae of building native libraries in a variety of tricky contexts & downstream distributors and users can benefit from a consistent approach that matches other Rust software using cargo-c.

Specifically, cargo-c:

  • Is very easy to use, and avoids the need to specify flags like --crate-type=cdylib or to remember to install the header file manually.
  • Frees us from having to maintain a manual Makefile installation process that didn't support Windows.
  • Supports both dynamic linking and static linking, where our Makefile only supported the latter.
  • Supports pkg-config out of the box.
  • Is easy to install (via system package, cargo, or downloading pre-built binaries as we do in CI).

Notably standardizing on cargo c does not require it. Adventurous users are free to find the right incantations to build the library with standard rust tooling. The only caveat is that they're on their own for determining the right arguments for their platform/linking type, installing the header file, generating .pc files, etc etc, but the library will build without cargo c.

Standardizing on CMake

Previously the GNU Makefile had three roles for Linux/MacOS systems:

  • building a rustls-ffi.a static library, and installing it and the rustls.h header file into the correct location (proposed to be replaced by cargo-c, per above).
  • building our client and server example C applications.
  • offering helpful shortcuts for development tasks (code formatting, running tests, etc)

In parallel we maintained a CMake based system in order to provide a better integration story for Windows and MSVC toolchains, but did not offer the development task helpers.

In addition to the duplication of work, maintaining the Makefile feels like it's starting to hit a complexity wall. We're shimming in new feature flags as environment variables but there's no good way for a consumer to list the existing options or intuit their values without reading the makefile code. It's also difficult to make complex logical expressions; instead you must nest each conditional. In practice I've also found IDE integration poor: CLion does very well with CMake/Rust projects and not so well with GNU Makefile projects. We could reach for something like GNU autotools, but this is the path to madness and we're still stuck with CMake for Windows.

I propose we bite the bullet and standardize on CMake. It's a mildly unpleasant build system with a bit of a learning curve but we're stuck with it for Windows and so we might as well make the most of its cross-platform abilities. The extra power is beneficial for expressing our build options and more complex logic like ("FIPS and dynamic linking is ok, but only on UNIX"). It's also helpful having a standardized way to list existing options/descriptions for a better user experience.

Notably cmake is only required for building the example binaries, where you also need a C compiler. In this context it feels very reasonable to request cmake as a prerequisite. Users only interested in the rustls-ffi library only require cargo, and cargo-c.

Resolves #390 where there's some additional context/discussion.

cpu added 3 commits November 25, 2024 12:36
Commits the result of running gersemi[0] on our CMake files:
```
gersemi -i CMakeLists.txt tests/CMakeLists.txt
```

Based on a short survey it appears Gersemi is the most active/well
supported CMake formatter.

The formatting might not be quite as nice as doing it by hand, but it is
consistent and enforceable in CI.

[0]: https://github.com/blankspruce/gersemi
Previously we used the `set` cmake command[0] to populate cache string
variables for two boolean options: `CERT_COMPRESSION` and `FIPS`.

The `option` cmake command[1] is exactly for this purpose:
> Provide a boolean option that the user can optionally select.

Use it instead so we get better handling of unknown values for free,
removing the more brittle str comparison to "true".

[0]: https://cmake.org/cmake/help/latest/command/set.html#command:set
[1]: https://cmake.org/cmake/help/latest/command/option.html#option
The top level CMakeLists.txt was getting pretty messy. Let's make it
easier to digest by splitting it up into:

* `cmake/options.cmake` - for build settings related to options.
* `cmake/rust.cmake` - for the external project that builds the
  rustls-ffi Rust library.
@cpu cpu force-pushed the cpu-cmake-it-go branch 2 times, most recently from 88c4689 to 25d055d Compare November 26, 2024 18:33
@cpu
Copy link
Member Author

cpu commented Nov 26, 2024

@ctz I'm curious if you have any strong feelings about this before I try and get Jsha's eyes on the proposal. WDYT?

Copy link
Member

@ctz ctz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good & reasonable to me, and I feel I know cmake a bit better after reading the commit messages 🥴

cmake/rust.cmake Outdated Show resolved Hide resolved
@cpu
Copy link
Member Author

cpu commented Nov 28, 2024

@divergentdave is there any chance you'd be interested in taking a look at this diff as the author of the original CMakeLists.txt? 😅

@divergentdave
Copy link
Contributor

Sure! I can take a look

@cpu cpu self-assigned this Nov 28, 2024
@cpu
Copy link
Member Author

cpu commented Nov 28, 2024

I need to debug dynamic linking in CI on Windows.

I was able to reproduce the issue I'm seeing in a small standalone repo and asked the cargo-c folks to weigh in. There's a strong chance I'm putting square pegs in round holes, win32 is greek to me.

@cpu cpu force-pushed the cpu-cmake-it-go branch 2 times, most recently from f8eb7ea to 68acdfc Compare November 29, 2024 16:26
@cpu
Copy link
Member Author

cpu commented Nov 29, 2024

I need to debug dynamic linking in CI on Windows.

All fixed w/ lu-zero/cargo-c#428. I'm building cargo-c from source on Windows to fix CI (at the cost of runtime) but will adjust it to use a binary release when available. Windows CI is now using the binaries from the v0.10.7 release.

We now have working CI for building the example binaries using static and dynamic linking for all of Linux, MacOS and Windows 🎉

@cpu cpu marked this pull request as ready for review November 29, 2024 17:19
cmake/rust.cmake Outdated Show resolved Hide resolved
README.md Show resolved Hide resolved
cmake/rust.cmake Outdated Show resolved Hide resolved
cmake/options.cmake Outdated Show resolved Hide resolved
.github/workflows/test.yaml Show resolved Hide resolved

add_custom_target(
cbindgen
# TODO(@cpu): I suspect this won't work on Windows :P
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, this does indeed fail for me on Windows, but thus far it's just due to missing dependencies. cbindgen invokes cargo rustc -Zunpretty=expanded, and that fails when trying to build aws-lc-fips-sys, since I don't have nasm, Go, and Ninja installed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good data point, thank you. I wasn't sure the stdout redirect would work on Windows but it sounds like your attempt to repro might have bailed before that point 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think stdout redirection with > should be safe, it's supported by cmd.exe.

cmake/rust.cmake Outdated Show resolved Hide resolved
cmake/format.cmake Outdated Show resolved Hide resolved
.gitignore Show resolved Hide resolved
cpu added 12 commits December 4, 2024 10:34
This is largely in preparation for supporting MacOS and Linux CMake
builds where the default for the C/C++ tooling is annoying to deal with.
It is a no-op for Windows/IDE based CMake builds that use a "Multi
config" setup.

See the diff's comment for more detail on why this is trickier than it
has any right to be.
The client and server test binary targets are almost the exact same. To
avoid a lot of duplication we can use a function to add the targets and
invoke it twice, once for `client` and once for `server`.

This also requires telling Gersemi to not warn about unknown commands as
any functions defined by ourselves will be unknown to the formatter for
some complicated CMake reasons[0] I don't pretend to understand.

Along the way, change to using the `target_sources` command[1] to
specify the target's source code separate from `add_executable`. I found
this easier to work with when later trying to format the C source code
from a cmake target.

[0]: https://github.com/blankspruce/gersemi?tab=readme-ov-file#lets-make-a-deal
[1]: https://cmake.org/cmake/help/latest/command/target_sources.html#target-sources
The `PUBLIC` scope for the `target_include_directories`[0] command is to
populate `INTERFACE_INCLUDE_DIRECTORIES` and is for libraries to list
their include directory requirements. We don't intend for the
client/server targets to be used as libraries so use `PRIVATE` scope.

[0]: https://cmake.org/cmake/help/latest/command/target_include_directories.html
Previously we used the `add_compile_options`[0] command to conditionally
add the sanitizer settings to have debug builds use ASAN. This commit
lifts that configuration to the target level in the `test_binary`
function using `target_compile_options`[1].

Soon we will want to apply the sanitizer options differently based on
whether we're building for Windows or UNIX. It also seems like
a generally useful practice to try and scope as much config to specific
targets as possible.

[0]: https://cmake.org/cmake/help/latest/command/add_compile_options.html
[1]: https://cmake.org/cmake/help/latest/command/target_compile_options.html
This is relatively straightforward compared to Windows. Some care is
required to set the right linker options for MacOS, and for cert
compression we want `-lm` when the option is enabled. Compiler options
are matched to what `Makefile` was setting `CFLAGS` to. The ASAN
settings are similarly handled like in `Makefile`.
When building a debug release w/ clang we can enable a few more
sanitizers for the test code:

* undefined
* unsigned-integer-overflow
* local-bounds
* implicit-conversion

This is the "undefined" check group plus a few of the ones that it
doesn't include by default.

See https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html for more
information.

You can test the build with the following on a UNIX system:
```
CC=clang CXX=clang cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
cmake --build build
```
We have several `.PHONY` targets in the GNU `Makefile` that handle code
formatting. This commit extends the CMake build system to be able to do
the same. We add `-fix` and `-check` targets for:

* `rust-format-fix` and `rust-format-check` for running/checking `cargo
  fmt` for rust code.
* `cmake-format-fix` and `cmake-format-check` for running/checking
  Gersemi for CMake files.
* `c-format-fix` and `c-format-check` for running/checking
  `clang-format` on the test .c/.h files.
* `format-fix` and `format-check` for handling all of the above.

You can use these from a UNIX system like:

```
cmake -B build -S .
cmake --build build --target format-fix

cmake --build build --target help
```
This ports the `src/rustls.h` GNU `Makefile` target to the CMake build
system.

For now I'm assuming the development related targets for formatting,
generating headers, etc, won't be run on Windows (since that's the
status quo). In the future it would be nicer for these targets to be
platform independent.
This ports the `.PHONY` targets for running a platform verifier
connection test and the integration tests. Both are ignored by default
during normal `cargo test` invocations because they depend on the
client/server binaries being built (and may make network requests in the
case of the connect-test target).

Run these like usual on a UNIX system:
```
cmake -S . -B build
cmake --build build --target connect-test
cmake --build build --target integration-test
```
Use one job for Windows with a matrix of inputs for:

* crypto (aws-lc-rs or ring)
* config (Debug or Release)
* cert compression (on or off)

We don't extensively test all possibilities to save CI time. Notably we
only test cert compression w/ aws-lc-rs and in Release mode.

The integration test handling is now simplified by the addition of the
`integration-test` target.
It was getting cramped in here.
* Avoid the GNU Makefile for building C test code, performing helper
  tasks like running formatters. Instead, use `cmake`.

* Collapse the separate cert compression job into the Build+Test matrix.
If we can't run the client/server binary, share more about why.
@cpu cpu force-pushed the cpu-cmake-it-go branch from 116aaf3 to 308ae44 Compare December 4, 2024 15:51
@cpu
Copy link
Member Author

cpu commented Dec 4, 2024

@divergentdave Thank you for the thorough review. I really appreciate it.

@cpu
Copy link
Member Author

cpu commented Dec 9, 2024

This looks good & reasonable to me, and I feel I know cmake a bit better after reading the commit messages 🥴

@ctz Would you be willing to turn this comment into a +1 review? I emailed Jsha Nov 27th to ask for his review and will give him a bit more time before I merge but in the meantime it'd be helpful to have your approval recorded.

Copy link
Collaborator

@jsha jsha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really nice and extensive cleanup. Thanks for working on it!

README.md Outdated Show resolved Hide resolved
README.md Outdated Show resolved Hide resolved
cpu added 7 commits December 12, 2024 10:07
* Emphasize that a C compiler and build tools (cmake, etc) aren't needed
  to build rustls-ffi: just the example client/server binaries.
* Prefer describing building the rustls-ffi library (static and dynamic)
  using `cargo capi`. It's `install` action takes over for the
  `Makefile`'s manually curated version (and also works on Windows).
* Prefer using `cmake` to describe how to build the client/server tests.
  It's easier to use once you're familiar with it, and it's
  a cross-platform solution that works on Windows, MacOS and Linux.
* Some additional coverage for optional features (cert compression, FIPS).
This is _much_ easier, provides a real `install` command, and can
provide dynamic linking support that integrates well with `pkg-config`.
This commit replaces the Makefile.pkg-config script that was testing
using our client/server with a dynamically linked librustls on macOS and
Linux. Now, the CMake based build script can be used for this purpose by
activating `-DDYN_LINK=on` when configuring the cmake build. It supports
MacOS, Linux and Windows.
Helpful for Visual Studio usage.
This requires an upstream bug-fix from cargo-c. To support dynamic
linking on windows ensure you are using cargo-c 0.10.7+.

With this in place, we can run integration tests against client/server
binaries that dynamically link librustls.dll \o/
As pointed out by Divergentdave we can avoid using
`--no-warn-about-unknown-commands` by adding a simple decl file with our
two custom functions.
Rather than handle the difference in output binaries per-platform on our
own, rely on the target artifact generator expression[0] cmake already
provides.

[0]: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#target-artifacts
@cpu
Copy link
Member Author

cpu commented Dec 12, 2024

35 successful and 19 expected checks

Admin merging to bypass the stale rules (which I will fix afterwards).

@cpu cpu merged commit 43ce352 into rustls:main Dec 12, 2024
35 checks passed
@cpu cpu deleted the cpu-cmake-it-go branch December 12, 2024 15:18
@cpu
Copy link
Member Author

cpu commented Dec 12, 2024

which I will fix afterwards

Done.

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

Successfully merging this pull request may close these issues.

Simplify build systems, consider removing GNU Makefiles
4 participants