Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 198 additions & 24 deletions .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ on:
- cron: '0 3 * * *'
workflow_dispatch:

# Early warning for environments outside the verified range: the newest
# compilers and the floating runner labels. These jobs deliberately use the
# wiring the required gates avoid — floating OS labels and compilers from
# external repositories — because breaking here is the signal we want, ahead
# of users hitting it. They never gate a pull request and must not be added to
# required status checks.
# Early warning for environments outside the verified range: the newest GCC,
# the Clang release next in line to enter the declared range, and the
# floating runner labels. These jobs deliberately use wiring the required
# gates avoid — floating OS labels, and compilers pulled from outside the
# distribution's own repositories — because breaking here is the signal we
# want, ahead of users hitting it. The Clang job watches a release pinned
# above the declared range; the GCC job tracks the newest versioned g++
# available once the toolchain PPA is in place, which is normally above
# that range too but is never checked against it. They never gate a pull
# request and must not be added to required status checks.

jobs:
newest-gcc:
Expand Down Expand Up @@ -56,46 +60,216 @@ jobs:
run: ./build/test/dross_test --gtest_color=yes

newest-clang:
name: Newest Clang
name: Next Clang
runs-on: ubuntu-latest

# Watches the smallest "-N" apt.llvm.org suite above the declared Clang
# range (CLANG_WATCH_VERSION below), not the largest one available:
# taking the largest would skip past the release most likely to enter the
# declared range next, leaving it with no CI evidence at the moment the
# decision is made. The unsuffixed suite (a rolling development snapshot)
# is never watched either, because -Werror is a PUBLIC compile option
# (src/CMakeLists.txt) and a diagnostic newly added upstream would turn a
# healthy build red for reasons unrelated to this project. The pinned
# version below is bumped together with the declared range so both move
# in the same commit and the watched release stays visible in the diff.
#
# The apt source line is composed directly instead of fetching and
# running apt.llvm.org's install script: this job only needs one source
# line and one package, so there is no need to pull a remote script and
# execute it as root. The script's own version table also points its
# newest entries at the unsuffixed development suite, which is exactly
# what this job is designed to avoid.
env:
CLANG_WATCH_VERSION: '23'

steps:
- uses: actions/checkout@v7

# llvm.sh adds the apt.llvm.org repository for the current stable release,
# then we take the newest versioned clang package available. Tracking the
# stable release rather than trunk (llvm.sh all) is deliberate: this watches
# what users will actually reach for, not what is still in development.
- name: Install newest Clang
- name: Probe and install the watched Clang release
id: watch
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh
version=$(apt-cache search --names-only '^clang-[0-9]+$' \
| sed 's/^clang-//; s/ .*//' | sort -n | tail -1)
if [ -z "$version" ]; then
echo "No versioned clang package found in the apt.llvm.org repository" >&2
set -euo pipefail
. /etc/os-release
codename="${UBUNTU_CODENAME:-}"
if [ -z "$codename" ]; then
echo "Could not read UBUNTU_CODENAME from /etc/os-release" >&2
exit 1
fi
sudo apt-get install -y "clang-$version"
echo "CLANG_VERSION=$version" >> "$GITHUB_ENV"

suite="llvm-toolchain-${codename}-${CLANG_WATCH_VERSION}"
base="https://apt.llvm.org/${codename}"
# binary-amd64 is hardcoded: ubuntu-latest is an x86-64 runner today,
# so that's the architecture whose package index this job needs.
index_url="${base}/dists/${suite}/main/binary-amd64/Packages.gz"

# A 200 on dists/<suite>/Release would only prove the suite exists,
# not that the compiler package is in it, so the package index itself
# is what gets probed. Capture the HTTP status without letting a
# transport failure abort the script before the branch below runs.
status=$(curl -sS -L -o /tmp/Packages.gz -w '%{http_code}' \
--retry 2 --retry-connrefused \
"$index_url") || {
echo "Failed to reach $index_url (curl transport error)" >&2
exit 1
}

case "$status" in
200)
;;
404)
# A bare 404 here is ambiguous: it's the expected shape while
# the suite simply hasn't been branched yet, but it is also
# what a *dead* watch looks like if apt.llvm.org has moved the
# index (e.g. to .xz/.zst) or stopped serving this codename —
# and a dead watch reporting green every night is worse than
# one that fails loudly. Two follow-up requests, made only on
# this branch, tell those cases apart. Each is judged strictly
# against 200 vs. 404: anything else (a 5xx, a 403, ...) is not
# evidence of either outcome, so it fails the job on its own
# rather than falling through to the next check.

