diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ff56f3d..61c7cf47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,11 +24,35 @@ # [1]: https://cliutils.gitlab.io/modern-cmake/chapters/intro/dodonot.html cmake_minimum_required(VERSION 3.27...3.29) +# Derive the project version from `au/version.hh`, which is the single source of truth for the +# library version (it also defines the `AU_VERSION*` macros for C++ code). This keeps the CMake +# version and the C++ macros from ever drifting apart, and means a release only needs to bump the +# version in one place. +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/au/version.hh" _au_version_hh) +string(REGEX MATCH "#define AU_VERSION_MAJOR ([0-9]+)" _ "${_au_version_hh}") +set(_au_version_major "${CMAKE_MATCH_1}") +string(REGEX MATCH "#define AU_VERSION_MINOR ([0-9]+)" _ "${_au_version_hh}") +set(_au_version_minor "${CMAKE_MATCH_1}") +string(REGEX MATCH "#define AU_VERSION_PATCH ([0-9]+)" _ "${_au_version_hh}") +set(_au_version_patch "${CMAKE_MATCH_1}") + +# Fail loudly if any component failed to parse, rather than silently building a malformed version +# such as "0.5." (which would otherwise flow into `project()`, the docs URL, and installed configs). +foreach(_part major minor patch) + if(NOT _au_version_${_part} MATCHES "^[0-9]+$") + message(FATAL_ERROR + "Could not parse AU_VERSION_${_part} from au/version.hh. " + "Expected a line like '#define AU_VERSION_${_part} '.") + endif() +endforeach() + +set(AU_VERSION "${_au_version_major}.${_au_version_minor}.${_au_version_patch}") + project( Au - VERSION 0.5.0 + VERSION ${AU_VERSION} DESCRIPTION "A C++ quantities and units library, by Aurora Innovation" - HOMEPAGE_URL "https://aurora-opensource.github.io/au/0.5.0/" + HOMEPAGE_URL "https://aurora-opensource.github.io/au/${AU_VERSION}/" LANGUAGES CXX ) diff --git a/RELEASE.md b/RELEASE.md index 17b8b13d..eafdaa0e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -71,16 +71,29 @@ Any empty section can be omitted. We try to follow [semantic versioning](https://semver.org/). Since we are currently in major version zero (0.y.z), incompatible changes don't force a major version upgrade. -### Update the CMake version number - -Edit the `CMakeLists.txt` file in the root folder, updating the version number in the `project` -command to the number chosen above. - -Also update the version number in the `HOMEPAGE_URL` parameter, because we link to the docs for the -latest release in our CMake project definition. (True, this URL won't exist until you complete the -remaining steps in this guide, but the danger of getting it wrong is pretty small.) - -Make a PR with these changes and land it before creating the tag. +### Update the version number + +The version number lives in exactly one place: the `AU_VERSION_MAJOR`, `AU_VERSION_MINOR`, and +`AU_VERSION_PATCH` macros in `au/version.hh`. Edit those three macros to the number chosen above. +Everything else is derived from them: + +- The C++ `AU_VERSION` macro (which downstream users read to detect the library and its version). +- The CMake `project(... VERSION ...)` and `HOMEPAGE_URL`, which the root `CMakeLists.txt` parses + out of `au/version.hh`. (The `HOMEPAGE_URL` points at the docs for this release, which won't + exist until you complete the remaining steps in this guide, but the danger of getting it wrong is + pretty small.) + +**Where this commit lands depends on the release type:** + +- **Minor or major release** (e.g. `0.6.0`): make a PR that bumps `au/version.hh` and land it on + `main` _before_ creating the tag. This is the "final commit", and it becomes the base commit for + the release branch (see "Prepare the release branch" below). Note that `main` will continue to + report this version until the _next_ minor/major bump, even as new (unreleased) changes land on + top of it; this is expected, and matches how the version macros are documented to behave. +- **Patch release** (`0.5.1` and later): the version bump does _not_ go on `main`. Patch releases + are cherry-picked from `main` onto the pre-existing release branch (e.g. `release-0.5.0`). Bump + `au/version.hh` in a commit _on the release branch_ alongside the cherry-picked fix(es), and tag + that. `main` keeps whatever version it already had. ### Fill out release notes template @@ -142,9 +155,9 @@ Issues! Alphabetically: ### Prepare the release branch -First, make sure the "final commit" (which updates the CMake variables) has already landed, and is -currently checked out. This will be the "base" commit for the release branch, which we'll create -and push to GitHub. +First, make sure the "final commit" (which updates the version in `au/version.hh`) has already +landed, and is currently checked out. This will be the "base" commit for the release branch, which +we'll create and push to GitHub. ```sh # Remember to update the version number! @@ -155,7 +168,7 @@ git push origin release-0.3.1 Branches named similarly to `release-0.3.1` are protected in the Au repo, so we will need to make PRs for the final changes for the release. -### PR: Update links +### PR: Update links and mark as a release Several C++ files in the repository link to the documentation website, but they link to the version at `main`. This version will change over time in ways that we can't predict. It's important for @@ -172,7 +185,19 @@ https://aurora-opensource.github.io/au/0.3.1/... ^^^^^ ``` -We do this in a PR on the release branch in order to avoid churn commits on the main branch. +In this same PR, flip the release flag in `au/version.hh`: + +``` +Find this: #define AU_VERSION_IS_RELEASE 0 +Replace with: #define AU_VERSION_IS_RELEASE 1 +``` + +This value is `0` on `main` at all times, and this release-branch-only PR is the _only_ place it +becomes `1`. That's what lets downstream users distinguish an official release from an arbitrary +`main` checkout (whose version numbers may otherwise be identical to the release it was branched +from). It must never be set to `1` on `main`. + +We do all of this in a PR on the release branch in order to avoid churn commits on the main branch. ### Create the tag for the release diff --git a/au/BUILD.bazel b/au/BUILD.bazel index 849bb0b9..3cb7d4c2 100644 --- a/au/BUILD.bazel +++ b/au/BUILD.bazel @@ -98,6 +98,23 @@ cc_library( name = "config", hdrs = ["config.hh"], visibility = ["//visibility:public"], + deps = [":version"], +) + +cc_library( + name = "version", + hdrs = ["version.hh"], + visibility = ["//visibility:public"], +) + +cc_test( + name = "version_test", + size = "small", + srcs = ["version_test.cc"], + deps = [ + ":version", + "@googletest//:gtest_main", + ], ) cc_library( diff --git a/au/CMakeLists.txt b/au/CMakeLists.txt index 3bceea81..fcfa74fa 100644 --- a/au/CMakeLists.txt +++ b/au/CMakeLists.txt @@ -45,6 +45,7 @@ header_only_library( truncation_risk.hh unit_of_measure.hh unit_symbol.hh + version.hh view.hh wrapper_operations.hh zero.hh @@ -644,6 +645,14 @@ gtest_based_test( testing ) +gtest_based_test( + NAME version_test + SRCS + version_test.cc + DEPS + au +) + gtest_based_test( NAME zero_test SRCS diff --git a/au/config.hh b/au/config.hh index 9f7ca52c..b067661b 100644 --- a/au/config.hh +++ b/au/config.hh @@ -14,6 +14,10 @@ #pragma once +// Make the version macros (`AU_VERSION`, etc.) available anywhere `config.hh` reaches --- which is +// effectively the entire library, since the core machinery includes this header. +#include "au/version.hh" + // // Device/GPU support (CUDA, HIP) // diff --git a/au/version.hh b/au/version.hh new file mode 100644 index 00000000..24057e46 --- /dev/null +++ b/au/version.hh @@ -0,0 +1,67 @@ +// Copyright 2026 Aurora Operations, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +// +// Version macros for the Au library. +// +// These serve two purposes. First, they let downstream code detect that Au has been included at +// all (for example, to `#error` if it has _not_ been): any Au header transitively includes this +// one, so `#if defined(AU_VERSION)` will be true whenever any part of Au is in scope. Second, they +// let downstream code detect _which version_ of Au is present, which is useful for writing code +// that must support multiple Au versions during a migration. +// +// The individual components are available as `AU_VERSION_MAJOR`, `AU_VERSION_MINOR`, and +// `AU_VERSION_PATCH`. For convenience, `AU_VERSION` combines them into a single integer that +// increases monotonically with the version, so that ordinary integer comparisons work: +// +// #if AU_VERSION < AU_VERSION_NUMBER(0, 5, 1) +// // ... code for Au older than 0.5.1 ... +// #endif +// +// IMPORTANT (release model): these numbers are only authoritative for _tagged releases_. Every Au +// release is cut from a dedicated release branch, and the released commit never lives on `main` +// (see `RELEASE.md`). On `main`, these macros name the _most recent release_, mirroring the +// version in the root `CMakeLists.txt` (which is derived from this file). That means a `main` +// checkout can report version `X.Y.Z` while actually containing changes made _after_ `X.Y.Z` was +// released. There is currently no programmatic way to tell such a `main` checkout apart from the +// tagged `X.Y.Z` release; they report the same version number. +// +// To keep the two build systems in sync, `CMakeLists.txt` parses the three component macros below +// to populate its `project(... VERSION ...)`. This file is therefore the single source of truth +// for the library version, and it is the _only_ place that needs to be edited when bumping the +// version for a release. +// + +#define AU_VERSION_MAJOR 0 +#define AU_VERSION_MINOR 5 +#define AU_VERSION_PATCH 0 + +// Combine major/minor/patch components into a single monotonically increasing integer. Each +// component gets three decimal digits, so components must be strictly less than 1000. +#define AU_VERSION_NUMBER(major, minor, patch) ((major) * 1000000 + (minor) * 1000 + (patch)) + +#define AU_VERSION AU_VERSION_NUMBER(AU_VERSION_MAJOR, AU_VERSION_MINOR, AU_VERSION_PATCH) + +// Whether this is an actual tagged release (`1`) or an in-development checkout (`0`). +// +// Because a minor/major release branch is cut _from_ the `main` commit that bumps the version, that +// commit's version numbers above are byte-identical to the release's --- so the version numbers +// alone cannot tell `main` apart from the release it was branched from. This flag closes that gap. +// It is `0` on `main` at all times, and is flipped to `1` only on the release branch (as part of +// the release-only step that also updates the doc links; see `RELEASE.md`). Downstream code that +// must be sure it is building against an official release, and not an arbitrary `main` checkout, +// can therefore test `#if AU_VERSION_IS_RELEASE`. +#define AU_VERSION_IS_RELEASE 0 diff --git a/au/version_test.cc b/au/version_test.cc new file mode 100644 index 00000000..cd4e1f63 --- /dev/null +++ b/au/version_test.cc @@ -0,0 +1,72 @@ +// Copyright 2026 Aurora Operations, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "au/version.hh" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace { + +using ::testing::AnyOf; +using ::testing::Eq; +using ::testing::Ge; +using ::testing::Gt; +using ::testing::Lt; + +TEST(Version, ComponentMacrosAreDefined) { +#ifndef AU_VERSION_MAJOR + FAIL() << "AU_VERSION_MAJOR is not defined"; +#endif +#ifndef AU_VERSION_MINOR + FAIL() << "AU_VERSION_MINOR is not defined"; +#endif +#ifndef AU_VERSION_PATCH + FAIL() << "AU_VERSION_PATCH is not defined"; +#endif + // Each component must fit in the three decimal digits that `AU_VERSION_NUMBER` allots it. + EXPECT_THAT(AU_VERSION_MINOR, Lt(1000)); + EXPECT_THAT(AU_VERSION_PATCH, Lt(1000)); +} + +TEST(Version, CombinedVersionMatchesComponents) { + EXPECT_THAT(AU_VERSION, + Eq(AU_VERSION_MAJOR * 1000000 + AU_VERSION_MINOR * 1000 + AU_VERSION_PATCH)); +} + +TEST(Version, VersionNumberOrdersLikeSemanticVersioning) { + // Patch increments increase the number. + EXPECT_THAT(AU_VERSION_NUMBER(0, 5, 1), Gt(AU_VERSION_NUMBER(0, 5, 0))); + // Minor increments outweigh any patch difference. + EXPECT_THAT(AU_VERSION_NUMBER(0, 6, 0), Gt(AU_VERSION_NUMBER(0, 5, 999))); + // Major increments outweigh any minor difference. + EXPECT_THAT(AU_VERSION_NUMBER(1, 0, 0), Gt(AU_VERSION_NUMBER(0, 999, 999))); +} + +TEST(Version, IncludingAnyAuHeaderDefinesVersion) { + // This is the "was Au included?" use case: `AU_VERSION` is truthy whenever Au is in scope. + EXPECT_THAT(AU_VERSION, Ge(AU_VERSION_NUMBER(0, 5, 0))); +} + +TEST(Version, IsReleaseFlagIsDefinedAndBoolean) { +#ifndef AU_VERSION_IS_RELEASE + FAIL() << "AU_VERSION_IS_RELEASE is not defined"; +#endif + // Note: we deliberately do _not_ assert that this is `0`. It is `0` on `main`, but `1` on the + // release branch (where CI also runs), so pinning the value would break release-branch builds. + // The "`main` must be `0`" invariant is a process guarantee (see `RELEASE.md`), not a test. + EXPECT_THAT(AU_VERSION_IS_RELEASE, AnyOf(Eq(0), Eq(1))); +} + +} // namespace diff --git a/docs/reference/index.md b/docs/reference/index.md index e746ec6d..e7f468ce 100644 --- a/docs/reference/index.md +++ b/docs/reference/index.md @@ -33,6 +33,9 @@ Here's a guide to the main reference pages. - **[Math functions](./math.md).** We provide many common mathematical functions out of the box. +- **[Version macros](./version.md).** Preprocessor macros (`AU_VERSION`, and friends) that let + downstream code detect that Au is present, and which version it is. + - **[Format support](./format.md).** Exercise fine-grained control over formatting `Quantity` and `QuantityPoint` to strings, using either the popular [{fmt}] library, or C++20's `std::format`. diff --git a/docs/reference/version.md b/docs/reference/version.md new file mode 100644 index 00000000..2265dacb --- /dev/null +++ b/docs/reference/version.md @@ -0,0 +1,63 @@ +# Version macros + +Au provides preprocessor macros that let downstream code detect that the library is present, and +which version of it is present. They live in [`"au/version.hh"`](../../au/version.hh), which every +Au header transitively includes, so they are available whenever any part of Au is in scope --- and +also in the single-file packages, since those always include `au/au.hh`. + +## Macros + +- `AU_VERSION_MAJOR`, `AU_VERSION_MINOR`, `AU_VERSION_PATCH`: the three components of the version, as + integer literals. + +- `AU_VERSION`: a single integer that increases monotonically with the version, so that ordinary + integer comparisons order versions correctly. + +- `AU_VERSION_NUMBER(major, minor, patch)`: computes the `AU_VERSION`-style integer for an arbitrary + version, so you can compare against it. Each component must be strictly less than `1000`. + +- `AU_VERSION_IS_RELEASE`: `1` if this is an official tagged release, and `0` for an in-development + (`main`) checkout. See ["`main` versus tagged releases"](#caveat-main-versus-tagged-releases) + below. + +## Use cases + +**Detecting that Au was included.** Because every Au header pulls in the version macros, you can +`#error` (or warn) if the library is _not_ in scope: + +```cpp +#if !defined(AU_VERSION) +#error "This file requires the Au units library to be included first." +#endif +``` + +**Detecting the version.** This is useful when writing code that must support more than one Au +version during a migration: + +```cpp +#if AU_VERSION < AU_VERSION_NUMBER(0, 5, 1) + // ... code path for Au older than 0.5.1 ... +#else + // ... code path for Au 0.5.1 and newer ... +#endif +``` + +## Caveat: `main` versus tagged releases + +These macros are only authoritative for **tagged releases**. Every Au release is cut from a +dedicated release branch, and the released commit never lives on `main`. On `main`, the macros name +the _most recent release_ (they are kept in sync with the version in the root `CMakeLists.txt`, +which is derived from `au/version.hh`). That means a `main` checkout can report version `X.Y.Z` +while actually containing changes made _after_ `X.Y.Z` was released. In fact, because a +minor/major release branch is cut _from_ the `main` commit that bumps the version, that `main` +commit's three version numbers are byte-identical to the release's --- so the version numbers alone +cannot tell them apart. + +Use `AU_VERSION_IS_RELEASE` to close that gap. It is `1` only on official tagged releases, and `0` +on `main`: + +```cpp +#if !AU_VERSION_IS_RELEASE +#warning "Building against an in-development checkout of Au, not a tagged release." +#endif +```