first attempt at generating sparse CI run matrix in python; also, cancel previous runs on the same branch

This commit is contained in:
Yves Delley
2024-11-12 18:07:52 +01:00
parent dc6fb931ea
commit 35ed472975
6 changed files with 367 additions and 312 deletions

168
.github/generate-job-matrix.py vendored Normal file
View File

@ -0,0 +1,168 @@
import argparse
import json
import typing
import itertools
import random
import dataclasses
from types import SimpleNamespace
from dataclasses import dataclass
from job_matrix import Configuration, Compiler, MatrixElement, CombinationCollector, dataclass_to_json
def make_gcc_config(version: int) -> Configuration:
return Configuration(
name=f"GCC-{version}",
os="ubuntu-24.04",
compiler=Compiler(
type="GCC",
version=version,
cc=f"gcc-{version}",
cxx=f"g++-{version}",
),
cxx_modules=False,
std_format_support=version >= 13,
)
def make_clang_config(version: int, platform: typing.Literal["x86-64", "arm64"] = "x86-64") -> Configuration:
ret = SimpleNamespace(
name=f"Clang-{version} ({platform})",
os=None, # replaced below
compiler=SimpleNamespace(
type="CLANG",
version=version,
cc=f"clang-{version}",
cxx=f"clang++-{version}",
),
lib="libc++",
cxx_modules=version >= 17,
std_format_support=version >= 17,
)
match platform:
case "x86-64":
ret.os = "ubuntu-24.04"
case "arm64":
ret.os = "macos-14"
pfx = f"/opt/homebrew/opt/llvm@{version}/bin/"
ret.compiler.cc = pfx + ret.compiler.cc
ret.compiler.cxx = pfx + ret.compiler.cxx
case _:
raise KeyError(f"Unsupported platform {platform!r} for Clang")
ret.compiler = Compiler(**vars(ret.compiler))
return Configuration(**vars(ret))
def make_apple_clang_config(version: int) -> Configuration:
ret = Configuration(
name=f"Apple Clang {version}",
os="macos-13",
compiler=Compiler(
type="APPLE_CLANG",
version=f"{version}.0",
cc="clang",
cxx="clang++",
),
cxx_modules=False,
std_format_support=False,
)
return ret
def make_msvc_config(release: str, version: int) -> Configuration:
ret = Configuration(
name=f"MSVC {release}",
os="windows-2022",
compiler=Compiler(
type="MSVC",
version=version,
cc="",
cxx="",
),
cxx_modules=False,
std_format_support=True,
)
return ret
configs = {c.name: c for c in [make_gcc_config(ver) for ver in [12, 13, 14]]
+ [make_clang_config(ver, platform) for ver in [16, 17, 18, 19] for platform in ["x86-64", "arm64"]]
+ [make_apple_clang_config(ver) for ver in [15]]
+ [make_msvc_config(release="14.4", version=194)]}
full_matrix = dict(
config=list(configs.values()),
std=[20, 23],
formatting=["std::format", "fmtlib"],
contracts=["none", "gsl-lite", "ms-gsl"],
build_type=["Release", "Debug"],
)
def main():
parser = argparse.ArgumentParser()
# parser.add_argument("-I","--include",nargs="+",action="append")
# parser.add_argument("-X","--exclude",nargs="+",action="append")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--preset", default=None)
parser.add_argument("--debug", nargs="+", default=["combinations"])
parser.add_argument("--suppress-output", default=False, action="store_true")
args = parser.parse_args()
rgen = random.Random(args.seed)
collector = CombinationCollector(full_matrix)
match args.preset:
case None:
pass
case "all":
collector.all_combinations()
case "conan" | "cmake":
collector.all_combinations(formatting="std::format", contracts="gsl-lite", build_type="Debug", std=20)
collector.all_combinations(
filter=lambda me: not me.config.std_format_support,
formatting="fmtlib", contracts="gsl-lite", build_type="Debug", std=20,
)
collector.sample_combinations(rgen=rgen, min_samples_per_value=2)
case "clang-tidy":
collector.all_combinations(config=configs["Clang-18 (x86-64)"])
case "freestanding":
collector.all_combinations(
config=[configs[c] for c in ["GCC-14", "Clang-18 (x86-64)"]],
contracts="none",
std=23,
)
case _:
raise KeyError(f"Unsupported preset {args.preset!r}")
if not collector.combinations:
raise ValueError(f"No combination has been produced")
data = sorted(collector.combinations)
if not args.suppress_output:
print(f"::set-output name=matrix::{json.dumps(data, default=dataclass_to_json)}")
for dbg in args.debug:
match dbg:
case "yaml":
import yaml
json_data = json.loads(json.dumps(data, default=dataclass_to_json))
print(yaml.safe_dump(json_data))
case "json":
print(json.dumps(data, default=dataclass_to_json, indent=4))
case "combinations":
for e in data:
print(f"{e.config!s:17s} c++{e.std:2d} {e.formatting:11s} {e.contracts:8s} {e.build_type:8s}")
case "counts":
print(f"Total combinations {len(data)}")
for (k, v), n in sorted(collector.per_value_counts.items()):
print(f" {k}={v}: {n}")
case "none":
pass
case _:
raise KeyError(f"Unknown debug mode {dbg!r}")
if __name__ == "__main__":
main()

