diff --git a/.github/actions/release-info/action.yml b/.github/actions/release-info/action.yml index d53344ce82..a3c233a387 100644 --- a/.github/actions/release-info/action.yml +++ b/.github/actions/release-info/action.yml @@ -15,26 +15,21 @@ outputs: runs: using: composite steps: - # A tag names its own version. Anything else takes it from BuildInfo.cpp and - # appends the commit hash as build metadata, joined with a plus sign because a - # Conan version cannot contain two hyphens. + # A tag names its own version. Anything else is a development build named by + # its commit hash, matching what cmake/XrplVersion.cmake derives: the head of + # a pull request rather than the merge commit GitHub creates for it. - name: Determine version id: version shell: bash env: IS_TAG: ${{ startsWith(github.ref, 'refs/tags/') }} REF_NAME: ${{ github.ref_name }} - SHA: ${{ github.sha }} + SHA: ${{ github.event.pull_request.head.sha || github.sha }} run: | if [[ "${IS_TAG}" == "true" ]]; then version="${REF_NAME}" else - version="$(awk -F'"' '/versionString =/ { print $2 }' src/libxrpl/protocol/BuildInfo.cpp)" - if [[ -z "${version}" ]]; then - echo "Unable to read versionString from BuildInfo.cpp." >&2 - exit 1 - fi - version="${version}+${SHA:0:7}" + version="0.0.0-dev+${SHA:0:7}" fi echo "version=${version}" | tee -a "${GITHUB_OUTPUT}" diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml index 1c0871f877..bc1557ae3c 100644 --- a/.github/workflows/on-tag.yml +++ b/.github/workflows/on-tag.yml @@ -1,9 +1,12 @@ # When a versioned tag is pushed, this workflow: # -# - uploads the libxrpl recipe to the Conan remote # - builds and tests the release binaries +# - uploads the libxrpl recipe to the Conan remote # - builds the DEB and RPM packages # - publishes those packages to the XRPLF package repositories +# +# Nothing is published unless the build passes, which is also where CMake +# rejects a tag that is not a valid version, e.g. 3.2.01. name: Tag on: @@ -22,6 +25,7 @@ defaults: jobs: upload-recipe: if: ${{ github.repository == 'XRPLF/rippled' }} + needs: build-test uses: ./.github/workflows/reusable-upload-recipe.yml secrets: remote_username: ${{ secrets.NEXUS_REMOTE_USERNAME }} diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index c1780d933a..9cfeee8407 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -196,6 +196,19 @@ jobs: env: BUILD_TYPE: ${{ inputs.build_type }} CMAKE_ARGS: ${{ inputs.cmake_args }} + # GitHub creates a merge commit for a PR + # https://www.kenmuse.com/blog/the-many-shas-of-a-github-pull-request/ + # + # We: + # - explicitly provide branch name + # - use `github.event.pull_request.head.sha` to get the SHA of last commit in the PR branch + # + # This way it works both for PRs and pushes to branches. + GITHUB_BRANCH_NAME: "${{ github.head_ref || github.ref_name }}" + GITHUB_HEAD_SHA: "${{ github.event.pull_request.head.sha || github.sha }}" + # + # If tag is being pushed, we use that version. + FORCE_XRPLD_VERSION: ${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || '' }} run: | cmake \ -G '${{ runner.os == 'Windows' && 'Visual Studio 18 2026' || 'Ninja' }}' \ diff --git a/BUILD.md b/BUILD.md index e98d204d0b..9c4d5558a9 100644 --- a/BUILD.md +++ b/BUILD.md @@ -51,6 +51,12 @@ git checkout release/3.2.x For a stable release, choose one of the [tagged releases](https://github.com/XRPLF/rippled/releases). +A build reports `0.0.0-dev` with its short commit hash as build metadata, e.g. +`0.0.0-dev+0123abc`. Only builds of a release, from a tag in CI or a versioned +Conan reference, report a release version. To use another version, set the +`FORCE_XRPLD_VERSION` environment variable when running CMake, e.g. +`FORCE_XRPLD_VERSION=3.4.0`. + ### Set Up Conan Once your [development environment](./docs/build/environment.md) is ready, set diff --git a/CMakeLists.txt b/CMakeLists.txt index ed4434ce03..3644cb520a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,6 @@ endif() include(PatchNixBinary) include(XrplSanity) -include(XrplVersion) include(XrplSettings) # this check has to remain in the top-level cmake because of the early return statement if(packages_only) diff --git a/bin/git/update-version.sh b/bin/git/update-version.sh deleted file mode 100755 index d9ef5ca6ef..0000000000 --- a/bin/git/update-version.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -if [[ $# -ne 3 || "$1" == "--help" || "$1" = "-h" ]]; then - name=$(basename $0) - cat <<-USAGE - Usage: $name workbranch base/branch version - - * workbranch will be created locally from base/branch. If it exists, - it will be reused, so make sure you don't overwrite any work. - * base/branch may be specified as user:branch to allow easy copying - from Github PRs. -USAGE - exit 0 -fi - -work="$1" -shift - -base=$(echo "$1" | sed "s/:/\//") -shift - -version=$1 -shift - -set -e - -git fetch upstreams - -git checkout -B "${work}" --no-track "${base}" - -push=$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \ - 2>/dev/null) || true -if [[ "${push}" != "" ]]; then - echo "Warning: ${push} may already exist." -fi - -build=$(find -name BuildInfo.cpp) -sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} >version.cpp && - diff "${build}" version.cpp && exit 1 || - mv -vi version.cpp ${build} - -git diff - -git add ${build} - -git commit -S -m "Set version to ${version}" - -git log --oneline --first-parent ${base}^.. - -cat <' as xrpld version") + + if(GIT_COMMIT_HASH STREQUAL "") + message( + FATAL_ERROR + "Unable to determine xrpld version without git, set FORCE_XRPLD_VERSION" + ) + endif() + + string(SUBSTRING ${GIT_COMMIT_HASH} 0 7 GIT_COMMIT_HASH_SHORT) + + set(XRPLD_VERSION "0.0.0-dev+${GIT_COMMIT_HASH_SHORT}") +endif() + +message(STATUS "Build version: ${XRPLD_VERSION}") diff --git a/conanfile.py b/conanfile.py index 7643f7e0f3..ccab522b95 100644 --- a/conanfile.py +++ b/conanfile.py @@ -1,9 +1,13 @@ +import os import re from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout +from conan.tools.env import Environment from conan import ConanFile +DEV_VERSION = "0.0.0-dev" + class Xrpl(ConanFile): name = "xrpl" @@ -120,13 +124,7 @@ class Xrpl(ConanFile): } def set_version(self): - if self.version is None: - path = f"{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp" - regex = r"versionString\s?=\s?\"(.*)\"" - with open(path, encoding="utf-8") as file: - matches = (re.search(regex, line) for line in file) - match = next(m for m in matches if m) - self.version = match.group(1) + self.version = self.version or DEV_VERSION def configure(self): if self.settings.compiler == "apple-clang": @@ -169,6 +167,17 @@ class Xrpl(ConanFile): generators = "CMakeDeps" def generate(self): + # The sources in the Conan cache have no git history, so the version + # comes from the reference, unless it is not one, like 'develop'. + if not os.path.exists(os.path.join(self.source_folder, ".git")): + version = str(self.version) + env = Environment() + env.define( + "FORCE_XRPLD_VERSION", + version if re.match(r"\d+\.\d+\.\d+", version) else DEV_VERSION, + ) + env.vars(self).save_script("xrpld_version") + tc = CMakeToolchain(self) tc.variables["tests"] = self.options.tests tc.variables["benchmark"] = self.options.benchmark diff --git a/package/README.md b/package/README.md index e0759d42c6..007e4a7dae 100644 --- a/package/README.md +++ b/package/README.md @@ -210,32 +210,36 @@ rejects request bodies over 100 MB, so uploads use the DNS-only host instead. The `release-info` action decides the channel from the event, and `publish_pkg.py` maps that channel to its repositories: -| Event | Version | Channel | DEB repository | RPM upload repository | -| ------------------------ | ----------------- | --------- | -------------- | --------------------- | -| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` | -| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` | -| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` | -| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` | -| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` | -| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` | +| Event | Version | Channel | DEB repository | RPM upload repository | +| ------------------------ | ------------------ | --------- | -------------- | --------------------- | +| tag | `X.Y.Z` | `stable` | `deb-stable` | `rpm-stable-hosted` | +| tag | `X.Y.Z-rcN` | `rc` | `deb-rc` | `rpm-rc-hosted` | +| tag | `X.Y.Z-bN` | `beta` | `deb-beta` | `rpm-beta-hosted` | +| tag, any other | `xrpld --version` | `custom` | `deb-custom` | `rpm-custom-hosted` | +| push to `develop` | `0.0.0-dev+` | `develop` | `deb-develop` | `rpm-develop-hosted` | +| tag, non-public codebase | _any_ | `private` | `deb-private` | `rpm-private-hosted` | A variant is published to the same channel under its own name, so `xrpld-assert` never overwrites `xrpld`. -Only a tag names a channel — do not extend that to `develop`, where -`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final -version during a release cycle, which would send develop builds into `stable`. -Versions sort in row order, so moving to a more mature channel never downgrades. -A tag matching none of the release patterns, such as `X.Y.Z-hotfix1`, publishes -to `custom`, which sits outside that order. +Only a tag picks a release channel. Versions sort in row order, so moving to a +more mature channel never downgrades. A tag matching none of the release +patterns, such as `X.Y.Z-hotfix1`, publishes to `custom`, which sits outside +that order. -The action decides the package release number on the same split: a tag's version -is unique, so its packages are release 1, while develop repeats the same version -and takes `.git`, e.g. -`857.20260826gitb6a8995` — the leading run number keeps each push superseding -the last, and the date and hash say which commit a package on -`packages.xrplf.org` came from. Both reach the packaging scripts as arguments, -so neither script derives anything itself. +Every untagged build reports `0.0.0-dev+`, which the `develop` channel +packages as `0.0.0~dev`, below every release. + +The action also picks the package release number: + +- A tag: `1`, since a tag's version is never reused. +- Anything else: `.git`, e.g. + `857.20260826gitb6a8995`. Develop packages all share `0.0.0~dev`, so the run + number orders them, and the date and hash name the commit a package on + `packages.xrplf.org` came from. + +Both reach the packaging scripts as arguments, so neither script derives +anything itself. Publishing is its own job, gated behind the install tests, uploading from the same image that built the packages with the `publish_pkg.py` shipped in it — the @@ -316,26 +320,30 @@ values; DEB writes them as `${pkg_version}-${PKG_RELEASE}` in With `PKG_RELEASE=1`, the package metadata becomes: -| Input version | RPM version/release | Debian version | -| ------------------ | ---------------------------- | -------------------- | -| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` | -| `3.2.0-b0+abc1234` | `3.2.0~b0+abc1234-1%{?dist}` | `3.2.0~b0+abc1234-1` | -| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` | -| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` | -| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` | +| Input version | RPM version/release | Debian version | +| ------------------- | -------------------------- | ------------------ | +| `3.2.0` | `3.2.0-1%{?dist}` | `3.2.0-1` | +| `3.2.0-b1` | `3.2.0~b1-1%{?dist}` | `3.2.0~b1-1` | +| `3.2.0-rc1` | `3.2.0~rc1-1%{?dist}` | `3.2.0~rc1-1` | +| `3.2.0-custom-1` | `3.2.0~custom.1-1%{?dist}` | `3.2.0~custom.1-1` | +| `0.0.0-dev+abc1234` | `0.0.0~dev-1%{?dist}` | `0.0.0~dev-1` | `build_pkg.py` defines `dist` as `.el9` rather than letting rpmbuild take it from the build host, so the RHEL image can track a newer release without changing what the packages claim to target. -The Debian changelog entry carries the channel passed as `--channel`, which -only accepts the channels in the table above plus `UNRELEASED`, the Debian -convention for a build that targets no channel at all — what local and CMake -builds pass, since nothing publishes them. An unsupported pre-release, and -build metadata on a final release such as `3.2.0+abc123`, are both rejected, -except in the `custom` and `private` channels, which accept any version and -switch each `-` inside the pre-release or build metadata to `.`, so -`3.4.0-custom-1` packages as `3.4.0~custom.1`. +The Debian changelog entry carries the channel passed as `--channel`: one of +the channels in the table above, or `UNRELEASED`, the Debian convention for a +build that targets no channel, which local and CMake builds pass. Each channel +accepts: + +- `stable`, `rc`, `beta`: `X.Y.Z`, or a `bN`/`rcN` pre-release, the only kind + that may carry build metadata. `3.2.0-b1` packages as `3.2.0~b1`. +- `custom`, `private`: any version, with each `-` inside the pre-release or + build metadata switched to `.`. `3.4.0-custom-1` packages as `3.4.0~custom.1`. +- `develop`: only `0.0.0-dev`, without its build metadata. `0.0.0-dev+abc1234` + packages as `0.0.0~dev`. +- `UNRELEASED`: `0.0.0-dev` as `develop` does, anything else as `stable` does. The RPM path intentionally uses `~` in `Version`, matching the Debian pre-release ordering convention, so RPM filenames/NVRs begin with forms like diff --git a/package/build_pkg.py b/package/build_pkg.py index 31223a03a2..4a623752d8 100755 --- a/package/build_pkg.py +++ b/package/build_pkg.py @@ -24,6 +24,12 @@ PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)$") # Channels a tag of any version is published to, rather than only bN/rcN. ANY_VERSION_CHANNELS = ("custom", "private") +# The version of any untagged build, which is all the develop channel publishes. +DEV_VERSION = "0.0.0-dev" + +# Channels an untagged build is packaged for: develop, or none for a local build. +DEV_VERSION_CHANNELS = ("develop", "UNRELEASED") + # The package name a variant suffixes, and the name every variant keeps for its # on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service). BASE_NAME = "xrpld" @@ -74,13 +80,23 @@ def capture(*command: object) -> str: def package_version(reported: str, channel: str) -> str: """Normalise a reported version into one the package formats accept. - A pre-release switches to '~' (3.2.0-b1 -> 3.2.0~b1), which also sorts before - the final 3.2.0; a no-op for a final release. The custom and private - channels accept any pre-release and build metadata, with any '-' inside - either switched to '.' (3.4.0-custom-1 -> 3.4.0~custom.1). + - Release: unchanged (3.2.0 -> 3.2.0). + - bN/rcN pre-release: '-' becomes '~', to sort before the release + (3.2.0-b1 -> 3.2.0~b1). + - custom, private: any version, '-' inside the pre-release and build + metadata becomes '.' (3.4.0-custom-1 -> 3.4.0~custom.1). + - develop, UNRELEASED: 0.0.0-dev without build metadata, so builds sort by + package release, not commit hash (0.0.0-dev+abc1234 -> 0.0.0~dev). + develop accepts nothing else. """ # Metadata first, as it may contain '-' too. release, plus, metadata = reported.partition("+") + if release == DEV_VERSION and channel in DEV_VERSION_CHANNELS: + return DEV_VERSION.replace("-", "~") + assert channel != "develop", ( + f"unsupported version {reported!r}: " + f"the develop channel only accepts {DEV_VERSION}." + ) base, _, pre_release = release.partition("-") if channel in ANY_VERSION_CHANNELS: pre_release = pre_release.replace("-", ".") diff --git a/src/libxrpl/protocol/BuildInfo.cpp b/src/libxrpl/protocol/BuildInfo.cpp index 9788b4c025..2902c528d0 100644 --- a/src/libxrpl/protocol/BuildInfo.cpp +++ b/src/libxrpl/protocol/BuildInfo.cpp @@ -13,34 +13,29 @@ #include #include +#ifndef XRPLD_VERSION +#error "XRPLD_VERSION must be defined" +#endif + namespace xrpl::build_info { namespace { -//-------------------------------------------------------------------------- -// The build version number. You must edit this for each release -// and follow the format described at http://semver.org/ -//------------------------------------------------------------------------------ -// clang-format off -// NOLINTNEXTLINE(readability-identifier-naming) -char const* const versionString = "3.5.0-b0" - // clang-format on - ; - -// -// Don't touch anything below this line -// +// Set by cmake/XrplVersion.cmake, see http://semver.org/ +constexpr char kVersionString[] = XRPLD_VERSION; std::string buildVersionString() { - std::string version = versionString; + std::string version = kVersionString; #if defined(DEBUG) || defined(SANITIZERS) + // A version derived from git already names its commit in the metadata. + bool const hasMetadata = version.contains('+'); std::string metadata; std::string const& commitHash = xrpl::git::getCommitHash(); - if (!commitHash.empty()) + if (!hasMetadata && !commitHash.empty()) metadata += commitHash + "."; #ifdef DEBUG @@ -56,7 +51,7 @@ buildVersionString() #endif if (!metadata.empty()) - version += "+" + metadata; + version += (hasMetadata ? "." : "+") + metadata; #endif return version; diff --git a/src/test/protocol/BuildInfo_test.cpp b/src/test/protocol/BuildInfo_test.cpp index a669e3e292..63d6fd9a40 100644 --- a/src/test/protocol/BuildInfo_test.cpp +++ b/src/test/protocol/BuildInfo_test.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -69,8 +70,12 @@ public: auto vFF = 0xFFFF'FFFF'FFFF'FFFFLLU; BEAST_EXPECT(!build_info::isNewerVersion(vFF)); + // 1.5.9 is newer than a development build (0.0.0-dev), but not a release. auto v159 = build_info::encodeSoftwareVersion("1.5.9"); - BEAST_EXPECT(!build_info::isNewerVersion(v159)); + BEAST_EXPECT( + build_info::isNewerVersion(v159) == + (beast::SemanticVersion(build_info::getVersionString()) < + beast::SemanticVersion("1.5.9"))); auto vCurrent = build_info::getEncodedVersion(); BEAST_EXPECT(!build_info::isNewerVersion(vCurrent));