# Does the suite exist at all, just not with an index at this
# path? A 200 here alongside the 404 on Packages.gz means
# upstream changed how (or where) the index is served, which
# this job cannot follow silently.
release_status=$(curl -sS -L -o /dev/null -w '%{http_code}' \
--retry 2 --retry-connrefused \
"${base}/dists/${suite}/Release") || {
echo "Failed to reach ${base}/dists/${suite}/Release (curl transport error)" >&2
exit 1
}
case "$release_status" in
200)
echo "$suite is published (${base}/dists/${suite}/Release is 200) but its package index is not at $index_url; apt.llvm.org may have moved or recompressed it." >&2
exit 1
;;
404)
;;
*)
echo "Unexpected HTTP $release_status probing ${base}/dists/${suite}/Release" >&2
exit 1
;;
esac

# Is this codename served by apt.llvm.org at all? The
# codename's own unsuffixed suite is published independently
# of any -N release, so it works as a version-independent
# oracle here. This job still never builds from the unsuffixed
# suite (see the header comment above) — it's read here only
# to answer "does apt.llvm.org know this codename", never as a
# source of packages.
codename_status=$(curl -sS -L -o /dev/null -w '%{http_code}' \
--retry 2 --retry-connrefused \
"${base}/dists/llvm-toolchain-${codename}/Release") || {
echo "Failed to reach ${base}/dists/llvm-toolchain-${codename}/Release (curl transport error)" >&2
exit 1
}
case "$codename_status" in
200)
;;
404)
echo "${base}/dists/llvm-toolchain-${codename}/Release returned 404, so apt.llvm.org does not serve codename '$codename'; the ubuntu-latest label may have moved to a codename apt.llvm.org doesn't carry yet." >&2
exit 1
;;
*)
echo "Unexpected HTTP $codename_status probing ${base}/dists/llvm-toolchain-${codename}/Release" >&2
exit 1
;;
esac

echo "::notice::$suite's package index was not found at $index_url; apt.llvm.org serves codename '$codename', so this suite may simply not have been branched yet."
echo "available=false" >> "$GITHUB_OUTPUT"
exit 0
;;
*)
echo "Unexpected HTTP $status probing $index_url" >&2
exit 1
;;
esac

# Decompression failure (corrupt/truncated download) and "package not
# listed" are different outcomes and must not be conflated: the first
# fails the job, the second is a plain notice-and-skip. /tmp/Packages
# is a real file rather than a pipe for the same reason: piping
# straight into `grep -q` would let grep exit as soon as it matched,
# killing gunzip with SIGPIPE, and pipefail would then report the
# whole pipeline as failed — misclassifying "the compiler IS
# published" as "not published yet".
if ! gunzip -c /tmp/Packages.gz > /tmp/Packages 2>/tmp/gunzip.err; then
echo "Failed to decompress $index_url: $(cat /tmp/gunzip.err)" >&2
exit 1
fi

# grep's exit status distinguishes "no match" (1, the expected
# not-yet-published case) from "grep itself failed" (anything else,
# e.g. an unreadable file) — the two must not be conflated, or a
# broken probe would silently read as a routine skip.
if grep -q "^Package: clang-${CLANG_WATCH_VERSION}\$" /tmp/Packages; then
grep_status=0
else
grep_status=$?
fi

case "$grep_status" in
0)
;;
1)
# The specific version wasn't found, but that reading only holds
# if this index lists clang packages under that name at all. If
# it has no "^Package: clang-" line whatsoever, the assumption
# behind the search has gone stale the same way a bare 404
# would have — and the file is already local, so checking costs
# no extra request.
if ! grep -q "^Package: clang-" /tmp/Packages; then
echo "$index_url has no '^Package: clang-' entries at all; the index format or clang package naming may have changed upstream." >&2
exit 1
fi
echo "::notice::$suite exists but clang-$CLANG_WATCH_VERSION is not published in it yet."
echo "available=false" >> "$GITHUB_OUTPUT"
exit 0
;;
*)
echo "grep exited $grep_status searching the decompressed index from $index_url for clang-$CLANG_WATCH_VERSION" >&2
exit 1
;;
esac

# apt-get update verifies this suite's InRelease signature against
# the keyring named by signed-by below, and package hashes chain
# from that signed release — which is why the key must stay scoped
# to this one source.
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL --proto '=https' --proto-redir '=https' \
https://apt.llvm.org/llvm-snapshot.gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/llvm-snapshot.gpg
sudo chmod a+r /etc/apt/keyrings/llvm-snapshot.gpg
echo "deb [signed-by=/etc/apt/keyrings/llvm-snapshot.gpg] $base/ $suite main" \
| sudo tee /etc/apt/sources.list.d/llvm-watch.list > /dev/null
sudo apt-get update
sudo apt-get install -y "clang-$CLANG_WATCH_VERSION"
echo "available=true" >> "$GITHUB_OUTPUT"

- name: Report toolchain versions
if: steps.watch.outputs.available == 'true'
run: |
clang-"$CLANG_VERSION" --version
clang-"$CLANG_WATCH_VERSION" --version
cmake --version

- name: Configure CMake
if: steps.watch.outputs.available == 'true'
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang-"$CLANG_VERSION" \
-DCMAKE_CXX_COMPILER=clang++-"$CLANG_VERSION"
-DCMAKE_C_COMPILER=clang-"$CLANG_WATCH_VERSION" \
-DCMAKE_CXX_COMPILER=clang++-"$CLANG_WATCH_VERSION"