113
.github/job_matrix.py vendored Normal file
View File

@ -0,0 +1,113 @@
import argparse
import json
import typing
import itertools
import random
import dataclasses
from types import SimpleNamespace
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Compiler:
type: typing.Literal["GCC", "CLANG", "APPLE_CLANG", "MSVC"]
version: str | int
cc: str
cxx: str
@dataclass(frozen=True, order=True)
class Configuration:
name: str
os: str
compiler: Compiler
cxx_modules: bool
std_format_support: bool
conan_config: str = ""
lib: typing.Literal["libc++", "libstdc++"] | None = None
def __str__(self):
return self.name
@dataclass(frozen=True, order=True)
class MatrixElement:
config: Configuration
std: typing.Literal[20, 23]
formatting: typing.Literal["std::format", "fmtlib"]
contracts: typing.Literal["none", "gsl-lite", "ms-gsl"]
build_type: typing.Literal["Release", "Debug"]
def dataclass_to_json(obj):
""" Convert dataclasses to something json-serialisable """
if dataclasses.is_dataclass(obj):
return dataclasses.asdict(obj)
raise TypeError(f"Unknown object of type {type(obj).__name__}")
class CombinationCollector:
""" Incremental builder of MatrixElements, allowing successive selection of entries.
"""
def __init__(self, full_matrix: dict[str, list[typing.Any]]):
self.full_matrix = full_matrix
self.combinations: set[MatrixElement] = set()
self.per_value_counts: dict[tuple[str, typing.Any], int] = {(k, v): 0 for k, options in full_matrix.items() for
v in options}
def _make_submatrix(self, **overrides):
new_matrix = dict(self.full_matrix)
for k, v in overrides.items():
if not isinstance(v, list):
v = [v]
new_matrix[k] = v
return new_matrix
def _add_combination(self, e: MatrixElement):
if e in self.combinations or (e.formatting == "std::format" and not e.config.std_format_support):
return
self.combinations.add(e)
# update per_value_counts
for k, v in vars(e).items():
idx = (k, v)
self.per_value_counts[idx] = self.per_value_counts.get(idx, 0) + 1
def all_combinations(self, *, filter: typing.Callable[[MatrixElement], bool] | None = None, **overrides):
""" Adds all combinations in the submatrix defined by `overrides`. """
matrix = self._make_submatrix(**overrides)
keys = tuple(matrix.keys())
for combination in itertools.product(*matrix.values()):
cand = MatrixElement(**dict(zip(keys, combination)))
if filter and not filter(cand):
continue
self._add_combination(cand)
def sample_combinations(self, *, rgen: random.Random, min_samples_per_value: int = 1,
filter: typing.Callable[[MatrixElement], bool] | None = None, **overrides):
""" Adds samples from the submatrix defined by `overrides`, ensuring each individual value appears at least n times. """
matrix = self._make_submatrix(**overrides)
missing: dict[tuple[str, typing.Any], int] = {}
for key, options in matrix.items():
for value in options:
idx = (key, value)
missing[idx] = min_samples_per_value - self.per_value_counts.get(idx, 0)
while missing:
(force_key, force_option), remaining = next(iter(missing.items()))
if remaining <= 0:
missing.pop((force_key, force_option))
continue
choice = {}
for key, options in matrix.items():
choice[key] = force_option if key == force_key else rgen.choice(options)
cand = MatrixElement(**choice)
if filter and not filter(cand):
continue
self._add_combination(cand)
for idx in choice.items():
if missing.pop(idx, 0) <= 0:
continue
remaining = min_samples_per_value - self.per_value_counts.get(idx, 0)
if remaining > 0:
missing[idx] = remaining

View File

@ -35,32 +35,33 @@ on:
- "docs/**" - "docs/**"
jobs: jobs:
cancel-previous:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- run: echo "Cancelling all previous runs of ${{ github.workflow }}-${{ github.ref }}"
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
needs: cancel-previous
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset clang-tidy --seed 42 --debug combinations counts
build: build:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none", "gsl-lite", "ms-gsl"]
std: [20, 23]
config:
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
build_type: ["Release", "Debug"]
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}

View File

@ -34,148 +34,36 @@ env:
CHANNEL: ${{ fromJSON('["testing", "stable"]')[github.ref_type == 'tag' && startsWith(github.ref_name, 'v')] }} CHANNEL: ${{ fromJSON('["testing", "stable"]')[github.ref_type == 'tag' && startsWith(github.ref_name, 'v')] }}
jobs: jobs:
cancel-previous:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- run: echo "Cancelling all previous runs of ${{ github.workflow }}-${{ github.ref }}"
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
needs: cancel-previous
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset conan --seed 42 --debug combinations counts
build: build:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none", "gsl-lite", "ms-gsl"]
std: [20, 23]
config:
- {
name: "MSVC 14.4",
os: windows-2022,
compiler: { type: MSVC, version: 194, cc: "", cxx: "" },
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "GCC-12",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 12,
cc: "gcc-12",
cxx: "g++-12",
},
cxx_modules: "False",
std_format_support: "False",
conan-config: "",
}
- {
name: "GCC-13",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 13,
cc: "gcc-13",
cxx: "g++-13",
},
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "GCC-14",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 14,
cc: "gcc-14",
cxx: "g++-14",
},
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-16",
os: ubuntu-22.04,
compiler:
{
type: CLANG,
version: 16,
cc: "clang-16",
cxx: "clang++-16",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "False",
conan-config: "",
}
- {
name: "Clang-17",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 17,
cc: "clang-17",
cxx: "clang++-17",
},
lib: "libc++",
cxx_modules: "True",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "True",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-18 on Apple M1 (arm64)",
os: macos-14,
compiler:
{
type: CLANG,
version: 18,
cc: "/opt/homebrew/opt/llvm@18/bin/clang-18",
cxx: "/opt/homebrew/opt/llvm@18/bin/clang++",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Apple Clang 15",
os: macos-13,
compiler:
{
type: APPLE_CLANG,
version: "15.0",
cc: "clang",
cxx: "clang++",
},
cxx_modules: "False",
std_format_support: "False",
conan-config: "",
}
build_type: ["Release", "Debug"]
exclude:
- formatting: "std::format"
config: { std_format_support: "False" }
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}
CXX: ${{ matrix.config.compiler.cxx }} CXX: ${{ matrix.config.compiler.cxx }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Generate unique cache id - name: Generate unique cache id
@ -265,14 +153,14 @@ jobs:
run: | run: |
conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \ conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \
-b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=True \ -b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=True \
-o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan-config }} -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan_config }}
- name: Create Conan package - name: Create Conan package
if: matrix.config.compiler.type == 'MSVC' if: matrix.config.compiler.type == 'MSVC'
shell: bash shell: bash
run: | run: |
conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \ conan create . --user mpusz --channel ${CHANNEL} --lockfile-out=package.lock \
-b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=False \ -b mp-units/* -b missing -c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config" -c user.mp-units.build:all=False \
-o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan-config }} -o '&:cxx_modules=${{ matrix.config.cxx_modules }}' -o '&:import_std=${{ env.import_std }}' -o '&:std_format=${{ env.std_format }}' -o '&:contracts=${{ matrix.contracts }}' ${{ matrix.config.conan_config }}
- name: Obtain package reference - name: Obtain package reference
id: get-package-ref id: get-package-ref
shell: bash shell: bash

View File

@ -35,46 +35,33 @@ on:
- "docs/**" - "docs/**"
jobs: jobs:
cancel-previous:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- run: echo "Cancelling all previous runs of ${{ github.workflow }}-${{ github.ref }}"
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
needs: cancel-previous
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset freestanding --seed 42 --debug combinations counts
build: build:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none"]
std: [23]
config:
- {
name: "GCC-14",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 14,
cc: "gcc-14",
cxx: "g++-14",
},
cxx_modules: "False",
std_format_support: "True",
conan-config: "",
}
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "True",
std_format_support: "True",
conan-config: "",
}
build_type: ["Release", "Debug"]
# TODO For some reason Clang-18 Debug with -ffreestanding does not pass CMakeTestCXXCompiler # TODO For some reason Clang-18 Debug with -ffreestanding does not pass CMakeTestCXXCompiler
exclude: exclude:
- build_type: "Debug" - build_type: "Debug"

View File

@ -39,135 +39,33 @@ on:
- "test/**" - "test/**"
jobs: jobs:
cancel-previous:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- run: echo "Cancelling all previous runs of ${{ github.workflow }}-${{ github.ref }}"
generate-matrix:
name: "Generate build matrix for ${{ github.workflow }}"
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
runs-on: ubuntu-24.04
needs: cancel-previous
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.x
- id: set-matrix
run: python .github/generate-job-matrix.py --preset conan --seed 42 --debug combinations counts
test_package: test_package:
name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}" name: "${{ matrix.formatting }} ${{ matrix.contracts }} C++${{ matrix.std }} ${{ matrix.config.name }} ${{ matrix.build_type }}"
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
needs: generate-matrix
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
formatting: ["std::format", "fmtlib"] include: ${{fromJson(needs.generate-matrix.outputs.matrix)}}
contracts: ["none", "gsl-lite", "ms-gsl"]
std: [20, 23]
config:
- {
name: "MSVC 14.4",
os: windows-2022,
compiler: { type: MSVC, version: 194, cc: "", cxx: "" },
cxx_modules: "False",
std_format_support: "True",
}
- {
name: "GCC-12",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 12,
cc: "gcc-12",
cxx: "g++-12",
},
cxx_modules: "False",
std_format_support: "False",
}
- {
name: "GCC-13",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 13,
cc: "gcc-13",
cxx: "g++-13",
},
cxx_modules: "False",
std_format_support: "True",
}
- {
name: "GCC-14",
os: ubuntu-24.04,
compiler:
{
type: GCC,
version: 14,
cc: "gcc-14",
cxx: "g++-14",
},
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Clang-16",
os: ubuntu-22.04,
compiler:
{
type: CLANG,
version: 16,
cc: "clang-16",
cxx: "clang++-16",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "False",
}
- {
name: "Clang-17",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 17,
cc: "clang-17",
cxx: "clang++-17",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True",
}
- {
name: "Clang-18",
os: ubuntu-24.04,
compiler:
{
type: CLANG,
version: 18,
cc: "clang-18",
cxx: "clang++-18",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Clang-18 on Apple M1 (arm64)",
os: macos-14,
compiler:
{
type: CLANG,
version: 18,
cc: "/opt/homebrew/opt/llvm@18/bin/clang-18",
cxx: "/opt/homebrew/opt/llvm@18/bin/clang++",
},
lib: "libc++",
cxx_modules: "False",
std_format_support: "True"
}
- {
name: "Apple Clang 15",
os: macos-14,
compiler:
{
type: APPLE_CLANG,
version: "15.0",
cc: "clang",
cxx: "clang++",
},
cxx_modules: "False",
std_format_support: "False",
}
build_type: ["Release", "Debug"]
exclude:
- formatting: "std::format"
config: { std_format_support: "False" }
env: env:
CC: ${{ matrix.config.compiler.cc }} CC: ${{ matrix.config.compiler.cc }}