- name: Build
if: steps.watch.outputs.available == 'true'
run: cmake --build build -j"$(nproc)"

- name: Test
if: steps.watch.outputs.available == 'true'
run: ./build/test/dross_test --gtest_color=yes

floating-runners:
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@
- **C++23** compatible compiler, verified in CI as:
- Linux: GCC 13–15, or Clang 20–22 with either libstdc++ or libc++
- macOS: the Apple Clang shipped with macOS 15 or 26
- Newer versions are best effort, exercised by the nightly toolchain watch
- Newer versions are best effort: the nightly toolchain watch tracks the
newest versioned GCC available once the toolchain PPA is in place, and
the specific Clang release next in line to enter this range
- Within the declared range, the required Linux jobs build GCC 13/15 and
Clang 20/22, with the Clang jobs pairing against the libstdc++ present
on Ubuntu 26.04 or against libc++. Clang against the older libstdc++
releases available on Ubuntu 24.04 (13 and 14) is not yet verified;
verification for that combination is to be added, and this sentence
will be removed once it is. GCC 14 and Clang 21 are inside the declared
range but are not built by a required job.
- **CMake 3.20+**

### Installation
Expand Down
4 changes: 4 additions & 0 deletions docs/sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Changed
- Enhanced error handling: ``timezone::from_string()`` returns ``std::optional<timezone>``
- Simplified API: removed redundant timezone methods (``is_local()``, ``has_offset()``)
- Updated documentation to reflect timestamp and timezone APIs
- **Breaking:** ``split()`` now takes its arguments as ``std::string_view``
instead of ``const std::string&``; callers passing a type that converts
implicitly to ``std::string`` but not to ``std::string_view`` will no
longer compile

v0.1.0 - 2024-01-20
-------------------
Expand Down
7 changes: 7 additions & 0 deletions docs/sphinx/source/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ Build Requirements
- C++23 compatible compiler: on Linux, GCC 13-15, or Clang 20-22 with either
libstdc++ or libc++; on macOS, the Apple Clang shipped with macOS 15 or 26.
Newer versions are best effort.
- Within that range, the required Linux jobs currently build GCC 13/15 and
Clang 20/22, with Clang built against the libstdc++ present on Ubuntu
26.04 or against libc++. Clang against the older libstdc++ releases
available on Ubuntu 24.04 (13 and 14) is not yet built by a required job;
verification for that combination is to be added, and this sentence will
be removed once it lands. GCC 14 and Clang 21 are inside the declared
range but are not built by a required job either.
- CMake 3.20 or later
- Git

Expand Down
16 changes: 12 additions & 4 deletions docs/sphinx/source/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@ To build and use dross, you need:
or libc++
- On macOS: the Apple Clang shipped with macOS 15 or 26

Newer versions are best effort: they are exercised by the nightly toolchain
watch rather than by the required build matrix.
Newer versions are best effort: GCC is exercised by the nightly toolchain
watch tracking the newest versioned GCC available once the toolchain PPA
is in place, and Clang by the same watch tracking the specific release
next in line to enter this range, rather than by the required build
matrix.

The Clang lower bound is higher than the GCC one because older Clang
releases cannot compile this library's C++23 ``std::expected`` usage against
the libstdc++ they are paired with on Ubuntu 24.04 — installing the
libstdc++ 14 headers alongside them does not change that either. From Clang
20 onwards both standard libraries work, and the build matrix verifies each
of them, so neither is imposed on you.
20 onwards both standard libraries are supported, but the required build
matrix currently exercises only Clang 20 and 22 (not the intermediate 21),
and only against the libstdc++ present on Ubuntu 26.04 or against libc++
— not against the older libstdc++ releases available on Ubuntu 24.04
(13 and 14). Verification for that lower-bound combination is to be added;
this paragraph will be trimmed once it is. GCC 14 and Clang 21 sit inside
the declared range the same way, without a required job of their own.

- **Build System**: CMake 3.20 or later
- **Operating System**: Linux or macOS
Expand Down
4 changes: 3 additions & 1 deletion include/dross/type.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
#include <dross/type/timezone.h>
#include <dross/type/value.h>

#include <string>
#include <string_view>
#include <vector>

/**
Expand All @@ -91,7 +93,7 @@ namespace dross {
* // Result: {"a", "b", "c"}
* @endcode
*/
std::vector<std::string> split(const std::string& s, const std::string& delimiter);
std::vector<std::string> split(std::string_view s, std::string_view delimiter);

/**
* @brief Join string tokens into a single string using a delimiter.
Expand Down
2 changes: 1 addition & 1 deletion src/type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace dross {

std::vector<std::string> split(const std::string& s, const std::string& delimiter)
std::vector<std::string> split(std::string_view s, std::string_view delimiter)
{
std::vector<std::string> tokens;

Expand Down
Loading